Android JNI介绍(三)- Java和Native的互相调用

5,379 阅读13分钟

本系列文章列表:

Android JNI介绍(一)- 第一个Android JNI工程

Android JNI介绍(二)- 第一个JNI工程的详细分析

Android JNI介绍(三)- Java和Native的互相调用

Android JNI介绍(四)- 异常的处理

Android JNI介绍(五)- 函数的注册

Android JNI介绍(六)- 依赖其他库

Android JNI介绍(七)- 引用的管理

Android JNI介绍(八)- CMakeLists的使用


在上一篇文章中,我们已经了解了第一个JNI工程的一些细节,以及对JNI调用进行了一些简单说明。接下来详细介绍下如何在Java和Native代码之间进行互相调用。

一、第一个NDK工程中的native函数解析

再看一下native函数:

extern "C" JNIEXPORT jstring JNICALL
Java_com_wsy_jnidemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

这个函数的内容也很简单,创建了一个string对象,然后将其转换为jstring对象并回传。

我们看一下NewStringUTF这个函数:

声明

  jstring NewStringUTF(const char* bytes)

art/runtime/jni/jni_internal.cc 中的实现(其他函数也可以参考这里的实现)

  static jstring NewStringUTF(JNIEnv* env, const char* utf) {
    if (utf == nullptr) {
      return nullptr;
    }
    ScopedObjectAccess soa(env);
    ObjPtr<mirror::String> result = mirror::String::AllocFromModifiedUtf8(soa.Self(), utf);
    return soa.AddLocalReference<jstring>(result);
  }

该函数的功能是根据传入的const char*对象创建一个java.lang.String对象。

一个简单的native函数介绍完了,接下来介绍下Java和Native的调用方法。

二、Java中的签名

在进行Java和Native的交互前,我们先理一下,他们是怎样确保对方的唯一性的?

(包括native函数、Java函数、Java变量、Java类)

native:

对于native函数,我们通过函数名、函数参数、返回值类型确保其唯一性。

Java:

对于Java而言,每个函数、变量、类,都有其唯一标识,就是签名,这里着重介绍下Java中的签名。

1. 签名的说明

以下段落摘抄自维基百科:

In computer science, a type signature or type annotation defines the inputs and outputs for a function, subroutine or method. A type signature includes the number of arguments, the types of arguments and the order of the arguments contained by a function. A type signature is typically used during overload resolution for choosing the correct definition of a function to be called among many overloaded forms.

