jni-04、靜態註冊、動態註冊、JavaVM與JNIEnv與jobject的地址問題

動態註冊性能優於靜態註冊

// 默認情況下,就是靜態註冊,靜態註冊是最簡單的方式,NDK開發過程中,基本上使用靜態註冊
// Android 系統的C++源碼:基本上都是動態註冊(麻煩)

// 靜態註冊: 優點:開發簡單
// 缺點
// 1.JNI函數名非常長
// 2.捆綁 上層 包名 + 類名
// 3.運行期 纔會去 匹配JNI函數,性能上 低於 動態註冊

// 靜態註冊:
// new Student.方法
// new Student.方法

// 動態註冊:
// s = new Student(); 所以的初始化已經做了
// s.方法
// s.方法

動態註冊 在JNI_OnLoad裏面註冊函數

  • Java native
    public native void dynamicJavaMethod01(); // 動態註冊1
    public native int dynamicJavaMethod02(String valueStr); // 動態註冊2
  • cpp
// 日誌輸出
#include <android/log.h>

#define TAG "JNISTUDY"
// __VA_ARGS__ 代表 ...的可變參數
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG,  __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG,  __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG,  __VA_ARGS__);

JavaVM *jVm = nullptr; // 0x003545 系統亂值,C++11後,取代NULL,作用是可以初始化指針賦值
const char *mainActivityClassName = "com/derry/as_jni_project/MainActivity";

// native 真正的函數
// void dynamicMethod01(JNIEnv *env, jobject thiz) { // OK的
void dynamicMethod01() { // 也OK  如果你用不到  JNIEnv jobject ,可以不用寫
    LOGD("我是動態註冊的函數 dynamicMethod01...");
}

int dynamicMethod02(JNIEnv *env, jobject thiz, jstring valueStr) { // 也OK
    const char *text = env->GetStringUTFChars(valueStr, nullptr);
    LOGD("我是動態註冊的函數 dynamicMethod02... %s", text);
    env->ReleaseStringUTFChars(valueStr, text);
    return 200;
}

/*
     typedef struct {
        const char* name;       // 函數名
        const char* signature; // 函數的簽名
        void*       fnPtr;     // 函數指針
     } JNINativeMethod;
     */
static const JNINativeMethod jniNativeMethod[] = {
        {"dynamicJavaMethod01", "()V",                   (void *) (dynamicMethod01)},
        {"dynamicJavaMethod02", "(Ljava/lang/String;)I", (int *) (dynamicMethod02)},
};


// Java:像 Java的構造函數,如果你不寫構造函數,默認就有構造函數,如果你寫構造函數 覆寫默認的構造函數
// JNI JNI_OnLoad函數,如果你不寫JNI_OnLoad,默認就有JNI_OnLoad,如果你寫JNI_OnLoad函數 覆寫默認的JNI_OnLoad函數
extern "C"
JNIEXPORT jint JNI_OnLoad(JavaVM *javaVm, void *) {
    // this.javaVm = javaVm;
    ::jVm = javaVm;

    // 做動態註冊 全部做完

    JNIEnv *jniEnv = nullptr;
    int result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv), JNI_VERSION_1_6);

    // result 等於0  就是成功    【C庫 FFmpeg 成功就是0】
    if (result != JNI_OK) {
        return -1; // 會奔潰,故意奔潰
    }

    LOGE("System.loadLibrary ---》 JNI Load init");

    jclass mainActivityClass = jniEnv->FindClass(mainActivityClassName);

    // jint RegisterNatives(Class, 我們的數組==jniNativeMethod, 註冊的數量 = 2)
    jniEnv->RegisterNatives(mainActivityClass,
                            jniNativeMethod,
                            sizeof(jniNativeMethod) / sizeof(JNINativeMethod));

    LOGE("動態 註冊沒有毛病");

    return JNI_VERSION_1_6; //  // AS的JDK在JNI默認最高1.6      存Java的JDKJNI 1.8
}

JNIEnv、jobject 都不能跨進程,會崩潰

  • Java 層代碼
public native void naitveThread(); // Java層 調用 Native層 的函數,完成JNI線程

/**
 * TODO 下面是 被native代碼調用的 Java方法
 * 第二部分 JNI線程
 */
