AI智能
改变未来

Android Studio下的Jni编程总结

文章目录

  • 1. jni的注册以及编译
  • 1.1 静态注册jni
  • 1.1.1 使用Android Studio创建NDK开发工程
  • 1.1.2 手动创建jni源文件
  • 1.2 动态注册jni
  • 1.3 jni编译的重要文件CMakeLists.txt
  • 2. Jni接口编写
    • 2.1 Jni的特殊描述符以及主要变量
    • 2.1.1 Jni中的特殊描述符
    • 2.1.2 JNIEnv和jobject类型总结
  • 2.2 Jni的主要函数
  • 2.3 Jni的函数签名
  • 2.4 Jni访问字符串
    • 2.4.1 关于编码方式
    • 2.4.2 需要用到的函数
  • 3 Jni中的反射
    • 3.1 访问成员属性
    • 3.2 访问成员方法

    1. jni的注册以及编译

    1.1 静态注册jni

    1.1.1 使用Android Studio创建NDK开发工程

    使用Android Studio直接创建jni接口比较简单,集成开发工具为我们做了大量工作,且默认为静态注册,下面首先简单梳理一下使用最新版本Android Studio(4.0.1)创建NDK开发工程的方法:

    1. 首先新建工程,选择C++工程;

    2. 填写工程信息后选择C++可支持的版本以及编译选项,这里暂时选择默认的toolchain;

    3. 完成后即可看到AS为我们自动生成NDK开发的c++目录,对应的CMakeLists.txt以及cpp源文件示例。接下来只需要在cpp目录下创建需要的其他c++源文件即可。在Android Studio自动配置的静态注册中,Java文件新定义任何接口都可以自动在cpp文件中生成对应的接口声明。

    Android Studio自动生成的c++接口声明示例:

    #include <jni.h>#include <string>extern \"C\" JNIEXPORT jstring JNICALLJava_com_example_jnitest_jniutil_JniManager_stringFromJNI(JNIEnv* env,jobject /* this */) {std::string hello = \"Hello from C++\";return env->NewStringUTF(hello.c_str());}extern \"C\"JNIEXPORT jint JNICALLJava_com_example_jnitest_jniutil_JniManager_getIDFromJni(JNIEnv *env, jobject thiz) {// TODO: implement getIDFromJni()return 0;}

    1.1.2 手动创建jni源文件

    由于IDE为我们完成了过多工作使我们无法看清jni在创建过程中具体做了什么,因此这里简单尝试一下手动进行静态注册jni的过程,稍微降低对IDE的依赖。

    创建jni主要需要三个步骤:创建必要的源文件、添加CMakeLists.txt、配置gradle。

    1) 创建相关源文件:在原有的普通Android工程中,创建需要调用Native方法的java源文件,如:

    package com.example.myappdemo.nativeManager;public class NativeManager {public native String getName(int id); // 本地方法声明}

    然后针对这个Java文件创建cpp的头文件,可以使用jdk指令:

    step1: javac NativeManage.java // 生成class文件

    step2: javah -jni com.example.jnitest2.nativeManager.NativeManager // 生成jni头文件

    但是实验发现以上命令无法再适用于jdk10以上了,jdk10以后javah被包含在javac中,因此以上命令可以合并为:

    javac -h -jni NativeManager.java // 直接在当前目录下创建jni目录并生成头文件,根据项目不同要求,可以将路径更改为c++目录下

    生成的头文件如下:

    /* DO NOT EDIT THIS FILE - it is machine generated */#include <jni.h>/* Header for class com_example_myappdemo_nativeManager_NativeManager */#ifndef _Included_com_example_myappdemo_nativeManager_NativeManager#define _Included_com_example_myappdemo_nativeManager_NativeManager#ifdef __cplusplusextern \"C\" {#endif/** Class:     com_example_myappdemo_nativeManager_NativeManager* Method:    getName* Signature: (I)Ljava/lang/String;*/JNIEXPORT jstring JNICALL Java_com_example_myappdemo_nativeManager_NativeManager_getName(JNIEnv *, jobject, jint);#ifdef __cplusplus}#endif#endif

    注意到h文件方法的名字结构是比较规则的,是以Java_包名_类名_方法名的结构出现,这也是静态注册的特点之一。

    然后创建同名的cpp文件(与h文件同名方便识别),如下:

    #include \"com_example_myappdemo_nativeManager_NativeManager.h\"JNIEXPORT jstring JNICALL Java_com_example_myappdemo_nativeManager_NativeManager_getName(JNIEnv *env, jobject obj, jint id) {std::string hello = \"Hello from C++\";return env->NewStringUTF(hello.c_str());}
    1. 创建CMakeLists.txt

    CMakeLists.txt是用来生成makefile(或者project文件)的工具,用于编译c++文件。具体CMakeLists的细节不在这里介绍,这里主要介绍基本的CMakeLists.txt如何创建和实现。

    首先创建CMakeLists.txt文件,然后列出编译基本规则,如下:

    // 这里是要求的最低cmake版本,但不是指定版本,指定版本在gradle中说明cmake_minimum_required(VERSION 3.4.1)// 这里是指定include文件夹路径,是CMakeLists.txt的相对路径,include由开发者根据情况自行创建include_directories(include)// 需要添加的library,native-lib为so库的名字;SHARED表示类型为共享库;add_library( native-lib SHARED// 编译该库需要的源文件com_example_myappdemo_nativeManager_NativeManager.cppcom_example_myappdemo_nativeManager_NativeManager.h  )
    1. 在gradle中配置CMakeLists.txt

    Android Studio是通过gradle完成代码编译的,因此CMakeLists.txt也需要配置在gradle中才能实现动态库的编译和加载。app/build.gradle中需要配置CMakeLists的位置有两处,一处为defaultConfig选项,一处为android选项中.

    其中在defaultConfig中externalNativeBuild的cmake参数为编译选项,如下所示:

    defaultConfig {applicationId \"com.example.myappdemo\"minSdkVersion 26targetSdkVersion 29versionCode 1versionName \"1.0\"testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"externalNativeBuild {cmake {cppFlags \"-std=c++11\" // 此处表示使用c++11标准}}}

    在android中的externalNativeBuild需要标时CMakeLists.txt的路径和使用的cmake版本:

    externalNativeBuild {cmake {// 这里的路径是相对build.gradle的path \"src/main/cpp/CMakeLists.txt\"version \"3.10.2\"}}

    1.2 动态注册jni

    动态注册jni不需要静态注册那样严格地要求方法名,但是需要手动将native方法和java方法关联起来,并手动注册。具体步骤如下:

    1. 创建java源文件,定义需要的native方法:
    public final class DynamicJniRegister {private DynamicJniRegister() {}private static class SingleInstHolder {private static DynamicJniRegister sInstance = new DynamicJniRegister();}public static DynamicJniRegister getInstance() {return SingleInstHolder.sInstance;}// 定义的native方法public native String getGreetingsFromDynamicJni();}
    1. 创建jni的cpp文件,可以自行命名,但方便后续开发,依然推荐取名为java类包名。源文件需要包含jni的基础功能头文件jni.h;

    2. 在cpp文件中定义native接口的实现,如:

    jstring getGreetingsFromDynamicJni(JNIEnv* env, jobject obj) {std::string hello = \"Greetings from dynamic C++\";return env->NewStringUTF(hello.c_str());}
    1. 定义jni方法与java方法的映射数组JNINativeMethod。这个数据类型主要包含三部分:java方法名、方法签名、jni函数指针。有关该数据结构和签名含义会在后面详细总结。
    static const JNINativeMethod jniNativeMethod[] = {{\"getGreetingsFromDynamicJni\", \"()Ljava/lang/String;\", (jstring *)getGreetingsFromDynamicJni}};
    1. 实现JNI_Onload函数。该函数会在java中加载so库时自动调用。由于动态注册的jni无法根据命名查找到正确的jni函数,需要手动在该方法中确定映射规则。因此方法的主要功能是使用RegisterNatives:
    static const char* mClassName = \"com/example/myappdemo/nativemanager/DynamicJniRegister\";jint JNI_OnLoad(JavaVM *vm, void *unused) {JNIEnv* env = NULL;int res = vm->GetEnv((void**)&env, JNI_VERSION_1_4);if (res != JNI_OK) {return -1;}jclass mainClass = env->FindClass(mClassName);res = env->RegisterNatives(mainClass, jniNativeMethod, 1);if (res != JNI_OK) {return -1;}return JNI_VERSION_1_4;}
    1. 在CMakeLists.txt中增加c++源文件,编译动态库:
    add_library( dynamic_native-lib SHAREDjni_dynamic_register.cpp)
    1. 在java文件中加载该动态库:
    static {System.loadLibrary(\"dynamic_native-lib\");}

    8)最后,使用1.1.2中的3)相同的方法配置build.gradle。至此就完成了动态注册jni.

    1.3 jni编译的重要文件CMakeLists.txt

    CMakeLists.txt是用来生成makefile并编译c++/c源文件的,可以通过简单的指令输入对源文件的编译需求。在使用Android Studio时,CMakeLists.txt是在修改后sync时执行生成makefile的,编译时自动读取externalNativeBuild中的makefile,并编译c/c++代码。
    这里总结一下CMakeLists的基本编写方法。

    1) 首先使用cmake_minimum_required限定cmake的最低版本,否则会产生警告:

    cmake_minimum_required(VERSION 3.4.1)

    2) 构建新的静态/动态库

    使用add_library可以根据已有的cpp源文件编译新的静态/动态库,命令的参数为:

    add_library(

    ​ dynamic_native-lib // 该参数为库的名字,如果是动态库,最终文件名为:libdynamic_native_lib.so

    ​ SHARED // 库的类型,SHARED为共享动态库,STATIC为静态库

    ​ jni_dynamic_register.cpp) // 对应的源文件,所有的源文件都可以罗列在这里,无需标点分割。

    3)使用预编译库中的函数

    Android中预制了一些NDK的库供开发者使用,比如log库。这类的原生库可以通过find_library将该库与变量关联,再通过target_link_libraries将变量连接在希望使用它的库中,以便于使用该库中的函数。

    find_library的参数结构为:

    find_library( log-lib // 希望连接到的变量名,即定义一个变量与需要使用的库对应

    ​ log) // 希望用到的NDK预制库

    target_link_libraries的参数结构为:

    target_link_libraries( dynamic_native-lib // 希望用到NDK预制库方法的目标库

    ​ ${log-lib} // find_library定义好的变量,这里可以添加多个

    4) 引入第三方so库
    引入第三方库的方法与创建一个新的native库类似,区别在于最后一个参数,我们通过IMPORTED标志告知CMake只希望将库导入到项目中。
    关于目标库的路径有几点需要说明:

    a. CMAKE_SOURCE_DIR表示的是CMakeLists.txt所在的路径,当指定第三方so所在路径时,应当以这个常量为起点。
    b, 在具体项目中可以为每种ABI提供单独的软件包,就可以在jinLibs(如果是project结构的目录则是libs)下建立多个目录,每个目录对应一种ABI接口类型,然后再通过${ANDROID_ABI}来泛化这一层目录的结构,这样有助于充分利用特定的CPU架构。第三方的库关联到原生库与NDK库关联到原生库的原理是一样的。
    c. 为了确保CMake可以在编译时定位我们的头文件,需要将include_directories() 命令添加到 CMake构建脚本中并指定头文件的路径

    add_library(
    #指定目标导入库
    imported-lib
    #设置导入库的类型(静态或动态)为shared library.
    SHARED
    #告知 CMake imported-lib 是导入的库
    IMPORTED )

    set_target_properties(
    #指定目标导入库
    imported-lib
    #指定属性(本地导入的已有库)
    PROPERTIES IMPORTED_LOCATION
    #指定你要导入库的路径. 比如:
    ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libimported-lib.so )
    #为了确保 CMake 可以在编译时定位到我们的 头文件,我们需要使用include_directories()命令,
    #并包含头文件的路径
    include_directories(libs/include/)

    #要将预构建库关联到我们的原生库,需要将其添加到CMake构建脚本的target_link_libraries()命令中
    target_link_libraries(
    #这里指定了三个库,分别是native-lib、imported-lib和log-lib.
    native-lib
    imported-lib
    #log-lib是包含在NDK中的一个日志库
    ${log-lib} )

    2. Jni接口编写

    Jni为C/C++编程,但是具体使用风格还是会有稍许不同(比如在字符串编码以及常用数据类型等),这里我会简单总结一下jni编程中的主要关键词、主要方法以及其他需要主意的事项。

    2.1 Jni的特殊描述符以及主要变量

    在1.1.1和1.1.2中我们看到静态注册的jni的c++和h文件中有一些特殊的描述符、宏以及数据结构,这里主要总结一下这些描述符的含义:
    #ifdef __cplusplus
    extern “C” {
    #endif
    /*

    • Class: com_example_jnitest2_nativeManager_NativeManager
    • Method: getName
    • Signature: (I)Ljava/lang/String;
      */
      JNIEXPORT jstring JNICALL Java_com_example_jnitest2_nativeManager_StaticRegisterJniManager_getName
      (JNIEnv *, jobject);

    #ifdef __cplusplus
    }

    2.1.1 Jni中的特殊描述符

    1. extern “C”:
      jni静态注册属于C/C++混合编程,是通过函数名来寻找函数入口的。C与C++在编译中对函数名字的处理有所区别。C++存在重载,因此不能用函数名作为唯一标识,会将参数列表与返回值引入参考。使用extern \”C\”以后,可以使被extern \”C\”标识的部分使用C的编译方法,函数名不会做特殊处理,以方便jni正确识别。
      为什么在jdk自动生成的h文件中增加了#ifdef __cplusplus条件限制呢?因为h文件不可避免有可能被C程序引入,而C程序本身是不识别extern \”C\”修饰符的,因此需要确认当前引入该头文件的是C++文件,才使该修饰符生效。
    2. JNIEXPORT:
      这个关键字表明这个函数是一个可导出函数。每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。
    3. JNICALL:
      说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别。

    2.1.2 JNIEnv和jobject类型总结

    1. JNIEnv
      JNIEnv类型实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。例如,创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作。因此每个函数中都有JNIEnv的指针参数。
      JNIEnv的部分源码如下:
    typedef _JNIEnv JNIEnv;struct _JNIEnv {/* do not rename this; it does not seem to be entirely opaque */const struct JNINativeInterface* functions;#if defined(__cplusplus)jint GetVersion(){ return functions->GetVersion(this); }jclass DefineClass(const char *name, jobject loader, const jbyte* buf,jsize bufLen){ return functions->DefineClass(this, name, loader, buf, bufLen); }jclass FindClass(const char* name){ return functions->FindClass(this, name); }jmethodID FromReflectedMethod(jobject method){ return functions->FromReflectedMethod(this, method); }......

    通过该源码可以看到,JNIEnv中包含JNINativeInterface结构,而后面的所有函数均是对JNINativeInterface中函数的封装,并对参数进行了修改。并且又看到了熟悉的宏:#if defined(__cplusplus)。这里可以看出C和C++对JNIEnv的处理有所不同。对于C编写的Jni程序,JNIEnv等同于JNINativeInterface,而对于C++来说,JNIEnv则对其进行了一次封装。
    2. jobject
    如果native方法不是static的话,jobject就代表这个native方法的类实例。
    如果native方法是static的话,jobject就代表这个native方法的类的class对象实例(static方法不需要类实例的,所以就代表这个类的class对象)。
    3. JavaVM
    JavaVM的源码如下:

    struct _JavaVM {const struct JNIInvokeInterface* functions;#if defined(__cplusplus)jint DestroyJavaVM(){ return functions->DestroyJavaVM(this); }jint AttachCurrentThread(JNIEnv** p_env, void* thr_args){ return functions->AttachCurrentThread(this, p_env, thr_args); }jint DetachCurrentThread(){ return functions->DetachCurrentThread(this); }jint GetEnv(void** env, jint version){ return functions->GetEnv(this, env, version); }jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args){ return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }#endif /*__cplusplus*/};

    从源码中可以看到JavaVM的主要成员函数都是围绕虚拟机进行的,包括:销毁虚拟机、归入线程、作为守护线程归入某线程、获取环境参数等等。因此这个变量是针对整个线程的性质进行描述和控制的。
    在jni动态注册的初始时,就是通过GetEnv获取虚拟机环境参数的。
    4. JNINativeMethod 源码如下:

    typedef struct {const char* name;const char* signature;void*       fnPtr;} JNINativeMethod;

    JNINativeMethod是用来联系Java方法与本地方法的数据,一般会定义成数组(因为有多个本地方法需要注册)。其中name为Java方法,signature是用来描述参数以及返回值的签名,fnPtr是C++的函数指针。

    2.2 Jni的主要函数

    这里所讲的函数,是指jni.h中的函数,包括JNIEnv以及JavaVM中的部分成员函数。

    1. JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved);
      这个是jni,h中声明的外部函数,由JNIEXPORT声明。该函数如果实现,则会在加载库成功后调用,提供Java虚拟机参数(第二个参数预留)。一般动态注册需要在这个函数中做一些自定义初始化工作,如寻找关联的Java类、动态注册本地函数等。
    2. JNIEXPORT void JNI_OnUnload(JavaVM* vm, void* reserved);
      当虚拟机释放该C库时,则会调用JNI_OnUnload()函数来进行善后清除动作。
    3. jint GetEnv(void** env, jint version)
      用于获取环境参数,是JavaVM的成员函数。一般用于在库刚刚加载的初始化工作中。
    4. jint RegisterNatives(jclass clazz, const JNINativeMethod* methods,
      jint nMethods)
      注册本地函数,是JNIEnv的成员函数,是用来动态注册Jni的函数。其中jclass需要额外获取相关类,JNINativeMethod中记录了Java函数和C++函数的映射关系,也是预先写好的。
    5. jclass FindClass(const char* name)
      用来根据类的全命名寻找类的class对象,也是JNIEnv的成员函数:
    static const char* mClassName = \"com/example/jnitest2/nativeManager/DynamicRegisterJniManager\";jclass nativeManagerCls = env->FindClass(mClassName);

    一般是用来做反射使用(调用某Java类的成员方法),或者在动态注册jni的初始时,需要用它来寻找关联的Java类。

    2.3 Jni的函数签名

    在1.2 动态注册jni中,以及2.1.2的第4条JNINativeMethod结构 中可以看到函数关系表都是有
    函数签名参与的,函数签名用来描述函数的参数以及返回值,当需要使用可以通过jdk直接查询签名表,如:javap -s -p StaticRegisterJniManager.class
    结果参考下图:

    常见的签名如下表所示:

    数据类型 签名
    boolean Z
    byte B
    char C
    short S
    int I
    long J
    float F
    double D
    void V
    object L开头,以/分隔包的完整类型,结尾加‘;’ 比如String为:Ljava/lang/String;
    Array 以[开头,加上数组元素类型的签名。如int[]应为[I,int[][]为[[I

    如何组织JNINativeMethod的签名格式呢?比如当我的Java方法是这样的:
    String foo(int a, boolean b, Date[] c), 对应的签名应该为:
    (参数1 参数2 参数3 …)返回值
    因此应该为:
    (I Z [Ljava/util/Date;)Ljava/lang/String;

    2.4 Jni访问字符串

    2.4.1 关于编码方式

    在Java的JVM内码中,String的编码是utf16编码格式,(也就是说每个字符的大小为2~4个字节),但是在jni中一般使用utf8编码格式,而在C/C++中则普遍使用原始数据(ASCII码),1个字节,中文使用GB2312(2个字节)。因此在使用jni进行字符串传递时,一定需要进行编码的转换,否则会出现乱码情况。
    编码的转换过程参考下图:

    2.4.2 需要用到的函数

    JNIEnv中有关String的函数参考下表:

    函数定义 函数功能
    jstring (NewString)(JNIEnv, const jchar*, jsize) 创建新的String
    jsize (GetStringLength)(JNIEnv, jstring) 返回Unicode字符串的字符数。
    const jchar* (GetStringChars)(JNIEnv, jstring, jboolean*) 获取以Unicode格式编码的字符串
    void (ReleaseStringChars)(JNIEnv, jstring, const jchar*) 释放字符串的空间
    jstring (NewStringUTF)(JNIEnv, const char*) 创建新的jstring字符串
    jsize (GetStringUTFLength)(JNIEnv, jstring) 返回UTF-8字符串的字节数,不包含末尾’\\0’。
    const char* (GetStringUTFChars)(JNIEnv, jstring, jboolean*) 把jstring指针(指向JVM内部Unicode序列)转化成UTF-8格式的C字符串
    void (ReleaseStringUTFChars)(JNIEnv, jstring, const char*) 释放UTF-8编码的字符串

    值得一提的是,GetStringLength与GetStringUTFChars两个函数的表现效果是不确定的,具体由可能会将提供的参数jstring指针直接强转编码形势后返回,也可能会开辟一段空间并将内容copy过去。第三个参数jboolean*会反应本次操作是否发生了拷贝。为了保险起见,返回的字符串不应该做任何修改,且使用完成应该对应使用ReleaseStringChars和ReleaseStringUTFChars进行释放,防止内存泄漏。

    3 Jni中的反射

    3.1 访问成员属性

    Jni中每个函数都具有JNIEnv*和jobject两个参数,其中jobject在前文中介绍到,是调用者类的对象或者其class。如果当前native方法不是static时,欲访问其成员需要以下步骤(此处假设目标类的某成员属性为 int property = 0):

    1. 获得其class引用:
      jclass clazz = env->GetObjectClass(jobj); // 此处以C++举例
    2. 获取成员属性ID:
      jfieldID jfid = env->GetfieldID(clazz, “property”, “I”) //property为属性名,“I”为其类型签名
    3. 获取成员属性的值:
      jint val = env->GetIntField(jobj, jfid);
    4. 修改成员属性值:
      env->SetIntField(jobj, jfid, val + 100);
      note:当操作私有成员时,需要设置setAccessible(true)后才可以哦!

      如果访问的是静态变量,获取/设置字段需要更换为:
      val = env->GetStaticIntField(clazz, jobj, jfid);
      env->SetStaticIntField(clazz, jobj, jfid, val+100);

    3.2 访问成员方法

    访问成员方法一般需要按照以下流程(获得类的引用流程基本类似,因此这里我们将前提条件那个了更换,在3.1中假设调用者类就是需要反射的类,此处我们将假定反射任意一个类,例如类名为:FooClass,目标方法为:void methodName(String, int) ):

    1. 获得class引用,对于任意一个存在的类,需要使用FindClass来查找:
      jclass clz = env->FindClass(“FooClass”);
    2. 查找需要使用的 (假设是静态方法,与普通方法流程一致,但调用函数名有区别)
      jmethodID jmeid = env->GetStaticMethodID(clz, “methodName”, \”
      (Ljava/lang/String;I)V\”);
    3. 调用该静态方法
      env->CallStaticVoidMethod(clz,jmeid,“test”,100);
    赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » Android Studio下的Jni编程总结