In the Java virtual machine, internal type signatures are used to identify methods and classes at the level of the virtual machine code.
Example: The method String String.substring(int, int) is represented in bytecode as Ljava/lang/String.substring(II)Ljava/lang/String;.
The signature of main() method looks like this:
public static void main(String[] args)
And in the disassembled bytecode, it takes the form ofLsome/package/Main/main:([Ljava/lang/String;)V.
The method signature for the main() method contains three modifiers:
public indicates that the main() method can be called by any object.
static indicates that the main() method is a class method.
void indicates that the main() method has no return value.

译:

在计算机科学中,类型签名或类型注释定义了函数,子程序或方法的输入和输出。类型签名包括参数的数量,参数的类型以及函数包含的参数的顺序。在重载解析期间通常使用类型签名来选择在许多重载函数中正确的那一项。

在Java虚拟机中,内部类型签名用于标识虚拟机代码级别的方法和类。
示例: 方法String String.substring(int,int)在字节码中表示为Ljava/lang/String.substring(II)Ljava/lang/String;
方法main()的签名如下所示:
public static void main(String[] args)
在反汇编的字节码中,它采用Lsome/package/Main/main:([Ljava/lang/String;)V的形式。
main()方法的方法签名包含三个修饰符:
public表示main()方法可以被任何对象调用。
static表示main()方法是一个类方法。
void表示main()方法没有返回值。

简单来说,签名就是能确保一个函数或一个变量的数据。jni在寻找一个java函数或者变量时,一般以如下方式寻找:

  • 寻找函数:需要知道java函数的函数名、返回值类型、参数类型
  • 寻找变量:需要知道java变量的变量名、数据类型

2. 如何获取签名

  • 根据规则自己编写

    oracle相关文档中可以查到:

    Type Signature Java Type
    Z boolean
    B byte
    C char
    S short
    I int
    J long
    F float
    D double
    Lfully-qualified-class; fully-qualified-class
    [type type[]
    ( arg-types ) ret-type method type

    For example, the Java method: long f (int n, String s, int[] arr); has the following type signature: (ILjava/lang/String;[I)J

  • 使用javap命令获取

    • 获取class文件的签名

      javap -s classFile
      

      例如获取MainActivity.class中方法和变量的签名(Java文件在底部github分享中):

      javap -s D:\android-project\study\JNIDemo\app\build\intermediates\classes\debug\com\wsy\jnidemo\MainActivity
      
      Compiled from "MainActivity.java"
      public class com.wsy.jnidemo.MainActivity extends android.support.v7.app.AppCompatActivity {
        public com.wsy.jnidemo.MainActivity();
          descriptor: ()V
      
        protected void onCreate(android.os.Bundle);
          descriptor: (Landroid/os/Bundle;)V
      
        public native java.lang.String testExceptionCrash() throws com.wsy.jnidemo.CustomException;
          descriptor: ()Ljava/lang/String;
      
        public native java.lang.String testExceptionNotCrash(int) throws com.wsy.jnidemo.CustomException;
          descriptor: (I)Ljava/lang/String;
      
        public native void nativeShowToast(android.app.Activity);
          descriptor: (Landroid/app/Activity;)V
      
        public native void testCallJava(com.wsy.jnidemo.MainActivity);
          descriptor: (Lcom/wsy/jnidemo/MainActivity;)V
      
        public native void methodNotExists();
          descriptor: ()V
      
        public void nativeThrowException(android.view.View);
          descriptor: (Landroid/view/View;)V
      
        public void cCallJava(java.lang.String);
          descriptor: (Ljava/lang/String;)V
      
        public void callJavaFromC(android.view.View);
          descriptor: (Landroid/view/View;)V
      
        public void nativeShowToast(android.view.View);
          descriptor: (Landroid/view/View;)V
      
        public void callMethodNotExists(android.view.View);
          descriptor: (Landroid/view/View;)V
      
        public void wrongSampleUsingJNIEnv(android.view.View);
          descriptor: (Landroid/view/View;)V
      
        static {};
          descriptor: ()V
      }
      
    • 获取jar中的class的方法和变量的签名

      javap -classpath XXX.jar -s fullyQualifiedClass
      

      例如获取android.jar中,android.widget.Toast类中方法和变量的签名:

      javap -classpath android.jar -s android.widget.Toast
      
      Compiled from "Toast.java"
      public class android.widget.Toast {
        public static final int LENGTH_LONG;
          descriptor: I
        public static final int LENGTH_SHORT;
          descriptor: I
        public android.widget.Toast(android.content.Context);
          descriptor: (Landroid/content/Context;)V
      
        public void show();
          descriptor: ()V
      
        public void cancel();
          descriptor: ()V
      
        public void setView(android.view.View);
          descriptor: (Landroid/view/View;)V
      
        public android.view.View getView();
          descriptor: ()Landroid/view/View;
      
        public void setDuration(int);
          descriptor: (I)V
      
        public int getDuration();
          descriptor: ()I
      
        public void setMargin(float, float);
          descriptor: (FF)V
      
        public float getHorizontalMargin();
          descriptor: ()F
      
        public float getVerticalMargin();
          descriptor: ()F
      
        public void setGravity(int, int, int);
          descriptor: (III)V
      
        public int getGravity();
          descriptor: ()I
      
        public int getXOffset();
          descriptor: ()I
      
        public int getYOffset();
          descriptor: ()I
      
        public static android.widget.Toast makeText(android.content.Context, java.lang.CharSequence, int);
          descriptor: (Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
      
        public static android.widget.Toast makeText(android.content.Context, int, int) throws android.content.res.Resources$NotFoundException;
          descriptor: (Landroid/content/Context;II)Landroid/widget/Toast;
      
        public void setText(int);
          descriptor: (I)V
      
        public void setText(java.lang.CharSequence);
          descriptor: (Ljava/lang/CharSequence;)V
      }
      

以上介绍了Java中的签名,下面介绍下如何使用签名进行Java和Native的交互。

三、jni中的类型介绍

其中,Java的Object对象传递给C++时类型都用jobject表示,Java中的基础类型都用j基础类型表示,Java中的数组对象类型(Java数组对象类型其实也是Object类型)都用jXXXXArray表示(包含Object数组基础类型数组),具体如下:

  • 基础类型:
/* Primitive types that match up with Java equivalents. */
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */
  • Object(包括数组、异常等):
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;

四、Java调用Native函数

JNI函数的注册一般分为两种:静态注册和动态注册

静态注册通过固定的命名规则映射Java和native函数;
动态注册通过重写JNI_OnLoad函数,用jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)函数将Java中定义的native函数和C/C++中定义的函数进行映射。

以下是具体介绍。

  • 静态注册:

    在Java代码中定义好native方法,并且在C++代码中编写好对应的方法:

    extern "C" JNIEXPORT RETURN_TYPE JNICALL Java_PackageConnectedByUnderline_ClassName_FunctionName(JNIEnv *env, jobject /* this */,  ... params)
    

    例如: com.wsy.jnidemo.MainActivity类中的定义的方法

    public native void testCallJava(MainActivity activity)
    

    对应的C++方法为

    extern "C" JNIEXPORT void JNICALL 
    Java_com_wsy_jnidemo_MainActivity_testCallJava(
            JNIEnv *env,
           jobject /* this */, jobject activity)
    

    com.wsy.jnidemo.MainActivity类中的定义的方法

    public native String testExceptionNotCrash(int i) throws CustomException;
    

    对应的C++方法为

    extern "C" JNIEXPORT jstring JNICALL 
    Java_com_wsy_jnidemo_MainActivity_testExceptionNotCrash(
            JNIEnv *env,
            jobject /* this */, jint i)
    
  • 动态注册:

    编写JNI_OnLoad函数,在其内部实现动态注册,示例代码如下

    Java:

    public native String dynamicRegister();
    

    C++:

    jstring dynamicRegister(JNIEnv *jniEnv, jobject obj) {
        return jniEnv->NewStringUTF("dynamicRegister");
    }
    
    int JNI_OnLoad(JavaVM *javaVM, void *reserved) {
           JNIEnv *jniEnv;
      if (JNI_OK == javaVM->GetEnv((void **) (&jniEnv), JNI_VERSION_1_4)) {
          // 动态注册的Java函数所在的类
          jclass registerClass = jniEnv->FindClass("com/wsy/jnidemo/MainActivity");
          JNINativeMethod jniNativeMethods[] = {
                  //3个参数分别为 Java函数的名称,Java函数的签名(不带函数名),本地函数指针
                   {"dynamicRegister", "()Ljava/lang/String;", (void *) (dynamicRegister)}
          };
           if (jniEnv->RegisterNatives(registerClass, jniNativeMethods,
                                       sizeof(jniNativeMethods) / sizeof((jniNativeMethods)[0])) < 0) {
               return JNI_ERR;
           }
       }
       return JNI_VERSION_1_4;
    }
    

    需要注意到的是,在进行动态注册时,由于动态注册时传入的是函数指针,因此即使函数名发生了变更也不会导致运行时找不到对应的函数,所以不用加extern "C"JNIEXPORT这些标识。

    顺便一提,翻阅Android源码中RegisterNative源码的具体实现时发现了这么一段代码;

        if (*sig == '!') {
            is_fast = true;
            ++sig;
        }
    

    意思就是,如果签名是以!开头的话,会把这个函数标记为fast jnicall,看到后我马上试了下,但是发现加不加好像速度上并没有什么区别。后来回来继续看代码,很遗憾的发现,后面又加了这么一段代码和注释:

      if (UNLIKELY(is_fast)) {
        // There are a few reasons to switch:
        // 1) We don't support !bang JNI anymore, it will turn to a hard error later.
        // 2) @FastNative is actually faster. At least 1.5x faster than !bang JNI.
        //    and switching is super easy, remove ! in C code, add annotation in .java code.
        // 3) Good chance of hitting DCHECK failures in ScopedFastNativeObjectAccess
        //    since that checks for presence of @FastNative and not for ! in the descriptor.
        LOG(WARNING) << "!bang JNI is deprecated. Switch to @FastNative for " << m->PrettyMethod();
        is_fast = false;
        // TODO: make this a hard register error in the future.
      }
    

    意思就是说,这种前面加个!的方式被弃用了,推荐在Java代码中添加@FastNative这个注解,运行后的确也打印出了这段日志。

    然后我尝试在Java代码中添加这个注解,发现加不了,这玩意是在dalvik.annotation.optimization包下的, 普通的开发者用不了。

    下面是相关的代码和注释:

      static jint RegisterNatives(JNIEnv* env,
                                  jclass java_class,
                                  const JNINativeMethod* methods,
                                  jint method_count) {
        if (UNLIKELY(method_count < 0)) {
          JavaVmExtFromEnv(env)->JniAbortF("RegisterNatives", "negative method count: %d",
                                           method_count);
          return JNI_ERR;  // Not reached except in unit tests.
        }
        CHECK_NON_NULL_ARGUMENT_FN_NAME("RegisterNatives", java_class, JNI_ERR);
        ScopedObjectAccess soa(env);
        StackHandleScope<1> hs(soa.Self());
        Handle<mirror::Class> c = hs.NewHandle(soa.Decode<mirror::Class>(java_class));
        if (UNLIKELY(method_count == 0)) {
          LOG(WARNING) << "JNI RegisterNativeMethods: attempt to register 0 native methods for "
              << c->PrettyDescriptor();
          return JNI_OK;
        }
        CHECK_NON_NULL_ARGUMENT_FN_NAME("RegisterNatives", methods, JNI_ERR);
        for (jint i = 0; i < method_count; ++i) {
          const char* name = methods[i].name;
          const char* sig = methods[i].signature;
          const void* fnPtr = methods[i].fnPtr;
          if (UNLIKELY(name == nullptr)) {
            ReportInvalidJNINativeMethod(soa, c.Get(), "method name", i);
            return JNI_ERR;
          } else if (UNLIKELY(sig == nullptr)) {
            ReportInvalidJNINativeMethod(soa, c.Get(), "method signature", i);
            return JNI_ERR;
          } else if (UNLIKELY(fnPtr == nullptr)) {
            ReportInvalidJNINativeMethod(soa, c.Get(), "native function", i);
            return JNI_ERR;
          }
          bool is_fast = false;
          // Notes about fast JNI calls:
          //
          // On a normal JNI call, the calling thread usually transitions
          // from the kRunnable state to the kNative state. But if the
          // called native function needs to access any Java object, it
          // will have to transition back to the kRunnable state.
          //
          // There is a cost to this double transition. For a JNI call
          // that should be quick, this cost may dominate the call cost.
          //
          // On a fast JNI call, the calling thread avoids this double
          // transition by not transitioning from kRunnable to kNative and
          // stays in the kRunnable state.
          //
          // There are risks to using a fast JNI call because it can delay
          // a response to a thread suspension request which is typically
          // used for a GC root scanning, etc. If a fast JNI call takes a
          // long time, it could cause longer thread suspension latency
          // and GC pauses.
          //
          // Thus, fast JNI should be used with care. It should be used
          // for a JNI call that takes a short amount of time (eg. no
          // long-running loop) and does not block (eg. no locks, I/O,
          // etc.)
          //
          // A '!' prefix in the signature in the JNINativeMethod
          // indicates that it's a fast JNI call and the runtime omits the
          // thread state transition from kRunnable to kNative at the
          // entry.
          if (*sig == '!') {
            is_fast = true;
            ++sig;
          }
    
          // Note: the right order is to try to find the method locally
          // first, either as a direct or a virtual method. Then move to
          // the parent.
          ArtMethod* m = nullptr;
          bool warn_on_going_to_parent = down_cast<JNIEnvExt*>(env)->GetVm()->IsCheckJniEnabled();
          for (ObjPtr<mirror::Class> current_class = c.Get();
               current_class != nullptr;
               current_class = current_class->GetSuperClass()) {
            // Search first only comparing methods which are native.
            m = FindMethod<true>(current_class, name, sig);
            if (m != nullptr) {
              break;
            }
    
            // Search again comparing to all methods, to find non-native methods that match.
            m = FindMethod<false>(current_class, name, sig);
            if (m != nullptr) {
              break;
            }
    
            if (warn_on_going_to_parent) {
              LOG(WARNING) << "CheckJNI: method to register \"" << name << "\" not in the given class. "
                           << "This is slow, consider changing your RegisterNatives calls.";
              warn_on_going_to_parent = false;
            }
          }
    
          if (m == nullptr) {
            c->DumpClass(LOG_STREAM(ERROR), mirror::Class::kDumpClassFullDetail);
            LOG(ERROR)
                << "Failed to register native method "
                << c->PrettyDescriptor() << "." << name << sig << " in "
                << c->GetDexCache()->GetLocation()->ToModifiedUtf8();
            ThrowNoSuchMethodError(soa, c.Get(), name, sig, "static or non-static");
            return JNI_ERR;
          } else if (!m->IsNative()) {
            LOG(ERROR)
                << "Failed to register non-native method "
                << c->PrettyDescriptor() << "." << name << sig
                << " as native";
            ThrowNoSuchMethodError(soa, c.Get(), name, sig, "native");
            return JNI_ERR;
          }
    
          VLOG(jni) << "[Registering JNI native method " << m->PrettyMethod() << "]";
    
          if (UNLIKELY(is_fast)) {
            // There are a few reasons to switch:
            // 1) We don't support !bang JNI anymore, it will turn to a hard error later.
            // 2) @FastNative is actually faster. At least 1.5x faster than !bang JNI.
            //    and switching is super easy, remove ! in C code, add annotation in .java code.
            // 3) Good chance of hitting DCHECK failures in ScopedFastNativeObjectAccess
            //    since that checks for presence of @FastNative and not for ! in the descriptor.
            LOG(WARNING) << "!bang JNI is deprecated. Switch to @FastNative for " << m->PrettyMethod();
            is_fast = false;
            // TODO: make this a hard register error in the future.
          }
    
          const void* final_function_ptr = m->RegisterNative(fnPtr);
          UNUSED(final_function_ptr);
        }
        return JNI_OK;
      }
    

五、native函数调用Java函数

1. 调用流程说明

  • 首先获取Java类

    • 根据包名+类名获取Java类
    // 参数name是带包名的类名,例如要获取android.widget.Toast类,那么写法就是
    // jclass cls = env->FindClass("android/widget/Toast");
    jclass FindClass(const char* name)
    
    • 获取jobject的Java类
    // java中的Object对象传给native层后即为jobject,我们可根据jobject获取其Java类
    jclass GetObjectClass(jobject obj)
    
  • 再获取Java类中指定的方法

    • 获取成员方法
    // 参数clazz是java类,name是java方法名称,sig是java方法签名
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    
    • 获取静态方法
    // 参数clazz是java类,name是java方法名称,sig是java方法签名
    jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig)
    
  • 调用Java方法

    • 调用无返回值的成员方法
    // 参数obj是被调用方法的java对象,methodID是使用GetMethodID获取的回传值,后面填方法需要的参数
    void CallVoidMethod(jobject obj, jmethodID methodID, ...)
    
    • 调用带返回值的成员方法
    // 调用返回值类型为Object的方法
    jobject CallObjectMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);
    
    // 调用返回值类型为基本类型的方法
    jint CallIntMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);
    jlong CallLongMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);
    jfloat CallFloatMethod(JNIEnv* env, jobject obj, jmethodID methodId, ...);
    ...
    
    • 调用无返回值的静态方法
    // 参数clazz是被调用方法的类,methodID是使用GetStaticMethodID获取的回传值,后面填方法需要的参数
    void CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...)
    
    • 调用带返回值的静态方法
    // 调用返回值类型为Object的方法
    jobject CallStaticObjectMethod(jclass clazz, jmethodID methodId, ...);
    
    // 调用返回值类型为基本类型的方法
    jint CallStaticIntMethod(JNIEnv* clazz, jclass, jmethodID, ...);
    jlong CallStaticLongMethod(JNIEnv* clazz, jclass, jmethodID, ...);
    jfloat CallStaticFloatMethod(JNIEnv* clazz, jclass, jmethodID, ...);
    ...
    