public void updateActivityUI() {
    if (Looper.getMainLooper() == Looper.myLooper()) { // TODO C++ 用主線程調用到此函數 ---->  主線程
        new AlertDialog.Builder(MainActivity.this)
                .setTitle("UI")
                .setMessage("updateActivityUI Activity UI ...")
                .setPositiveButton("老夫知道了", null)
                .show();
    } else {  // TODO  C++ 用異步線程調用到此函數 ---->  異步線程
        Log.d(TAG, "updateActivityUI 所屬於子線程,只能打印日誌了..");

        runOnUiThread(new Runnable() { // 哪怕是異步線程  UI操作 正常下去 runOnUiThread
            @Override
            public void run() {

                // 可以在子線程裏面 操作UI
                new AlertDialog.Builder(MainActivity.this)
                        .setTitle("updateActivityUI")
                        .setMessage("所屬於子線程,只能打印日誌了..")
                        .setPositiveButton("老夫知道了", null)
                        .show();
            }
        });
    }
}
  • cpp
class MyContext {
public:
    JNIEnv *jniEnv = nullptr;  // 不能跨線程 ,會奔潰
    jobject instance = nullptr; // 不能跨線程 ,會奔潰
};

void *myThreadTaskAction(void *pVoid) { // 當前是異步線程
    LOGE("myThreadTaskAction run");

    // 需求:有這樣的場景,例如:下載完成 ,下載失敗,等等,必須告訴Activity UI端,所以需要在子線程調用UI端

    // 這兩個是必須要的
    // JNIEnv *env
    // jobject thiz   OK

    MyContext * myContext = static_cast<MyContext *>(pVoid);

    // jclass mainActivityClass = myContext->jniEnv->FindClass(mainActivityClassName); // 不能跨線程 ,會奔潰
    // mainActivityClass = myContext->jniEnv->GetObjectClass(myContext->instance); // 不能跨線程 ,會奔潰

    // TODO 解決方式 (安卓進程只有一個 JavaVM,是全局的,是可以跨越線程的)
    JNIEnv * jniEnv = nullptr; // 全新的JNIEnv  異步線程裏面操作
    jint attachResult = ::jVm->AttachCurrentThread(&jniEnv, nullptr); // 附加當前異步線程後,會得到一個全新的 env,此env相當於是子線程專用env
    if (attachResult != JNI_OK) {
        return 0; // 附加失敗,返回了
    }

    // 1.拿到class
    jclass mainActivityClass = jniEnv->GetObjectClass(myContext->instance);

    // 2.拿到方法
    jmethodID updateActivityUI = jniEnv->GetMethodID(mainActivityClass, "updateActivityUI", "()V");

    // 3.調用
    jniEnv->CallVoidMethod(myContext->instance, updateActivityUI);

    ::jVm->DetachCurrentThread(); // 必須解除附加,否則報錯

    LOGE("C++ 異步線程OK")

    return nullptr;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_derry_as_1jni_1project_MainActivity_naitveThread(JNIEnv *env, jobject job) { // 當前是主線程
    /*pthread_t pid;
    pthread_create(&pid, nullptr, myThreadTaskAction, nullptr);
    pthread_join(pid, nullptr);*/

    MyContext * myContext = new MyContext;
    myContext->jniEnv = env;
    // myContext->instance = job; // 默認是局部引用,會奔潰
    myContext->instance = env->NewGlobalRef(job); // 提升全局引用

    pthread_t pid;
    pthread_create(&pid, nullptr, myThreadTaskAction, myContext);
    pthread_join(pid, nullptr);
}

JavaVM、JNIEnv、jobject地址問題

  • Java
public class MainActivity extends AppCompatActivity {

    public native void nativeFun1();
    public native void nativeFun2(); // 2
    public static native void staticFun3(); // 3
    public static native void staticFun4();
    
    public void clickMethod4(View view) {
        nativeFun1(); // main線程調用的
        nativeFun2(); // main線程調用的
        staticFun3(); // main線程調用的
        // 第四個  new Thread 調用  ThreadClass == clasz 當前函數clazz地址
        new Thread() {
            @Override
            public void run() {
                super.run();
                staticFun4(); // Java的子線程調用
            }
        }.start();
    }
    
     public void clickMethod5(View view) {
        startActivity(new Intent(this, MainActivity2.class));
    }
    
}

public class MainActivity2 extends AppCompatActivity {

    public native void nativeFun5();

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        nativeFun5(); // main線程調用的
    }
}
  • cpp
