Android JNI开发(一)

作者: qiuxintai | 来源:发表于2020-07-02 17:57 被阅读0次

    前言

    长文预警,本文是JNI开发的基础知识介绍和使用经验总结,基本上涵盖了Android JNI开发的大多数知识点,因此文章较长。

    1. NDK介绍

    1.1 NDK简介

    NDK,即原生开发套件 (Native Development Kit),它是一套工具集,让我们能够在 Android 应用中使用 C/C++代码。Android Studio首先使用NDK 将 C/C++ 代码编译成原生库,然后使用集成构建系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过JNI(Java原生接口)框架调用原生库中的C/C++函数。
    NDK主要包括了:

    • 从C / C++生成原生代码库所需要的工具和构建文件。
    • 支持Android平台的一系列原生系统(C/C++、JNI)的头文件和库。

    1.2 NDK的使用场景

    我们知道,C/C++是比java更底层的语言,C/C++的执行效率更高、延迟更小,因此在某些情况下我们可能会使用NDK,通过Java代码来调用C/C++代码,完成一些工作。例如:

    • 进一步提升设备性能,以降低延迟,或运行计算密集型应用,如游戏或物理模拟。
    • 重复使用已有的 C 或 C++ 库。
    • 图像、音频、视频处理。

    1.3 下载 NDK 和工具

    要进行NDK开发,我们需要下载以下组件:

    • Android 原生开发套件 (NDK):编译C/C++
    • CMake:C/C++构建工具,类似Makefile,比Makefile使用更加方便。可与 Gradle 搭配使用来构建原生库。
    • LLDB:Android Studio 用于调试原生代码的调试程序。

    我们可以直接在SDKManager中下载这三个组件。


    NDK.png

    2. JNI简介

    JNI (Java Native Interface),即java本地接口,它是为java语言和其它语言交互调用而设计的标准应用程序接口。JNI允许JVM中运行的Java代码与用其他编程语言(例如C,C ++和汇编)编写的应用程序和库进行交互操作。

    实际使用中,JNI的一个重要的用途是让java通过JNI调用C/C++函数。C/C++函数编译后就存放在库文件中,如大家所知,库文件在Windows平台是DLL文件,在UNIX/Linux平台上是SO文件。而库文件和本地环境是强依赖的,是不具备跨平台特性的。因此,想要实现跨平台,就必须提供在所有的目标平台能运行的库文件。例如实现常见的android平台,一般需要提供这些版本的库文件:arm64-v8a, armeabi, armeabi-v7a, mips, mips64, x86, x86_64。

    3. JNI版Hello world

    和学习所有编程语言一样,我们首先来写一个JNI版的Hello world。

    3.1 手动编译和使用JNI

    虽然很繁琐,但是为了完整的了解JNI的开发流程,我们还是要先来说说手动编译和使用JNI的一般流程:

    • a. 编写Test.java,添加native方法
    package com.qxt;
    
    public class Test {
        public static native String sayHello();
    }
    
    • b. 使用javac命令将Test.java编译生成Test.class:
     javac Test.java
    
    • c. 使用javah命令将Test.class编译生成com_qxt_Test.h头文件。注意要指定正确的classpath。
    javah com.qxt.Test
    

    生成的com_qxt_Test.h头文件:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_qxt_Test */
    
    #ifndef _Included_com_qxt_Test
    #define _Included_com_qxt_Test
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_qxt_Test
     * Method:    sayHello
     * Signature: ()Ljava/lang/String;
     */
    JNIEXPORT jstring JNICALL Java_com_qxt_Test_sayHello
      (JNIEnv *, jclass);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    
    • d. 编写com_qxt_Test.cpp实现头文件,并实现具体的函数。
      com_qxt_Test.cpp文件:
    #include <jni.h>
    
    extern "C" JNIEXPORT jstring JNICALL
    Java_com_qxt_Test_sayHello(
            JNIEnv* env,
            jclass clazz) {
        return env->NewStringUTF("Hello world from JNI");
    }
    
    • e. 使用Makefile或者Cmake构建和使用NDK编译com_qxt_Test.cpp或者com_qxt_Test.c源文件,生成libtest.so库文件

    Application.mk:

    APP_ABI := armeabi-v7a arm64-v8a
    

    Android.mk:

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    
    LOCAL_MODULE := test
    LOCAL_C_INCLUDES :=  ./
    LOCAL_SRC_FILES := ./com_qxt_Test.cpp
    
    LOCAL_LDLIBS := -llog
    
    include $(BUILD_SHARED_LIBRARY)
    

    NDK编译命令:

    ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk APP_BUILD_SCRIPT=Android.mk
    
    • f. 在Test.java中使用静态代码块加载库文件:
        static {
            System.loadLibrary("test");
        }
    

    可以看到手动编译和使用JNI的流程是比较麻烦的,不禁让人想起大学时代初学java,老师让大家用记事本手撕java项目的恐惧。不过幸运的是google的Android Studio已经可以帮我们完成这些工作。接下来将介绍使用Android Studio开发JNI。

    3.2 使用Android Studio开发JNI

    我们只要在Android Studio中选择 File > New Project,在Select a Project Template界面选择Native C++,一路Next往下,一个JNI项目就创建好了,如图:


    jni.png

    创建好的项目如上图所示。我们无需再生成头文件以及手动写Makefile了,Android Studio已经帮我们完成了这一切,并且新版的Android Studio已经用使用上更加方便的cmake替换了Makefile了。开发时我们只要编辑java文件和cpp文件、配置好CMakeLists.txt、在build.gradle中指定CMakeLists.txt就可以了。在项目编译时,Android Studio会自动编译cpp文件,并将生成的库文件打包到apk或者aab文件当中。我们来看看主要文件的代码:
    MainActivity.java:

    package com.qxt.test;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.widget.TextView;
    
    public class MainActivity extends Activity {
    
        // 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();
    }
    

    native-lib.cpp

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

    CMakeLists.txt:(为了看起来比较清晰,已删除了无效的注释)

    cmake_minimum_required(VERSION 3.4.1)
    
    add_library( native-lib
                 SHARED
                 native-lib.cpp )
    
    find_library( log-lib
                  log )
    
    target_link_libraries( native-lib
                           ${log-lib} )
    

    刚刚我们已经创建了一个简单的JNI项目。

    我们注意一下,在native-lib.cpp中本地函数stringFromJNI的参数中有个JNIEnv* env。JNIEnv,顾名思义就是JNI环境变量或者叫JNI接口指针。JNIEnv非常重要,JNI中所有的接口函数都是通过JNIEnv来调用的。

    在native-lib.cpp中我们还导入了一个叫jni.h头文件,这个头文件也非常重要。jni.h中定义了JNI的数据类型、数据结构、接口函数、回调函数以及常量等等。在接下来的章节中会逐一介绍相关的重要内容,并举例说明。在最后还会简单介绍cmake的使用。

    另外,由于native、method、function等单词翻译的问题,JNI和Java的概念经常容易搞混,因此我们在接下来的章节中,有如下命名约定:

    • native方法指代Java层的native方法。
    • 本地函数指代JNI层的C/C++函数。

    4. JNI的数据类型和数据结构

    4.1 基本类型

    JNI包括了许多与Java基本类型相对应的基本类型。具体如下表:


    JNI和Java基本类型对照表.png

    4.2 引用类型

    JNI包括了许多与Java引用类型相对应的引用类型。具体如下表:


    JNI和Java引用类型对照表.png

    4.3 字段和方法ID

    除了Java中常用的基本类型和引用类型,JNI还定义了jfieldID和jmethodID。
    jfieldID和jmethodID是常规的C指针类型,它们的声明如下:

    struct _jfieldID;              /* opaque structure */
    typedef struct _jfieldID *jfieldID;   /* field IDs */
    
    struct _jmethodID;              /* opaque structure */
    typedef struct _jmethodID *jmethodID; /* method IDs */
    

    在JNI中调用java对象的变量或者方法时常常会用到jfieldID和jmethodID。

    4.4 jvalue类型

    除了以上介绍的数据类型,JNI还定义了jvalue联合类型,jvalue被用作自变量数组的元素类型。jvalue的声明如下:

    typedef union jvalue {
        jboolean z;
        jbyte    b;
        jchar    c;
        jshort   s;
        jint     i;
        jlong    j;
        jfloat   f;
        jdouble  d;
        jobject  l;
    } jvalue;
    

    jvalue在本地函数调用java方法时,经常会被用做参数类型。

    4.5 类型签名

    JNI同样使用了JVM的类型签名表示。具体如下表:


    JVM类型签名对照表.png

    例如,对于Java方法:

    long f (int n, String s, int[] arr);
    

    它的类型签名为:

    (ILjava/lang/String;[I)J
    

    使用javap命令查看类型签名
    在接下来的章节中会介绍JNI函数的动态注册,在动态注册时,就需要用到类型签名,而类型签名可以通过javap命令来查看,例如,查看Test.java的类型签名,先将Test.java编译成Test.class。然后:

    javap -s Test.class
    

    5. JNI的引用

    在了解完JNI的数据类型之后,我们接下来继续说说JNI的引用。JNI的引用分为四种:

    • 全局引用(GlobalReferences),全局引用全局有效,JVM无法释放和回收全局引用,全局引用必须通过调用DeleteGlobalRef()显式释放。

    分配全局引用:

    jobject NewGlobalRef(JNIEnv *env, jobject obj);
    

    释放全局引用:

    void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
    
    • 弱全局引用(WeakGlobalReferences),弱全局引用是一种特殊的全局引用,与全局引用不同,JVM可以对它进行垃圾回收。弱全局引用可以在使用全局或局部引用的任何情况下使用。当进行垃圾回收时,如果一个对象仅被弱全局引用所引用,则它将被释放。指向该对象的弱全局引用将指向NULL。因此,使用弱全局引用前需要进行非空判断。我们还可以通过IsSameObject将弱引用与NULL进行比较,来检测弱全局引用是否指向NULL。

    分配弱全局引用:

    jweak NewWeakGlobalRef(JNIEnv *env, jobject obj);
    

    释放弱全局引用:

    void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);
    
    • 局部引用(LocalReferences),局部引用在本地方法调用期间有效,局部引用在本地方法返回后自动释放。每个局部引用都要消耗一定数量的JVM资源。因此,使用时需要确保本地方法不会分配过多的局部引用。尽管在本地方法返回Java之后会自动释放局部引用,但是分配过多的局部引用可能会导致JVM在执行本机方法期间耗尽内存(OOM)。

    分配局部引用:

    jobject NewLocalRef(JNIEnv *env, jobject ref);
    

    释放局部引用:

    void DeleteLocalRef(JNIEnv *env, jobject localRef);
    

    查询局部引用容量:

    jint EnsureLocalCapacity(JNIEnv *env, jint capacity);
    
    • 无效引用(InvalidReferences),无效引用一般情况下没有什么用,不展开介绍。

    总的来说,JNI的引用并不复杂,但使用时我们仍需要保持良好的编程习惯:
    a. 一个引用不管能不能被JVM释放和回收,不再使用后立即显示释放。
    b. 在不需要额外引用的情况下,绝不分配新的引用。

    6. JNI使用类和对象

    6.1 类

    DefineClass

    /*
     * @param env: JNI接口指针.
     * @param name: 要定义的类或接口的名称。该字符串以修改后的UTF-8编码。
     * @param loader: 分配给已定义类的类加载器。
     * @param buf: 包含.class文件数据的缓冲区。
     * @param bufLen: 缓冲区长度。
     * @return 返回Java类对象,如果发生错误,则返回NULL。
     */
    jclass DefineClass(JNIEnv *env, const char *name, jobject loader, const jbyte *buf, jsize bufLen);
    

    FindClass

    /*
     * @param env: JNI接口指针。
     * @param name: 完全限定的类名(即,包名,以“ /” 分隔,后跟类名,例如java.lang.String:“java/lang/String”)。
     *              如果名称以“ [”(数组签名字符)开头,则返回数组类。该字符串以修改后的UTF-8编码。
     * @return 返回完全限定的名称的类的对象,如果找不到该类,则返回NULL。
     */
    jclass FindClass(JNIEnv *env, const char *name);
    

    GetSuperclass

    /*
     * @param env: JNI接口指针。
     * @param clazz: 一个Java类对象。
     * @return 返回以clazz所属类的超类或者NULL。
     */
    jclass GetSuperclass(JNIEnv *env, jclass clazz);
    

    IsAssignableFrom

    /*
     * @param env: JNI接口指针。
     * @param clazz1: 第一个类参数。
     * @param clazz2: 第二个类参数。
     * @return 如果以下任一条件为真,则返回JNI_TRUE:
     *         第一个类参数和第二个类参数引用相同的Java类。
     *         第一个类参数是第二个类参数的子类。
     *         第二个类参数为接口,第一个类参数实现了该接口。
     */
    jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1, jclass clazz2);
    

    类的使用实例将在后面的小节中和对象一起介绍。

    6.2 对象

    AllocObject

    /*
     * 分配新的Java对象,而无需调用该对象的任何构造函数。返回对该对象的引用。clazz参数不能为任何数组类。
     * @param env: JNI接口指针。
     * @param clazz: 一个Java类对象。
     * @return 返回Java对象,无法构造该对象则返回NULL。
     */
    jobject AllocObject(JNIEnv *env, jclass clazz);
    

    NewObject

    /*
     * 构造一个新的Java对象。methodID指定要调用的构造方法。必须通过GetMethodID()获取构造方法的methodID。
     * GetMethodID获取构造方法的methodID时方法名为<init>,返回类型为void(V)。clazz参数不能引用数组类。
     * @param env: JNI接口指针。
     * @param clazz: 一个Java类对象。
     * @param methodID:构造函数的methodID。
     * @param ...:构造函数的参数。
     * @param args:构造函数的参数数组。
     * @param args:构造函数参数的va_list。
    * @return 返回Java对象,无法构造该对象则返回NULL。
    */
    jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
    jobject NewObjectA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
    jobject NewObjectV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
    

    GetObjectClass

    /*
     * 获取对象所属的类
     * @param env: JNI接口指针。
     * @param obj:一个Java对象(必须不是NULL)。
     * @return 返回一个Java类对象。
     */
    jclass GetObjectClass(JNIEnv *env, jobject obj);
    

    GetObjectRefType

    /*
     * 获取obj的引用类型 。该参数obj可以是局部引用,全局引用或弱全局引用。
     * @param env: JNI接口指针。
     * @param obj: 局部引用、全局引用或者弱全局引用。
     * @return 返回以下枚举值之一:
     *        如果obj不是有效的引用,则返回JNIInvalidRefType = 0。
     *        如果obj是局部引用类型,则返回JNILocalRefType = 1。
     *        如果obj是全局引用类型,则返回JNIGlobalRefType = 2。
     *        如果obj是弱全局引用类型,则返回JNIWeakGlobalRefType = 3。
    */
    jobjectRefType GetObjectRefType(JNIEnv* env, jobject obj);
    

    IsInstanceOf

    /*
     * 判断对象是否是类的实例。
     * @param env: JNI接口指针。
     * @param obj:一个Java对象
     * @return obj可以强制转换为clazz返回JNI_TRUE; 否则返回JNI_FALSE。一个NULL对象可以强制转换为任何类。
     */
    jboolean IsInstanceOf(JNIEnv *env, jobject obj, jclass clazz);
    

    IsSameObject

    /*
     * 测试两个引用是否引用相同的Java对象。
     * @param env: JNI接口指针。
     * @param ref1:一个Java对象。
     * @param ref2:一个Java对象。
     * @return 如果ref1和ref2引用相同的Java对象,或者两者均为NULL,返回JNI_TRUE; 否则返回JNI_FALSE。
     */
    jboolean IsSameObject(JNIEnv *env, jobject ref1, jobject ref2);
    

    6.3 类和对象的应用实例

    调用类的构造方法创建一个对象。
    MainActivity.java:

    public native Object testClass(int value);
    

    native-lib.cpp

    extern "C" JNIEXPORT jobject JNICALL Java_com_qxt_myapp_MainActivity_testClass(JNIEnv *env, jobject thiz, jint value) {
        jclass clazz = env->FindClass("java/lang/Integer");
        if (clazz != nullptr) {
            jmethodID integerConstructID = env->GetMethodID(clazz, "<init>", "(I)V");
            return env->NewObject(clazz, integerConstructID, value);
        }
        return NULL;
    }
    

    6.4 调用实例字段和方法

    6.4.1 调用实例字段

    GetFieldID

    /*
     * 返回类的实例(非静态)字段的字段ID。该字段由其名称和签名指定。
     * Get<type>Field和Set<type>Field系列函数使用字段ID检索对象字段。
     * GetFieldID() 将使未初始化的类被初始化。GetFieldID()无法用于获取数组的长度,获取数组长度请使用GetArrayLength()代替。
     *
     * @param env: JNI接口指针。
     * @param clazz:一个Java类对象。
     * @param name:字段名称,以\0结尾的UTF-8字符串。
     * @param sig:字段签名,以\0结尾的UTF-8字符串。
    * @return 返回字段ID,如果操作失败返回NULL。
    */
    jfieldID GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    

    Get<type>Field
    NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);

    jobject     GetObjectField(JNIEnv*, jobject, jfieldID);
    jboolean    GetBooleanField(JNIEnv*, jobject, jfieldID);
    jbyte       GetByteField(JNIEnv*, jobject, jfieldID);
    jchar       GetCharField(JNIEnv*, jobject, jfieldID);
    jshort      GetShortField(JNIEnv*, jobject, jfieldID);
    jint        GetIntField(JNIEnv*, jobject, jfieldID);
    jlong       GetLongField(JNIEnv*, jobject, jfieldID);
    jfloat      GetFloatField(JNIEnv*, jobject, jfieldID);
    jdouble     GetDoubleField(JNIEnv*, jobject, jfieldID);
    

    Set<type>Field
    void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);

    void SetObjectField(JNIEnv*, jobject, jfieldID, jobject);
    void SetBooleanField(JNIEnv*, jobject, jfieldID, jboolean);
    void SetByteField(JNIEnv*, jobject, jfieldID, jbyte);
    void SetCharField(JNIEnv*, jobject, jfieldID, jchar);
    void SetShortField(JNIEnv*, jobject, jfieldID, jshort);
    void SetIntField(JNIEnv*, jobject, jfieldID, jint);
    void SetLongField(JNIEnv*, jobject, jfieldID, jlong);
    void SetFloatField(JNIEnv*, jobject, jfieldID, jfloat);
    void SetDoubleField(JNIEnv*, jobject, jfieldID, jdouble);
    
    6.4.2 调用实例方法

    GetMethodID

    /*
     * 返回类或接口的实例(非静态)方法的方法ID。该方法可以在clazz的超类之一中定义,并由继承clazz。该方法由其名称和签名确定。
     * 调用 GetMethodID() 将使未初始化的类被初始化。
     * 要获取构造函数的方法ID,请提供 <init>作为方法名称,并提供 void(V)作为返回类型。
     *
     * @param env: JNI接口指针。
     * @param clazz:一个Java类对象。
     * @param name:方法名称,以\0结尾的UTF-8字符串。
     * @param sig:方法签名,以\0结尾的UTF-8字符串。
    * @return 返回方法ID,如果操作失败返回NULL。
    */
    jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    

    Call<type>Method
    NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
    NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
    NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
    这三个操作族中的方法用于从本地函数中调用Java实例方法,它们的区别仅仅是传参机制不同。

    /*
     * 调用实例方法
     * GetMethodID获取构造方法的methodID时方法名为<init>,返回类型为void(V)。clazz参数不能引用数组类。
     * @param env: JNI接口指针。
     * @param jobject: 一个Java对象。
     * @param methodID:java函数的methodID, 必须通过调用GetMethodID()来获得。
     * @param ...:java函数的参数。
     * @param args:java函数的参数数组。
     * @param args:java函数参数的va_list。
    * @return 返回Java对象,无法构造该对象则返回NULL。
    */
    jobject     CallObjectMethod(JNIEnv*, jobject, jmethodID, ...);
    jobject     CallObjectMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jobject     CallObjectMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jboolean    CallBooleanMethod(JNIEnv*, jobject, jmethodID, ...);
    jboolean    CallBooleanMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jboolean    CallBooleanMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jbyte       CallByteMethod(JNIEnv*, jobject, jmethodID, ...);
    jbyte       CallByteMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jbyte       CallByteMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jchar      CallCharMethod(JNIEnv*, jobject, jmethodID, ...);
    jchar      CallCharMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jchar      CallCharMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jshort     CallShortMethod(JNIEnv*, jobject, jmethodID, ...);
    jshort     CallShortMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jshort     CallShortMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jint       CallIntMethod(JNIEnv*, jobject, jmethodID, ...);
    jint       CallIntMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jint       CallIntMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jlong      CallLongMethod(JNIEnv*, jobject, jmethodID, ...);
    jlong      CallLongMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jlong      CallLongMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jfloat     CallFloatMethod(JNIEnv*, jobject, jmethodID, ...);
    jfloat     CallFloatMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jfloat     CallFloatMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    jdouble    CallDoubleMethod(JNIEnv*, jobject, jmethodID, ...);
    jdouble    CallDoubleMethodV(JNIEnv*, jobject, jmethodID, va_list);
    jdouble    CallDoubleMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    void       CallVoidMethod(JNIEnv*, jobject, jmethodID, ...);
    void       CallVoidMethodV(JNIEnv*, jobject, jmethodID, va_list);
    void       CallVoidMethodA(JNIEnv*, jobject, jmethodID, const jvalue*);
    

    CallNonvirtual<type>Method
    NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, ...);
    NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, const jvalue *args);
    NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass clazz, jmethodID methodID, va_list args);
    这三个操作族中的方法用于从本地函数中调用Java实例方法,它们的区别仅仅是传参机制不同。
    CallNonvirtual<type>Method族方法和Call<type Method族方法的区别在于:
    Call<type>Method基于对象的类来调用方法,而CallNonvirtual<type>Method基于由clazz参数指定的类来调用方法,并从中获取方法ID。方法ID必须从对象的真实类或其超类之一获得。

    jobject     CallNonvirtualObjectMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jobject     CallNonvirtualObjectMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jobject     CallNonvirtualObjectMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jboolean    CallNonvirtualBooleanMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jboolean    CallNonvirtualBooleanMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jboolean    CallNonvirtualBooleanMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jbyte       CallNonvirtualByteMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jbyte       CallNonvirtualByteMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jbyte       CallNonvirtualByteMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jchar       CallNonvirtualCharMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jchar       CallNonvirtualCharMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jchar       CallNonvirtualCharMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jshort      CallNonvirtualShortMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jshort      CallNonvirtualShortMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jshort      CallNonvirtualShortMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jint        CallNonvirtualIntMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jint        CallNonvirtualIntMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jint        CallNonvirtualIntMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jlong       CallNonvirtualLongMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jlong       CallNonvirtualLongMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jlong       CallNonvirtualLongMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jfloat      CallNonvirtualFloatMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jfloat      CallNonvirtualFloatMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jfloat      CallNonvirtualFloatMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    jdouble     CallNonvirtualDoubleMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    jdouble     CallNonvirtualDoubleMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    jdouble     CallNonvirtualDoubleMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    void        CallNonvirtualVoidMethod(JNIEnv*, jobject, jclass, jmethodID, ...);
    void        CallNonvirtualVoidMethodV(JNIEnv*, jobject, jclass, jmethodID, va_list);
    void        CallNonvirtualVoidMethodA(JNIEnv*, jobject, jclass, jmethodID, const jvalue*);
    
    6.4.3 对象的字段和方法使用实例

    MainActivity.java:

    public int age = 20;
    
    public String getAge(String name) {
        return "Hello " + name + ", I'm java method getAge";
    }
    
    public native void testObject();
    

    native-lib.cpp:

    extern "C" JNIEXPORT void JNICALL Java_com_qxt_myapp_MainActivity_testObject(JNIEnv *env, jobject thiz) {
        jclass clazz = env->FindClass("com/qxt/myapp/MainActivity");
        if (clazz != nullptr) {
            //Access object field
            jfieldID ageID = env->GetFieldID(clazz, "age", "I");
            jint ageInt = (jint) env->GetIntField(thiz, ageID);
    
            //Access object method
            jmethodID getAgeID = env->GetMethodID(clazz, "getAge", "(Ljava/lang/String;)Ljava/lang/String;");
            jstring nameStr = env->NewStringUTF("JNI");
            jstring msgStr = (jstring) env->CallObjectMethod(thiz, getAgeID, nameStr);
    
            //Use string, convert jstring to char sequence
            char *name = (char *) env->GetStringUTFChars(nameStr, NULL);
            char *msg = (char *) env->GetStringUTFChars(msgStr, NULL);
            LOGD("[testObject] message:%s; age:%d", msg, ageInt);
    
            env->ReleaseStringUTFChars(nameStr, name);
            env->ReleaseStringUTFChars(msgStr, msg);
        }
        env->DeleteLocalRef(clazz);
    }
    

    6.5 调用静态的字段和方法

    6.5.1 调用静态字段

    GetStaticFieldID

    /*
     * 返回类的静态字段的字段ID。该字段由其名称和签名指定。
     * GetStatic<type>Field和SetStatic<type>Field系列函数使用字段ID检索静态字段。
     * GetFieldID() 将使未初始化的类被初始化。
     *
     * @param env: JNI接口指针。
     * @param clazz:一个Java类对象。
     * @param name:字段名称,以\0结尾的UTF-8字符串。
     * @param sig:字段签名,以\0结尾的UTF-8字符串。
    * @return 返回字段ID,如果操作失败返回NULL。
    */
    
    jfieldID GetStaticFieldID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    

    GetStatic<type>Field
    NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);

    jobject     GetStaticObjectField(JNIEnv*, jclass, jfieldID);
    jboolean    GetStaticBooleanField(JNIEnv*, jclass, jfieldID);
    jbyte       GetStaticByteField(JNIEnv*, jclass, jfieldID);
    jchar       GetStaticCharField(JNIEnv*, jclass, jfieldID);
    jshort      GetStaticShortField(JNIEnv*, jclass, jfieldID);
    jint        GetStaticIntField(JNIEnv*, jclass, jfieldID);
    jlong       GetStaticLongField(JNIEnv*, jclass, jfieldID);
    jfloat      GetStaticFloatField(JNIEnv*, jclass, jfieldID);
    jdouble     GetStaticDoubleField(JNIEnv*, jclass, jfieldID);
    

    SetStatic<type>Field
    void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);

    void        SetStaticObjectField(JNIEnv*, jclass, jfieldID, jobject);
    void        SetStaticBooleanField(JNIEnv*, jclass, jfieldID, jboolean);
    void        SetStaticByteField(JNIEnv*, jclass, jfieldID, jbyte);
    void        SetStaticCharField(JNIEnv*, jclass, jfieldID, jchar);
    void        SetStaticShortField(JNIEnv*, jclass, jfieldID, jshort);
    void        SetStaticIntField(JNIEnv*, jclass, jfieldID, jint);
    void        SetStaticLongField(JNIEnv*, jclass, jfieldID, jlong);
    void        SetStaticFloatField(JNIEnv*, jclass, jfieldID, jfloat);
    void        SetStaticDoubleField(JNIEnv*, jclass, jfieldID, jdouble);
    
    6.5.2 调用静态方法

    GetStaticMethodID

    /*
     * 返回类或接口的静态方法的方法ID。该方法由其名称和签名确定。
     * 调用 GetStaticMethodID() 将使未初始化的类被初始化。
     * 要获取构造函数的方法ID,请提供 <init>作为方法名称,并提供 void(V)作为返回类型。
     *
     * @param env: JNI接口指针。
     * @param clazz:一个Java类对象。
     * @param name:方法名称,以\0结尾的UTF-8字符串。
     * @param sig:方法签名,以\0结尾的UTF-8字符串。
    * @return 返回方法ID,如果操作失败返回NULL。
    */
    jmethodID GetStaticMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig);
    

    CallStatic<type>Method
    NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
    NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, jvalue *args);
    NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);

    jobject     CallStaticObjectMethod(JNIEnv*, jclass, jmethodID, ...);
    jobject     CallStaticObjectMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jobject     CallStaticObjectMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jboolean    CallStaticBooleanMethod(JNIEnv*, jclass, jmethodID, ...);
    jboolean    CallStaticBooleanMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jboolean    CallStaticBooleanMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jbyte       CallStaticByteMethod(JNIEnv*, jclass, jmethodID, ...);
    jbyte       CallStaticByteMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jbyte       CallStaticByteMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jchar      CallStaticCharMethod(JNIEnv*, jclass, jmethodID, ...);
    jchar      CallStaticCharMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jchar      CallStaticCharMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jshort     CallStaticShortMethod(JNIEnv*, jclass, jmethodID, ...);
    jshort     CallStaticShortMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jshort     CallStaticShortMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jint       CallStaticIntMethod(JNIEnv*, jclass, jmethodID, ...);
    jint       CallStaticIntMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jint       CallStaticIntMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jlong      CallStaticLongMethod(JNIEnv*, jclass, jmethodID, ...);
    jlong      CallStaticLongMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jlong      CallStaticLongMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jfloat     CallStaticFloatMethod(JNIEnv*, jclass, jmethodID, ...);
    jfloat     CallStaticFloatMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jfloat     CallStaticFloatMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    jdouble    CallStaticDoubleMethod(JNIEnv*, jclass, jmethodID, ...);
    jdouble    CallStaticDoubleMethodV(JNIEnv*, jclass, jmethodID, va_list);
    jdouble    CallStaticDoubleMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    void       CallStaticVoidMethod(JNIEnv*, jclass, jmethodID, ...);
    void       CallStaticVoidMethodV(JNIEnv*, jclass, jmethodID, va_list);
    void       CallStaticVoidMethodA(JNIEnv*, jclass, jmethodID, const jvalue*);
    
    6.5.3 静态的字段和方法使用实例

    MainActivity.java:

    public static String LOG_TAG = "MainActivity";
    
    public static String getLogTag(String name) {
        return "Hello " + name + ", I'm java static method getLogTag";
    }
    
    public native void testStatic();
    

    native-lib.cpp:

    extern "C" JNIEXPORT void JNICALL Java_com_qxt_myapp_MainActivity_testStatic(JNIEnv *env, jobject thiz) {
        jclass clazz = env->FindClass("com/qxt/myapp/MainActivity");
        if (clazz != nullptr) {
            //Access static field
            jfieldID logTagID = env->GetStaticFieldID(clazz, "LOG_TAG", "Ljava/lang/String;");
            jstring logTagStr = (jstring) env->GetStaticObjectField(clazz, logTagID);
    
            //Access static method
            jmethodID getLogTagID = env->GetStaticMethodID(clazz, "getLogTag", "(Ljava/lang/String;)Ljava/lang/String;");
            jstring nameStr = env->NewStringUTF("JNI");
            jstring msgStr = (jstring) env->CallStaticObjectMethod(clazz, getLogTagID, nameStr);
    
            //Use string, convert jstring to char sequence
            char *logTag = (char *) env->GetStringUTFChars(logTagStr, NULL);
            char *name = (char *) env->GetStringUTFChars(nameStr, NULL);
            char *msg = (char *) env->GetStringUTFChars(msgStr, NULL);
            LOGD("[testStatic] message:%s; logTag:%s", msg, logTag);
            env->ReleaseStringUTFChars(logTagStr, logTag);
            env->ReleaseStringUTFChars(nameStr, name);
            env->ReleaseStringUTFChars(msgStr, msg);
        }
        env->DeleteLocalRef(clazz);
    }
    

    7. JNI的字符串

    7.1 API介绍

    NewString

    /*
     * 创建unicode字符串
     */
    jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize len);
    

    GetStringLength

    /*
     * 获取字符串长度
     */
    jsize GetStringLength(JNIEnv *env, jstring string);
    

    GetStringChars

    /*
     * 将字符串转换成字符数组
     */
    const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);
    

    ReleaseStringChars

    /*
     * 释放字符串
     */
    void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);
    

    GetStringChars和ReleaseStringChars通常成对使用。

    GetStringRegion

    /*
     * 从字符串中的指定位置复制指定长度的字符到字符数组中
     */
    void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize len, jchar *buf);
    

    NewStringUTF

    /*
     * 创建UTF-8字符串
     */
    jstring NewStringUTF(JNIEnv *env, const char *bytes);
    

    GetStringUTFLength

    /*
     * 获取UTF-8字符串长度
     */
    jsize GetStringUTFLength(JNIEnv *env, jstring string);
    

    GetStringUTFChars

    /*
     * 将UTF-8字符串转换成字符数组
     */
    const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
    

    ReleaseStringUTFChars

    /*
     * 释放UTF-8字符串
     */
    void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
    

    GetStringUTFChars和ReleaseStringUTFChars通常成对使用。

    GetStringUTFRegion

    /*
     * 从UTF-8字符串中的指定位置复制指定长度的字符到字符数组中
     */
    void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize len, char *buf);
    

    7.2 字符串的使用实例

    MainActivity.java:

    public native String testString(String s);
    

    native-lib.cpp:

    extern "C" JNIEXPORT jstring JNICALL Java_com_qxt_myapp_MainActivity_testString(JNIEnv *env, jobject thiz, jstring s) {
        //Get java string
        char *msg = (char *) env->GetStringUTFChars(s, NULL);
        std::string hello = msg;
        hello.append("\n");
        hello.append("Hello java");
        env->ReleaseStringUTFChars(s, msg);
        //New java string
        return env->NewStringUTF(hello.c_str());
    }
    

    8. JNI的数组

    8.1 基本类型数组

    New<PrimitiveType>Array
    ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

    jbooleanArray NewBooleanArray(JNIEnv*, jsize);
    jbyteArray    NewByteArray(JNIEnv*, jsize);
    jcharArray    NewCharArray(JNIEnv*, jsize);
    jshortArray   NewShortArray(JNIEnv*, jsize);
    jintArray     NewIntArray(JNIEnv*, jsize);
    jlongArray    NewLongArray(JNIEnv*, jsize);
    jfloatArray   NewFloatArray(JNIEnv*, jsize);
    jdoubleArray  NewDoubleArray(JNIEnv*, jsize);
    

    Get<PrimitiveType>ArrayElements
    NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);

    jboolean* GetBooleanArrayElements(JNIEnv*, jbooleanArray, jboolean*);
    jbyte*    GetByteArrayElements(JNIEnv*, jbyteArray, jboolean*);
    jchar*    GetCharArrayElements(JNIEnv*, jcharArray, jboolean*);
    jshort*   GetShortArrayElements(JNIEnv*, jshortArray, jboolean*);
    jint*     GetIntArrayElements(JNIEnv*, jintArray, jboolean*);
    jlong*    GetLongArrayElements(JNIEnv*, jlongArray, jboolean*);
    jfloat*   GetFloatArrayElements(JNIEnv*, jfloatArray, jboolean*);
    jdouble*  GetDoubleArrayElements(JNIEnv*, jdoubleArray, jboolean*);
    

    Release<PrimitiveType>ArrayElements
    void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);

    void ReleaseBooleanArrayElements(JNIEnv*, jbooleanArray, jboolean*, jint);
    void ReleaseByteArrayElements(JNIEnv*, jbyteArray, jbyte*, jint);
    void ReleaseCharArrayElements(JNIEnv*, jcharArray, jchar*, jint);
    void ReleaseShortArrayElements(JNIEnv*, jshortArray, jshort*, jint);
    void ReleaseIntArrayElements(JNIEnv*, jintArray, jint*, jint);
    void ReleaseLongArrayElements(JNIEnv*, jlongArray, jlong*, jint);
    void ReleaseFloatArrayElements(JNIEnv*, jfloatArray, jfloat*, jint);
    void ReleaseDoubleArrayElements(JNIEnv*, jdoubleArray, jdouble*, jint);
    

    Get<PrimitiveType>ArrayRegion
    void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, NativeType *buf);

    void GetBooleanArrayRegion(JNIEnv*, jbooleanArray, jsize, jsize, jboolean*);
    void GetByteArrayRegion(JNIEnv*, jbyteArray, jsize, jsize, jbyte*);
    void GetCharArrayRegion(JNIEnv*, jcharArray, jsize, jsize, jchar*);
    void GetShortArrayRegion(JNIEnv*, jshortArray, jsize, jsize, jshort*);
    void GetIntArrayRegion(JNIEnv*, jintArray, jsize, jsize, jint*);
    void GetLongArrayRegion(JNIEnv*, jlongArray, jsize, jsize, jlong*);
    void GetFloatArrayRegion(JNIEnv*, jfloatArray, jsize, jsize, jfloat*);
    void GetDoubleArrayRegion(JNIEnv*, jdoubleArray, jsize, jsize, jdouble*);
    

    Set<PrimitiveType>ArrayRegion
    void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize len, const NativeType *buf);

    void SetBooleanArrayRegion(JNIEnv*, jbooleanArray, jsize, jsize, const jboolean*);
    void SetByteArrayRegion(JNIEnv*, jbyteArray, jsize, jsize, const jbyte*);
    void SetCharArrayRegion(JNIEnv*, jcharArray, jsize, jsize, const jchar*);
    void SetShortArrayRegion(JNIEnv*, jshortArray, jsize, jsize, const jshort*);
    void SetIntArrayRegion(JNIEnv*, jintArray, jsize, jsize, const jint*);
    void SetLongArrayRegion(JNIEnv*, jlongArray, jsize, jsize, const jlong*);
    void SetFloatArrayRegion(JNIEnv*, jfloatArray, jsize, jsize, const jfloat*);
    void SetDoubleArrayRegion(JNIEnv*, jdoubleArray, jsize, jsize, const jdouble*);
    

    8.2 引用类型数组

    NewObjectArray

    /*
     * 构造一个新的数组,它包含 elementClass 类的对象。数组中所有元素的初值都设置为initialElement。
     *
     * @param env: JNI接口指针。
     * @param length:数组长度。
     * @param elementClass:数组元素类。
     * @param initialElement:数组元素初值。
     * @return 返回Java数组对象,无法构造数组则返回NULL。
     */
    jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
    

    GetObjectArrayElement

    /*
     * 返回Object数组的元素。
     *
     * @param env: JNI接口指针。
     * @param array:一个Java数组对象。
     * @param index:数组索引。
     * @return 返回一个Java对象。
     */
    jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
    

    SetObjectArrayElement

    /*
     * 设置Object数组的元素。
     *
     * @param env: JNI接口指针。
     * @param array:一个Java数组对象。
     * @param index:数组索引。
     * @param value:新值。
     */
    void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
    

    8.3 获取数组长度

    GetArrayLength

    /*
     * 返回数组中元素的数量。
     *
     * @param env: JNI接口指针。
     * @param array:一个Java数组对象。
     * @return 返回数组的长度。
     */
    jsize GetArrayLength(JNIEnv *env, jarray array);
    

    8.4 数组的使用实例

    MainActivity.java:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    
        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        int[] arr1 = {1, 1};
        String[] arr2 = {"java value"};
        String s = "result:" + Arrays.toString(testArray(arr1, arr2))
                + "\n" + "arr1:" + Arrays.toString(arr1)
                + "\n" + "arr2:" + Arrays.toString(arr2);
        tv.setText(s);
    }
    
    public native int[] testArray(int[] arr1, String[] arr2);
    

    native-lib.cpp:

    extern "C" JNIEXPORT jintArray JNICALL Java_com_qxt_myapp_MainActivity_testArray(JNIEnv *env, jobject thiz, jintArray arr1, jobjectArray arr2) {
        //Update primitive type array item
        jint* _arr1 = env->GetIntArrayElements(arr1, NULL);
        int length1 = env->GetArrayLength(arr1);
        for (int i = 0; i < length1; i++) {
            _arr1[i] = 2;
        }
        env->ReleaseIntArrayElements(arr1, _arr1, 0);
    
        //Update object array
        jstring _arr2 = (jstring) env->GetObjectArrayElement(arr2, 0);
        const char* s = env->GetStringUTFChars(_arr2, NULL);
        LOGD("[testArray] old arr2[0]:%s", s);
        jstring newArr2 = env->NewStringUTF("JNI value");
        env->SetObjectArrayElement(arr2, 0, newArr2);
    
        //create new array
        int array[2] = {3, 3};
        jintArray dst = env->NewIntArray(2);
        env->SetIntArrayRegion(dst, 0, 2, array);
        return dst;
    }
    

    9. JNI的异常

    9.1 API介绍

    Throw

    /*
     * 抛出一个 java.lang.Throwable对象。
     *
     * @param env: JNI接口指针。
     * @param obj:java.lang.Throwable对象。
     * @return 成功返回0,否则返回负数。
     */
    jint Throw(JNIEnv *env, jthrowable obj);
    

    ThrowNew

    /*
     * 使用指定的消息从指定的类构造一个异常对象,并抛出该异常。
     *
     * @param env: JNI接口指针。
     * @param clazz:java.lang.Throwable的子类 
     * @param message:用于构造java.lang.Throwable对象的消息。该字符串以修改后的UTF-8编码。
     * @return 成功返回0,否则返回负数。
     */
    jint ThrowNew(JNIEnv *env, jclass clazz, const char *message);
    

    ExceptionOccurred

    /*
     * 确定是否引发异常。在本地代码调用ExceptionClear()或Java代码处理该异常之前,该异常将一直被抛出 。
     *
     * @param env: JNI接口指针。
     * @return 返回当前正在抛出的异常对象,如果当前没有抛出异常则返回NULL。
     */
    jthrowable ExceptionOccurred(JNIEnv *env);
    

    ExceptionDescribe

    /*
     * 将异常和堆栈的回溯打印到系统错误报告通道,例如stderr。这是为调试提供的便利例程。
     *
     * @param env: JNI接口指针。
     */
    void ExceptionDescribe(JNIEnv *env);
    

    ExceptionClear

    /*
     * 清除当前引发的任何异常。如果当前未引发任何异常,则该工作不生效。
     *
     * @param env: JNI接口指针。
     */
    void ExceptionClear(JNIEnv *env);
    

    FatalError

    /*
     * 引发致命错误,并且不希望VM恢复。此函数不返回。
     *
     * @param env: JNI接口指针。
     * @param msg:错误消息。该字符串以修改后的UTF-8编码。
     */
    void FatalError(JNIEnv *env, const char *msg);
    

    ExceptionCheck

    /*
     * JNI提供的一种便利功能,可以检查正在抛出的异常,而无需创建对异常对象的本地引用。
     *
     * @param env: JNI接口指针。
     * @return 有正在抛出的异常时返回JNI_TRUE;否则返回JNI_FALSE。
     */
    jboolean ExceptionCheck(JNIEnv *env);
    

    9.2 异常使用实例

    MainActivity.java:

    public native void throwException();
    

    native-lib.cpp:

    extern "C" JNIEXPORT void JNICALL Java_com_qxt_myapp_MainActivity_throwException(JNIEnv *env, jobject thiz) {
        jclass clazz = env->FindClass("java/lang/UnsupportedOperationException");
        if (clazz != nullptr) {
            env->ThrowNew(clazz, "Sorry, device is unsupported.");
        }
        env->DeleteLocalRef(clazz);
    }
    

    10. 注册native方法

    java 中的native方法和JNI中的本地函数,需要建立起对应关系才能正常调用。建立对应关系的方式有两种:

    • 静态注册
    • 动态注册

    10.1 静态注册

    本地函数静态注册的格式为:

    extern "C" JNIEXPORT [JNI参数类型] JNICALL Java_[包名][类名][方法名](JNIEnv* env, jobject, [JNI参数类型,参数名])
    

    其中,包名、类名、方法名之间的点用下划线代替,包名、类名、方法名都必须与java文件中声明的native方法完全一致,返回类型和参数类型,根据第三节的Java和JNI的参数对照表,一一对应。从第4节中的代码截图可以看到,用Android Studio刚刚创建的这个项目,Android Studio已经自动为我们选择了自动静态注册的方式。

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

    静态注册的缺点:
    编写不方便,JNI 方法名字必须遵循固定的规则且名字很长,在包名和类名较长的情况下,会显得非常恶心,对于有代码规范强迫症的人,这一点很难忍受。
    程序运行效率不高,首次调用native方法时需要根据方法名在JNI中查找对应的本地函数并建立对应关系,这个过程是比较耗时的。

    静态注册的优点:
    使用方便,本来按手动流程,静态注册使用起来是很麻烦的,但是现在Android Studio已经完美的帮我们解决了这个问题。现在静态注册使用起来非常方便,静态注册的native方法和本地函数之间可以像普通java方法一样进行跳转查看。

    10.2 动态注册

    在调用 System.loadLibrary的加载库文件时,JNI会回调一个叫 JNI_OnLoad()的函数,在JNI_OnLoad函数中做一些初始化相关的工作。JNI还提供一个叫RegisterNatives的函数,用于注册native方法。它的定义为:

    jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);
    

    因此,我们可以在JNI_OnLoad()的函数中调用RegisterNatives函数注册native方法。 这样提前建立native方法和本地函数的对应关系,优化掉静态注册首次调用需要查找的耗时。动态注册整体流程如下:

    • a. 编写cpp实现JNI_Onload()方法。
    • b. 将Java 方法和 C/C++方法通过签名信息一一对应起来,可以使用javap -s xx.class查看签名信息。
    • c. 使用类名和对应起来的方法作为参数,调用RegisterNatives函数注册native方法。

    这里已经写了一个比较标准的模板,可以直接拷贝使用,需要注册新函数时只需要在nativeMethods数组中填写相应的native方法名、签名信息、本地函数名即可。

    jstring stringJNI(JNIEnv *env, jobject) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    static int registerNatives(JNIEnv *env) {
        //要注册的java类的路径(完整的包名和类名)
        const char *className = "com/qxt/myapp/MainActivity";
        /*
         * 要注册的函数列表
         * 参数:
         * 1.java中用native关键字声明的函数名
         * 2.函数签名,格式:(参数类型)返回类型, 可以使用javap -s xx.class查看
         * 3.C/C++中对应函数的函数名(地址)
         * */
        const JNINativeMethod nativeMethods[] = {
                {"stringFromJNI",       "()Ljava/lang/String;", (void *) stringJNI},
        };
    
        jclass clazz = nullptr;
        clazz = env->FindClass(className);
        if (clazz == nullptr) {
            return JNI_FALSE;
        }
        int methodsCount = sizeof(nativeMethods) / sizeof(nativeMethods[0]);
        //注册函数 参数:java类名, 要注册的函数数组 ,要注册函数的数量
        if (env->RegisterNatives(clazz, nativeMethods, methodsCount) < 0) {
            return JNI_FALSE;
        }
        return JNI_TRUE;
    }
    
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
        JNIEnv *env = nullptr;
        if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) {
            return JNI_ERR;
        }
        assert(env != nullptr);
        //registerNatives -> env->RegisterNatives
        if (!registerNatives(env)) {
            return JNI_ERR;
        }
    
        return JNI_VERSION_1_6;
    }
    

    动态注册的缺点:

    • 使用不方便,与静态注册相对的,动态注册的本地函数目前在Android Studio中是无法进行跳转查看的,使用起来相对不方便。

    动态注册的优点:

    • 流程更加清晰可控。
    • 效率更高,提前建立了对应关系,首次调用无需查找。

    11. cmake的简单使用介绍

    在介绍cmake的使用之前,我们先来回顾一下使用Makefile构建和编译C/C++源代码,一般情况下我们需要在Android.mk文件中:

    • 定义头文件路径。
    • 定义依赖的库文件路径。
    • 定义源代码路径。
    • 定义相关的FLAG,ABI等等。(这里只简单介绍cmake的使用,这项不展开讲,有需要的可以去cmake官网看一下。)
    • 定义需要链接的库。

    对应到CMakeLists.txt也是一样的套路,以下是一个比较常见的cmake使用的例子,包含导入头文件、库文件、链接库文件等等:

    #定义支持的cmake的最小版本
    cmake_minimum_required(VERSION 3.4.1)
    
    #导入的库对应的头文件的路径
    include_directories(${CMAKE_SOURCE_DIR}/src/main/cpp/include)
    
    #定义导入的库
    add_library(#库名称
                opencv_java3
                #库的类型,可以为SHARED或者STATIC,根据导入的库填写,.so为SHARED, .a为STATIC
                SHARED
                #声明是导入的
                IMPORTED)
    #定义导入的库文件的路径
    set_target_properties(#库名称
                          opencv_java3
                          PROPERTIES
                          #库路径
                          IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libopencv_java3.so)
    #定义要编译的库
    add_library( #库名称
                 native-lib
                 #库的类型,可以为SHARED或者STATIC,一般为SHARED
                 SHARED
                 #C/C++源文件,需要完整的路径和名称,源文件可以有多个,每个以空格隔开
                 native-lib.cpp )
    
    find_library( log-lib
                  log )
    
    #链接
    target_link_libraries( #编译的库
                           native-lib
                           #需要依赖的库,可以有多个,每个以空格隔开
                           opencv_java3
                           ${log-lib} )
    

    cmake就简单介绍一下,我们只需要知道它是用来替代Makefile用来构建和编译C/C++源文件的,并且了解CMakeLists.txt的一般配置,应付一般的JNI项目开发就完全没有问题了。如果需要了解更多cmake的使用,请看官网教程:https://cmake.org/cmake/help/latest/guide/tutorial/index.html

    12. 参考:

    https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html
    https://blog.csdn.net/qq_20404903/article/details/80662316
    https://www.oschina.net/p/android+ndk?hmsr=aladdin1e1
    感谢几位原作者辛勤付出。

    欢迎交流、点赞、转载,码字不易,转载请注明出处。

    相关文章

      网友评论

        本文标题:Android JNI开发(一)

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