美文网首页Android
NDK<第二篇>:JNI编程

NDK<第二篇>:JNI编程

作者: NoBugException | 来源:发表于2022-10-19 21:50 被阅读0次

    JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作。
    Gradle 3.0之前,AS可以NDK的方式配置JNI环境,Gradle 3.0之后,AS只能用Cmake的方式配置JNI环境。

    一、Java调用C++

    public class JNI {
    
        static {
            // 导入动态库
            System.loadLibrary("jniproject");
        }
    
        /**
         * 加法运算(Java调用C中的方法)
         * @param x
         * @param y
         */
        public native int add(int x, int y);
    
        /**
         * 从Java传递字符串,C进行拼接
         * @param s
         */
        public native String sayHello(String s);
    
        /**
         * 让C代码让每个元素加上10
         * @param intArray
         * @return
         */
        public native int[] increaseArrayEles(int[] intArray);
    
        /**
         * 校验密码是否正确, 如果正确,则返回200,否则返回400
         * @param pwd
         * @return
         */
        public native int checkPwd(String pwd);
    
    }
    
    #include <jni.h>
    #include <string>
    using namespace std;
    
    /*
     * 加法运算
     *
     * Class:     com_nobug_jniproject_JNI
     * Method:    add
     * Signature: (II)I
     */
    extern "C" JNIEXPORT jint JNICALL
    Java_com_nobug_jniproject_JNI_add(JNIEnv *, jobject, jint x, jint y) {
        return x + y;
    }
    
    /*
     * Class:     com_nobug_jniproject_JNI
     * Method:    sayHello
     * Signature: (Ljava/lang/String;)Ljava/lang/String;
     */
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_nobug_jniproject_JNI_sayHello(JNIEnv * env, jobject, jstring oldStr) {
        char* fromJava = (char *) env->GetStringChars(oldStr, JNI_FALSE);
        char* fromC = "123";
        strcat(fromJava, fromC);
        return env->NewStringUTF(fromJava);
    }
    
    /*
     * Class:     com_nobug_jniproject_JNI
     * Method:    increaseArrayEles
     * Signature: ([I)[I
     */
    extern "C" JNIEXPORT jintArray JNICALL
    Java_com_nobug_jniproject_JNI_increaseArrayEles(JNIEnv* env, jobject, jintArray aarFromJava) {
        jint* aar = env->GetIntArrayElements(aarFromJava, JNI_FALSE);
        for (int i=0;i<env->GetArrayLength(aarFromJava);i++) {
            *(aar + i) += 10;
        }
        return aarFromJava;
    }
    
    /*
     * Class:     com_nobug_jniproject_JNI
     * Method:    checkPwd
     * Signature: (Ljava/lang/String;)I
     */
    extern "C" JNIEXPORT jint JNICALL
    Java_com_nobug_jniproject_JNI_checkPwd(JNIEnv* env, jobject, jstring pwd) {
        int code = strcmp(env->GetStringUTFChars(pwd, JNI_FALSE), "123456");
        if (code == 0) {
            return 200;
        }
        return 400;
    }
    

    Java调用C++:

        // 计算两数之和
        int sum = jni.add(1, 3);
        tv.setText("两数之和:" + sum);
        // 字符串拼接
        String newStr = jni.sayHello("Hi");
        tv.setText("newStr:" + newStr);
    
        int[] arr = {1, 2, 3};
        // 数组元素全部加10
        int[] newArr = jni.increaseArrayEles(arr);
        tv.setText("newArr[0]="+newArr[0] + " newArr[1]="+newArr[1] + " newArr[2]="+newArr[2]);
    
        int resultCode = jni.checkPwd("123456");
        tv.setText(resultCode == 200? "密码正确" : "密码错误");
    

    二、C++调用Java

    使用JNI反射,可以实现C++调用Java代码。

    public class JNI {
    
        static {
            // 导入动态库
            System.loadLibrary("jniproject");
        }
    
        public native void notifyAdd();
    
        public native void notifyHelloFromJava();
    
        public native void notifyPrintString();
    
        public native void notifySayHello();
    
        public int add(int x, int y) {
            Log.d("TAG", "C调用了 add 方法");
            return x + y;
        }
    
        public void helloFromJava() {
            Log.d("TAG", "C调用了 helloFromJava 方法");
        }
    
        public void printString(String s) {
            Log.d("TAG", "C调用了 printString 方法:" + s);
        }
    
        public static void sayHello(String s) {
            Log.d("TAG", "C调用了 sayHello 方法:" + s);
        }
    }
    
    #include <jni.h>
    #include <string>
    using namespace std;
    
    /*
     * Class:     com_nobug_jniproject_JNI
     * Method:    notifyAdd
     * Signature: ()V
     */
    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_JNI_notifyAdd(JNIEnv* env, jobject obj) {
        // 得到字节码
        jclass clazz = env->GetObjectClass(obj);
        // 得到methodId
        jmethodID methodId = env->GetMethodID(clazz, "add", "(II)I");
        // 执行方法
        env->CallIntMethod(obj, methodId, 1, 2);
    }
    
    /*
     * Class:     com_nobug_jniproject_JNI
     * Method:    notifyHelloFromJava
     * Signature: ()V
     */
    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_JNI_notifyHelloFromJava(JNIEnv* env, jobject obj) {
        // 得到字节码
        jclass clazz = env->GetObjectClass(obj);
        // 得到methodId
        jmethodID methodId = env->GetMethodID(clazz, "helloFromJava", "()V");
        // 执行方法
        env->CallVoidMethod(obj, methodId);
    }
    
    /*
     * Class:     com_nobug_jniproject_JNI
     * Method:    notifyPrintString
     * Signature: ()V
     */
    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_JNI_notifyPrintString(JNIEnv* env, jobject obj) {
        // 得到字节码
        jclass clazz = env->GetObjectClass(obj);
        // 得到methodId
        jmethodID methodId = env->GetMethodID(clazz, "printString", "(Ljava/lang/String;)V");
        // 执行方法
        env->CallVoidMethod(obj, methodId, env->NewStringUTF("adc"));
    }
    
    /*
     * Class:     com_nobug_jniproject_JNI
     * Method:    notifySayHello
     * Signature: ()V
     */
    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_JNI_notifySayHello(JNIEnv* env, jobject obj) {
        // 得到字节码
        jclass clazz = env->GetObjectClass(obj);
        // 得到methodId
        jmethodID methodId = env->GetStaticMethodID(clazz, "sayHello", "(Ljava/lang/String;)V");
        // 执行方法
        env->CallStaticVoidMethod(clazz, methodId, env->NewStringUTF("adc"));
    }
    

    调用:

    public class MainActivity extends AppCompatActivity {
    
        private ActivityMainBinding binding;
        private JNI jni = new JNI();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            binding = ActivityMainBinding.inflate(getLayoutInflater());
            setContentView(binding.getRoot());
    
            jni.notifyAdd();
            jni.notifySayHello();
            jni.notifyPrintString();
            jni.notifyHelloFromJava();
    
        }
    }
    

    三、C++日志打印到AS控制台

    【第一步】在cmake中配置日志库

    find_library( # Sets the name of the path variable.
            log-lib
    
            # Specifies the name of the NDK library that
            # you want CMake to locate.
            log)
    
    
    target_link_libraries( # Specifies the target library.
            jniproject
    
            # Links the target library to the log library
            # included in the NDK.
            ${log-lib})
    

    【第二步】在C++中输出日志

    #include <android/log.h>
    #define LOG_TAG "native-lib"
    #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
    #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
    #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
    #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
    

    输出日志:

    LOGD("从C++中打印日志");
    

    【第三步】在AS中查看日志

    image.png

    四、如何在C++中更新UI

    UI操作必须含有上下文,native 方法必须在Activity中。

    public native void showToastFromC();
    
    public void showToast() {
        Toast.makeText(MainActivity.this, "show toast", Toast.LENGTH_SHORT).show();
    }
    
    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_MainActivity_showToastFromC(JNIEnv *env, jobject obj) {
        // 得到字节码
        jclass clazz = env->GetObjectClass(obj);
        // 得到methodId
        jmethodID methodId = env->GetMethodID(clazz, "showToast", "()V");
        // 执行方法
        env->CallVoidMethod(obj, methodId);
    }
    

    五、JNI引用

    JNI引用包括:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)

    【1】局部引用

    大多数JNI函数会创建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。
    局部引用只有在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放。
    因此无法跨线程、跨方法使用。
    
    释放一个局部引用有两种方式:
    1、本地方法执行完毕后VM自动释放;
    2、通过DeleteLocalRef手动释放;
    
    一般情况下,我们应该依赖JVM去自动释放 JNI 局部引用;
    但下面两种情况必须手动调用 DeleteLocalRef() 去释放:
    [1](在循环体或回调函数中)创建大量 JNI 局部引用,即使它们并不会被同时使用,因为 JVM 需要足够的空间去跟踪所有的 JNI 引用,所以可能会造成内存溢出或者栈溢出;
    [2] 如果对一个大的 Java 对象创建了 JNI 局部引用,也必须在使用完后手动释放该引用,否则 GC 迟迟无法回收该 Java 对象也会引发内存泄漏;
    

    【2】全局引用

    全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。

    extern "C" JNIEXPORT jstring JNICALL 
    Java_com_xxx_xxx_xxx(JNIEnv * env, jobject instance) { 
        // 定义全局变量
        static jstring globalStr; 
        if(globalStr == NULL){ 
            jstring str = env->NewStringUTF("C++字符串"); 
            //删除全局引用调用 
            DeleteGlobalRef globalStr = static_cast<jstring>(env->NewGlobalRef(str)); 
            //可以释放,因为有了一个全局引用使用str,局部str也不会使用了 
            env->DeleteLocalRef(str); 
        } 
        return globalStr; 
    }
    

    【3】弱引用

    与全局引用类似,弱引用可以跨方法、跨线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象 。
    在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。
    在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象。

    extern "C" JNIEXPORT jclass JNICALL 
    Java_com_xxx_xxx_xxx(JNIEnv * env, jobject instance) {
        static jclass globalClazz = NULL; 
        //对于弱引用 如果引用的对象被回收,则返回true,否则返回false 
        //对于局部和全局引用则判断是否引用java的null对象 
        jboolean isEqual = env->IsSameObject(globalClazz, NULL); 
        if (globalClazz == NULL || isEqual) { 
            jclass clazz = env->GetObjectClass(instance); 
            //删除使用 DeleteWeakGlobalRef 
            globalClazz = static_cast<jclass>(env->NewWeakGlobalRef(clazz)); 
            env->DeleteLocalRef(clazz); 
        } 
        return globalClazz; 
    }
    

    六、JNI_OnLoad函数

    JNI_Onload在执行system.loadLibrary()函数时被调用,主要用途:

    【1】通过JNI_Onload告知VM,当前so库使用的JNI版本,最老的版本问JNI 1.1(JNI_Onload默认返回的是1.1版本)
    【2】可以在JNI_Onload中进行数据的初始化
    【3】可以在JNI_Onload对java类中的native函数进行注册。java类是通过VM来调用本地方法,调用时需要通过VM在so库中寻找该本地函数,如果该本地函数需要频繁调用的话,会花费很多时间,可以在JNI_Onload调用registerNativeMethods,把native函数注册到VM中,减少寻找花费的时间。

    七、静态注册和动态注册

    public class JNI {
    
        static {
            // 导入动态库
            System.loadLibrary("jniproject");
        }
        public native void printLog();
        public native void addFunc(int a,int b);
    }
    

    已知存在两个native方法,printLogaddFunc

    静态注册方法:

    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_JNI_printLog(JNIEnv *env, jobject thiz) {
        LOGV("print verbose log");
        LOGD("print debug log");
        LOGI("print info log");
        LOGW("print warn log");
        LOGE("print error log");
    }
    
    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_JNI_addFunc(JNIEnv *env, jobject thiz, jint a, jint b) {
        int a1,b1,c1;
        a1=a;
        b1=b;
        c1=a1+b1;
        LOGI("addFunc return:%d",c1);
    }
    

    使用 Java_com_nobug_jniproject_JNI_addFunc 为函数名叫做静态注册。

    我们还可以使用 JNI_OnLoad 函数进行动态注册:

    void native_printLog() {
        LOGI("native_printLog");
    }
    
    void native_addFunc(int x, int y) {
        LOGI("native_addFunc");
    }
    
    static const JNINativeMethod methods[] = {
            {"printLog","()V",(void*)native_printLog},
            {"addFunc","(II)V",(void*)native_addFunc}
    };
    
    static int registerNativeMethods(JNIEnv *env){      //native函数的注册
        jclass clazz;
        LOGI("in registerNativeMethods");
        clazz = env->FindClass("com/nobug/jniproject/JNI");
        if(clazz == NULL){
            LOGI("class is null");
            return JNI_FALSE;
        }
        if(env->RegisterNatives(clazz, methods,sizeof(methods)/sizeof(methods[0])) < 0){  //注册函数
            LOGI("注册失败");
            return JNI_FALSE;
        }
        LOGI("注册成功");
        return JNI_TRUE;
    }
    
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,void*) {
        LOGI("in JNI_Onload");
        JNIEnv *env;
        jint result = -1;
        if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
            return -1;
        }
        if (!registerNativeMethods(env)) {
            return -1;
        }
        result = JNI_VERSION_1_6; //返回JNI 1.4版本信息给VM
        return result;
    }
    

    静态注册动态注册 从性能上来讲是一样的,动态注册的函数名看起来更加简单。

    八、JNI线程

    JNIEnv 不支持切换线程,如果在子线程中使用JNIEnv需要特殊处理。

    假设,现在需要更新Android的UI:

    public native void testThread();
    
    public void updateUI() {
        if (Looper.getMainLooper() == Looper.myLooper()) {
            Toast.makeText(MainActivity.this, "updateUI", Toast.LENGTH_SHORT).show();
        } else {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(MainActivity.this, "updateUI", Toast.LENGTH_SHORT).show();
                }
            });
        }
    }
    

    现在需要做的是,让C++在子线程中调用 updateUI 方法。

    C++代码如下:

    #include <jni.h>
    #include <string>
    #include <pthread.h>
    #include <android/log.h>
    #define LOG_TAG "native-lib"
    #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
    #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
    #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
    #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
    
    using namespace std;
    
    // 全局变量
    JavaVM* javaVm;
    struct Context {
        jobject obj;
    };
    
    /**
     * 子线程
     * @param args
     * @return
     */
    void* threadTask(void* args) {
        LOGI("start thread task");
        if (javaVm == NULL) {
            LOGE("javaVm is null");
            return JNI_FALSE;
        }
        JNIEnv* env;
        // 将native线程添加到JVM中
        jint isAttach = javaVm->AttachCurrentThread(&env, 0);
        if (isAttach != JNI_OK) {
            LOGE("attact thread error");
            return JNI_FALSE;
        }
        Context* context = static_cast<Context *>(args);
        // 得到字节码
        jclass clazz = env->GetObjectClass(context->obj);
        // 得到methodId
        jmethodID methodId = env->GetMethodID(clazz, "updateUI", "()V");
        // 执行方法
        env->CallVoidMethod(context->obj, methodId);
        // 分离
        javaVm->DetachCurrentThread();
        delete context;
        context = NULL;
        return JNI_FALSE;
    }
    
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm,void*) {
        LOGI("in JNI_Onload");
        javaVm = vm;
        return JNI_VERSION_1_6;
    }
    
    extern "C" JNIEXPORT void JNICALL
    Java_com_nobug_jniproject_MainActivity_testThread(JNIEnv *env, jobject obj) {
        LOGI("testThread from C");
        Context* context = new Context;
        context->obj = env->NewGlobalRef(obj);
        pthread_t pid;
        // 启动一个线程
        pthread_create(&pid, 0,threadTask, context);
    }
    

    加载动态库是,会执行 JNI_OnLoad,将 JavaVM 做为全局变量。
    在子线程中,将子线程附加到JVM中:

    JNIEnv* env;
    // 将native线程添加到JVM中
    jint isAttach = javaVm->AttachCurrentThread(&env, 0);
    

    通过这样可以获取 JNIEnv 对象。

    [本章完...]

    相关文章

      网友评论

        本文标题:NDK<第二篇>:JNI编程

        本文链接:https://www.haomeiwen.com/subject/cqfyzrtx.html