Android JNI介紹(三)- Java和Native的互相調用

上一篇文章中,咱們已經瞭解了第一個JNI工程的一些細節,以及對JNI調用進行了一些簡單說明。接下來詳細介紹下如何在Java和Native代碼之間進行互相調用。html

1、第一個NDK工程中的native函數解析

再看一下native函數:java

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對象並回傳。android

咱們看一下NewStringUTF這個函數:git

聲明github

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對象。sass

一個簡單的native函數介紹完了,接下來介紹下Java和Native的調用方法。bash

2、Java中的簽名

在進行Java和Native的交互前,咱們先理一下,他們是怎樣確保對方的惟一性的?oracle

(包括native函數、Java函數、Java變量、Java類)app

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的交互。

3、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;
複製代碼

4、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;
      }
    複製代碼

5、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的交互介紹。

相關文章
相關標籤/搜索