2. 具体代码示例

首先在MainActivity中定义如下变量和函数:

    private int code = 10;
    
    private String msg = "hello world";

    public void cCallJava(String str) {
        Log.i(TAG, "cCallJava: " + str);
        Toast.makeText(this, str, Toast.LENGTH_SHORT).show();
    }
    
    public native void testCallJava(MainActivity activity);

Native代码的实现:


#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,"wsy" ,__VA_ARGS__)

// 第二个参数this被省略了,实际上,这个参数也是activity,但是在这里我们手动传入演示
extern "C" JNIEXPORT void
JNICALL
Java_com_wsy_jnidemo_MainActivity_testCallJava(
        JNIEnv *env,
        jobject /* this */, jobject activity) {
    
    // 首先获取MainActivity的类
    jclass cls = env->GetObjectClass(activity);
    // 获取这个类中的code成员变量ID
    jfieldID codeId = env->GetFieldID(cls, "code", "I");
    // 获取这个类中的msg成员变量ID
    jfieldID msgId = env->GetFieldID(cls, "msg", "Ljava/lang/String;");
    
    // 获取code成员变量的值
    jint code = env->GetIntField(activity, codeId);

    // 获取msg成员变量的值
    jstring msg = (jstring) env->GetObjectField(activity, msgId);
    
    // 获取java.lang.String对象中的内容
    const char *cMsg = env->GetStringUTFChars(msg, JNI_FALSE);
    // 打印日志
    LOGI("code = %d,msg = %s", code, cMsg);
    // 用完String后要释放
    env->ReleaseStringUTFChars(msg, cMsg);
    
    // 找到MainActivity类中的cCallJava函数
    jmethodID callJavaMethodId = env->GetMethodID(cls, "cCallJava", "(Ljava/lang/String;)V");
    // 创建一个java.lang.String对象,内容如下
    jstring nativeMsg = env->NewStringUTF("java method cCallJava called");
    // 调用java中的cCallJava方法
    env->CallVoidMethod(activity, callJavaMethodId, nativeMsg);

    // 这里的DeleteLocalRef可以不执行,在函数执行完毕后LocalRef会自动释放,
    // 但是在循环次数较多的循环中需要Delete,否则可能会溢出
    env->DeleteLocalRef(msg);
    env->DeleteLocalRef(nativeMsg);
    env->DeleteLocalRef(cls);
}

运行效果:

手机运行
日志内容

以上大致就是JavaNative的交互介绍。