extern "C"
JNIEXPORT void JNICALL
Java_com_derry_as_1jni_1project_MainActivity_nativeFun1(JNIEnv *env, jobject job) {
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:當前函數env地址, 當前函數jvm地址, 當前函數job地址,  JNI_OnLoad的jvm地址
    LOGE("nativeFun1 當前函數env地址%p,  當前函數jvm地址:%p,  當前函數job地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, job, ::jVm);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_derry_as_1jni_1project_MainActivity_nativeFun2(JNIEnv *env, jobject job) {
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:當前函數env地址, 當前函數jvm地址, 當前函數job地址,  JNI_OnLoad的jvm地址
    LOGE("nativeFun2 當前函數env地址%p,  當前函數jvm地址:%p,  當前函數job地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, job, ::jVm);
}

void * run(void *) { // native的子線程 env地址  和  Java的子線程env地址,一樣嗎  不一樣的
    JNIEnv * newEnv = nullptr;
    ::jVm->AttachCurrentThread(&newEnv, nullptr);
    // 打印:當前函數env地址, 當前函數jvm地址, 當前函數clazz地址,  JNI_OnLoad的jvm地址

    LOGE("run jvm地址:%p,  當前run函數的newEnv地址:%p \n", ::jVm, newEnv);

    ::jVm->DetachCurrentThread();
    return nullptr;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_derry_as_1jni_1project_MainActivity_staticFun3(JNIEnv *env, jclass clazz) {
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:當前函數env地址, 當前函數jvm地址, 當前函數clazz地址,  JNI_OnLoad的jvm地址
    LOGE("nativeFun3 當前函數env地址%p,  當前函數jvm地址:%p,  當前函數clazz地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, clazz, ::jVm);

    // 調用run
    pthread_t pid;
    pthread_create(&pid, nullptr, run, nullptr);
}

// Java子線程調用的
extern "C"
JNIEXPORT void JNICALL
Java_com_derry_as_1jni_1project_MainActivity_staticFun4(JNIEnv *env, jclass clazz) {
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:當前函數env地址, 當前函數jvm地址, 當前函數clazz地址,  JNI_OnLoad的jvm地址
    LOGE("nativeFun4 當前函數env地址%p,  當前函數jvm地址:%p,  當前函數clazz地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, clazz, ::jVm);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_derry_as_1jni_1project_MainActivity2_nativeFun5(JNIEnv *env, jobject job) {
    JavaVM * javaVm = nullptr;
    env->GetJavaVM(&javaVm);

    // 打印:當前函數env地址, 當前函數jvm地址, 當前函數clazz地址,  JNI_OnLoad的jvm地址
    LOGE("nativeFun5 當前函數env地址%p,  當前函數jvm地址:%p,  當前函數job地址:%p, JNI_OnLoad的jvm地址:%p\n", env, javaVm, job, ::jVm);
}
  • 打印的log
nativeFun1 當前函數env地址0x754b4c5460,  當前函數jvm地址:0x754b4bd6c0,  當前函數job地址:0x7fc4fa9d64, JNI_OnLoad的jvm地址:0x754b4bd6c0
nativeFun2 當前函數env地址0x754b4c5460,  當前函數jvm地址:0x754b4bd6c0,  當前函數job地址:0x7fc4fa9d64, JNI_OnLoad的jvm地址:0x754b4bd6c0
nativeFun3 當前函數env地址0x754b4c5460,  當前函數jvm地址:0x754b4bd6c0,  當前函數clazz地址:0x7fc4fa9db4, JNI_OnLoad的jvm地址:0x754b4bd6c0
run jvm地址:0x754b4bd6c0,  當前run函數的newEnv地址:0x754b4c81e0
nativeFun4 當前函數env地址0x7542e22840,  當前函數jvm地址:0x754b4bd6c0,  當前函數clazz地址:0x7518bfe984, JNI_OnLoad的jvm地址:0x754b4bd6c0
nativeFun5 當前函數env地址0x754b4c5460,  當前函數jvm地址:0x754b4bd6c0,  當前函數job地址:0x7fc4fa9cb4, JNI_OnLoad的jvm地址:0x754b4bd6c0

結論

  1. JavaVM全局,綁定當前進程, 只有一個地址
  2. JNIEnv線程綁定, 綁定主線程,綁定子線程
  3. jobject 誰調用JNI函數,誰的實例會給jobject
  • JNIEnv *env 不能跨越線程,否則奔潰, 他可以跨越函數 【解決方式:使用全局的JavaVM附加當前異步線程 得到權限env操作】
  • jobject thiz 不能跨越線程,否則奔潰,不能跨越函數,否則奔潰 【解決方式:默認是局部引用,提升全局引用,可解決此問題】
  • JavaVM 能夠跨越線程,能夠跨越函數
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章