Android 使用JNI

作者: cain_huang | 来源:发表于2018-01-23 18:37 被阅读179次

    在Android Studio 新建一个包含C++的工程,新建完成后,可以在app module目录下看到一个叫做CMakeLists.txt的文件,我们打开该文件,可以看到以下的内容,我对其中的cmake代码做了注释:

    # cmake的最低版本
    cmake_minimum_required(VERSION 3.4.1)
    
    # 添加库
    add_library( #  库名称
                 native-lib
    
                 # 库的类型 静态(STATIC)/动态(SHARED)
                 SHARED
    
                 # 包含的源文件
                 src/main/cpp/native-lib.cpp )
    
    # 查找库
    find_library( # 重新命名
                  log-lib
    
                  # 查找NDK中的库
                  log )
    
    # 链接库
    target_link_libraries( # 库名称,跟前面add_library中的名称保持一致
                           native-lib
    
                           # 需要链接的库名称, 可以链接多个库,用空格或换行符隔开
                           ${log-lib} )
    

    在app/src/main/cpp/目录下看到一个native-lib.cpp的文件。打开文件,我们可以看到诸如以下的代码:

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

    然后我们回到Java层的MainActivity.java文件,可以看到诸如以下的代码:

    public class MainActivity extends AppCompatActivity {
    
        // Used to load the 'native-lib' library on application startup.
        static {
            System.loadLibrary("native-lib");
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            // Example of a call to a native method
            TextView tv = (TextView) findViewById(R.id.sample_text);
            tv.setText(stringFromJNI());
        }
    
        /**
         * A native method that is implemented by the 'native-lib' native library,
         * which is packaged with this application.
         */
        public native String stringFromJNI();
    }
    

    那么,根据这个模板,我们来分析一下,Android JNI的使用方法吧。
    首先,Java层中使用了下面的代码加载了一个库,库的名称是native-lib:

    static {
            System.loadLibrary("native-lib");
        }
    

    这个库就是CMakeLists.txt中的add_library中定义的名称,接着定义了一个native方法:

    public native String stringFromJNI();
    

    然后,我们看到native-lib.cpp文件中定义了同样的方法:

    JNIEXPORT jstring
    
    JNICALL
    Java_com_cgfay_ndklearningproject_MainActivity_stringFromJNI(
            JNIEnv *env,
            jobject /* this */)
    

    这个方法包含了包名,Java层的包名中的 "." 了 "_"。至此,我们了解到了JNI调用的最基本方法。就是通过定义一串长长的包名以及对应方法,通过这样的方式绑定native层。好了,废话不多说,直接进入正题。首先,为了方便使用log输出,我们定义如下方法帮助我们打印输出,用来替代printf:

    #include "android/log.h"
    #define JNI_TAG "JNI_LEARN"
    #define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, JNI_TAG, __VA_ARGS__)
    #define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, JNI_TAG, __VA_ARGS__)
    
    数据类型介绍

    JNI的原始数据类型 —— Primitive Data Types

    Java Type JNI Type C/C++ Type Size
    Boolean Jboolean unsigned char unsigned 8 bits
    Byte jbyte char signed 8 bits
    Char jchar unsigned short unsgined 16 bits
    Short jshort short signed 16 bits
    Int jint int signed 32 bits
    Long jlong long long signed 64 bits
    Float jfloat float 32 bits
    Double jdouble double 64 bits

    JNI 引用类型:

    Java Type Native Type
    java.lang.Class jclass
    java.lang.Throwable jthrowable
    java.lang.String jstring
    Other objects jobject
    java.lang.Object[] jobjectArray
    boolean[] jbooleanArray
    byte[] jbyteArray
    char[] jcharArray
    short[] jshortArray
    int[] jintArray
    long[] jlongArray
    float[] jfloatArray
    double[] jdoubleArray
    Other Arrays jArray
    String 类型
    新建一个Java String
    std::string str = "hello world";
    jstring javaString = env->NewStringUTF(str.c_str())
    
    将Java String转成 C String
        const char *str;
        jboolean isCopy;
        str = env->GetStringUTFChars(javaString, &isCopy);
        if (!str) {
            printf("java string: %s", str);
            if (isCopy == JNI_TRUE) {
                ALOGI("C string is a copy of the Java string");
            } else {
                ALOGI("C string points to actual string");
            }
        }
        env->ReleaseStringUTFChars(javaString, str);
    

    编译运行我们可以打印一下Log:


    打印的log
    Array 类型

    Array类型操作如下:

    extern "C"
    JNIEXPORT jintArray JNICALL
    Java_com_cgfay_ndklearningproject_JniHelper_arrayFromJNI(JNIEnv *env, jobject instance) {
    
        // 新建一个 int 数组
        jintArray javaArray = env->NewIntArray(10);
        if (javaArray != 0) {
            // 赋值操作
            jint *h = new jint[10];
            for (int i = 0; i < 10; ++i) {
                h[i] = i;
            }
            env->SetIntArrayRegion(javaArray, 0, 10, h);
    
            // 从jArray中取得数据
            jint nativeArray[10];
            env->GetIntArrayRegion(javaArray, 0, 10, nativeArray);
            for (int i = 0; i < sizeof(nativeArray) / sizeof(jint); ++i) {
                ALOGI("nativeArray: %d", nativeArray[i]);
            }
    
            // 使用GetIntArrayElements方法获取的是一个指针,是需要释放的
            jint *nativeDirectArray;
            jboolean isCopy;
            nativeDirectArray = env->GetIntArrayElements(javaArray, &isCopy);
            for (int i = 0; i < 10; ++i) {
                ALOGI("nativeDirectArray: %d", nativeDirectArray[i]);
            }
            if (isCopy == JNI_TRUE) {
                ALOGI("native direct array is a copy of the java array");
            } else {
                ALOGI("native direct array points to actual array");
            }
            // 释放类型
            // 0: 复制内容并释放本地数组
            // JNI_CIMMIT: 复制内容但不释放本地数据,用于定期更新Java数据
            // JNI_ABORT: 释放本地数组,但不复制内容
            env->ReleaseIntArrayElements(javaArray, nativeDirectArray, 0);
        }
    
        return javaArray;
    }
    
    NIO操作

    NIO(Native I/O)在缓冲管理区、大规模网络和文件I/O以及字符集支持方面的性能有所改进。NIO缓冲区的数据传送性能较好,更适合在原生代码和Java应用程序之间传送大量数据。

    创建字节缓冲区
    unsigned char *buffer  = (unsigned char *) malloc(1024);
    ...
    jobject directBuffer = env->NewDirectByteBuffer(buffer, 1024);
    
    备注:

    原生方法中的内存分配超出虚拟机管理范围,且不能用虚拟机的垃圾回收器回收原生方法中的内存。原生函数应该手动释放未使用的内存以避免内存泄漏。

    直接缓冲区获取

    直接获取原生字节数组的内存地址:

    unsigned char* buffer = (unsigned char *) env->GetDirectBufferAddress(directBuffer);
    
    访问域

    Java有两类域: 实例域 和 静态域。所有实例共享一个静态域,实例有自己的实例域副本。访问域的操作如下:
    JniHelper中增加两个变量和一个native方法:

        // 静态域
        private static String staticField = "Static Field";
        // 实例域
        private String instanceField = "Instance Field";
        // 静态域与实例域
        public native void instanceFieldAndStaticField();
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_cgfay_ndklearningproject_JniHelper_instanceFieldAndStaticField(JNIEnv *env,
                                                                            jobject instance) {
        // 获取class对象
        jclass clazz =  env->GetObjectClass(instance);
        // 获取实例域的Id
        jfieldID instanceFieldId = env->GetFieldID(clazz, "instanceField", "Ljava/lang/String;");
        // 根据实例域Id获取实际的java 的字符串,也就是获取实例域
        jstring instanceField = (jstring) env->GetObjectField(instance, instanceFieldId);
        const char *instanceStr;
        jboolean isCopy;
        instanceStr = env->GetStringUTFChars(instanceField, &isCopy);
        ALOGI("instance field: %s", instanceStr);
        env->ReleaseStringUTFChars(instanceField, instanceStr);
    
        // 获取静态域的Id
        jfieldID staticFieldId = env->GetStaticFieldID(clazz, "staticField", "Ljava/lang/String;");
        // 根据静态域Id获取实际的Java的字符串,也就是获取静态域
        jstring staticField = (jstring) env->GetStaticObjectField(clazz, staticFieldId);
        const char *staticStr;
        staticStr = env->GetStringUTFChars(staticField, 0);
        ALOGI("instance field: %s", staticStr);
        env->ReleaseStringUTFChars(staticField, staticStr);
    
    }
    

    其中,"Ljava/lang/String;" 这个后续会讲到。现在你只要知道这是获取String 类型就行。
    打印输出的Log如下:


    访问域输出
    备注:

    获取单个域值需要调用两到三个JNI函数,存在性能问题,一般情况下都是直接从Java层传输native方法调用,而不是让Native方法去访问java中的域值。

    调用方法

    java有两类方法 —— 实例方法和静态方法。下面介绍如何使用Native方法反过来调用java层的方法。
    在JniHelper中添加三个方法:

        /**
         * native调用的静态方法
         * @return
         */
        private static String staticMethod() {
            return "Static Method";
        }
    
        /**
         * native调用的实例方法
         * @return
         */
        private String instanceMethod() {
            return "Instance Method";
        }
    
        // 静态方法与实例方法
        public native void instanceMethodAndStaticMethod();
    

    访问方法的方式如下:

    extern "C"
    JNIEXPORT void JNICALL
    Java_com_cgfay_ndklearningproject_JniHelper_instanceMethodAndStaticMethod(JNIEnv *env,
                                                                              jobject instance) {
    
        // 获取class对象
        jclass clazz = env->GetObjectClass(instance);
        // 获取实例方法Id
        jmethodID instanceMethodId = env->GetMethodID(clazz, "instanceMethod", "()Ljava/lang/String;");
        jstring instanceMethodResult = (jstring) env->CallObjectMethod(instance, instanceMethodId);
        const char *instanceStr;
        instanceStr = env->GetStringUTFChars(instanceMethodResult, 0);
        ALOGI("instance method: %s", instanceStr);
        env->ReleaseStringUTFChars(instanceMethodResult, instanceStr);
    
        // 获取静态方法Id
        jmethodID staticMethodId = env->GetStaticMethodID(clazz, "staticMethod", "()Ljava/lang/String;");
        jstring staticMethodResult = (jstring) env->CallStaticObjectMethod(clazz, staticMethodId);
        const char *staticStr;
        staticStr = env->GetStringUTFChars(staticMethodResult, 0);
        ALOGI("static method call: %s", staticStr);
        env->ReleaseStringUTFChars(staticMethodResult, staticStr);
    }
    

    其中,"()Ljava/lang/String;" 表示的是方法的签名,包括参数和返回值。后续将会介绍到。编译运行,则可以看到打印输出一下Log:


    访问方法输出
    备注:

    为了性能考虑,一般会缓存常用的方法Id。比如native层执行操作完成之后,通过这样的方式回调输出。

    域和方法描述符

    前面提到的获取于域ID 和 方法Id 均分别需要域描述符和方法描述符。描述符可以通过下面的表格对应的签名映射获得:

    Java 类型 签名
    Boolean Z
    Byte B
    Char C
    Short S
    Int I
    Long J
    Float F
    Double D
    类全称 L类全称
    type[] [type
    void V
    方法类型 (参数类型)返回值类型

    比如前面的 "()Ljava/lang/String;" 表示的是参数为void,返回值为String的方法。java/lang/String; 表示的是类的全称,也就是java.lang.String。
    我们可以通过使用javap 来反汇编程序,得到各种类里面的签名

    异常处理

    native中的异常行为与Java中的有所不同。我们可以在native层发生异常后回传给java层做处理。

    捕获与抛出异常

    在JniHelper中定义两个方法:

        /**
         * native层捕获异常与抛出异常
         * @throws NullPointerException
         */
        private void throwingMethod() throws NullPointerException {
            throw new NullPointerException("Null Pointer!");
        }
        // 捕获异常
        public native void acessMethods();
    

    native层的实现如下:

    /**
     * 捕获异常与抛出异常
     */
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_cgfay_ndklearningproject_JniHelper_acessMethods(JNIEnv *env, jobject instance) {
    
        jthrowable ex;
        jclass clazz = env->GetObjectClass(instance);
        // 获取跑出异常的方法
        jmethodID throwingMethodId = env->GetMethodID(clazz, "throwingMethod", "()V");
        // 调用方法
        env->CallVoidMethod(instance, throwingMethodId);
        // 查询虚拟机中是否有挂起的异常(显式做异常处理)
        ex = env->ExceptionOccurred();
        if (ex != 0) {
            // 描述
            env->ExceptionDescribe();
            // 使用完成需要清除异常
            env->ExceptionClear();
    
            // 抛出异常
            jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException");
            if (newExcCls) {
                env->ThrowNew(clazz, "Exception message");
            }
        }
    
    }
    

    调用acessMethods 方法后,你就会看到如下所示的异常:


    异常处理
    局部和全局引用
    局部引用

    大多数JNI函数返回局部引用。可以通过FindClass函数返回一个局部引用。局部引用不能再后续的调用中被缓存以及重用。当一个原生方法返回时,它被自动释放,也可以调用DeleteLocalRef(clazz);方法释放原生代码

    全局引用

    全局引用在原生方法的后续调用过程中依然有效,除非被显式地释放掉。全局方法的使用如下:
    1、创建全局引用

    jclass localClazz;
    jclass globalClazz;
    ...
    localClazz = env->FindClass("java/lang/String");
    globalClazz = env->NewGlobalRef(localClazz);
    ...
    env->DeleteLocalRef(localClazz);
    

    2、删除全局引用

    env->DeleteGlobalRef(globalClazz);
    
    弱全局引用

    1、创建弱全局引用

    jclass weakGlobalClazz = env->NewWeakGlobalRef(localClazz);
    

    2、弱全局引用的有效性检验

    if (env->IsSameObject(weakGlobalClazz, NULL) == JNI_FALSE) {
    // 对象仍然处于活动状态且可以使用
    } else {
    // 对象回收,不能使用
    }
    

    3、删除弱全局引用

    env->DeleteWeakGlobalRef(weakGlobalClazz);
    
    线程

    虚拟机支持运行时原生代码,但有约束:
    只在原生方法执行期间以及正在执行原生方法的线程环境下局部引用是有效的,局部引用不能在多线程间共享
    被传递给每个原生方法的JNIEnv接口指针在于方法调用相关的线程中也是有效的,它不能被其他线程缓存或使用

    同步

    JNI允许原生代码利用Java对象同步,虚拟机保证存储监视器的线程能够安全执行,而其他线程等待监视器对象变成可用状态。
    Java 同步代码块

    synchronized(obj) {
    // 同步线程安全代码块
    }
    

    等价原生代码块:

    if (env->MonitorEnter(obj) == JNI_OK) {
    // 错误处理
    }
    
    // 同步线程安全代码块
    
    
    if (env->MonitorExit(obj) == JNI_OK) {
    // 错误处理
    }
    
    原生线程

    JNI通过JavaVM接口指针提供了AttachCurrentThread 函数便于让原生代码将原生线程附着到虚拟机上。JavaVM接口指针应该尽早被缓存,否则不能被获取
    将当前线程与虚拟机附着和分离:

    JavaVM *cachedJvm;
    
    JNIEnv *env;
    
    // 将当前线程附着到虚拟机
    cachedJvm->AttachCurrentThread(&env, NULL);
    
    /* 可以用JNIEnv 接口实现线程与Java应用程序的通信 */
    ...
    
    // 将当前线程与虚拟机分离
    cachedJvm->DetachCurrentThread();
    

    线程使用例子如下:
    在JniHelper中添加以下方法:

        /**
         * native 线程回调
         */
        private String threadCallBack() {
            return "thread call back";
        }
    
        // 开始线程
        public native void startThread();
    

    native层实现如下:

    
    /**
     * 线程操作例子
     */
    
    JavaVM* javaVM;
    jobject gInstance;
    
    static void *thread_sample(void* argv) {
        JNIEnv *env;
        // 从全局的JavaVM中获取环境变量
        javaVM->AttachCurrentThread(&env, NULL);
        ALOGI("log in another thread!!!!");
        // 获取Java层对应的类
        jclass clazz = (jclass) env->GetObjectClass(gInstance);
        // 获取方法
        jmethodID id = env->GetMethodID(clazz, "threadCallBack", "()Ljava/lang/String;");
        jstring jStr = (jstring) env->CallObjectMethod(gInstance, id);
        const char *instanceStr;
        instanceStr = env->GetStringUTFChars(jStr, 0);
        ALOGI("instance method: %s", instanceStr);
        env->ReleaseStringUTFChars(jStr, instanceStr);
        sleep(1);
        // 删除全局变量
        env->DeleteGlobalRef((jobject)gInstance);
        javaVM->DetachCurrentThread();
    
        return NULL;
    }
    
    extern "C"
    JNIEXPORT void JNICALL
    Java_com_cgfay_ndklearningproject_JniHelper_startThread(JNIEnv *env, jobject instance) {
    
        // 获取JavaVM
        env->GetJavaVM(&javaVM);
        // 创建全局对象
        gInstance = env->NewGlobalRef(instance);
    
        // 创建新线程
        pthread_t thread;
    
        pthread_create(&thread, NULL, thread_sample, NULL);
    }
    

    最后编译运行,可以看到以下的Log:


    线程使用log

    工程的Github 地址如下:
    NDKLearningProject

    相关文章

      网友评论

        本文标题:Android 使用JNI

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