Android平台NDK开发知识汇总

2019/06/07 Android

JNI定义的数据类型

JNI 中定义的数据类型和 Java 中定义的数据类型是一一对应的关系。JNI 中定义的数据类 型分为基本数据类型和引用类型。基本数据类型见下表:

JNI 类型 Java 类型 描述
void void 无类型
jboolean boolean 布尔型
jbyte byte 字节型
jchar char 字符型
jshort short 短整型
jint int 整型
jlong long 长整型
jfloat float 浮点型
jdouble bouble 双精度浮点型

引用类型:

Table 1: JNI 的引用类型
JNI 类型 Java 类型 描述
jstring String 字符串对象
jobjectArray Object[] 对象数组
jbooleanArray boolean[] 布尔型数组
jbyteArray byte[]  
jcharArray char[]  
jshortArray sort[]  
jintArray int[]  
jlongArray long[]  
jfloatArray float[]  
jdoubleArra bouble[]  
jthrowable Throwable  

除了以上的类型之外,还有一点需要注意, Java 语言的中的数据类型的签名如下表所示:

Table 2: 数据类型的签名
Java 类型 signature
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void void
String Ljava/lang/String;
Object Ljava/lang/Object;

Gradle中配置 cmak

在 gradle 的配置文件中配置 cmake 的目的是为了让 gradle 在构建 APK 时能够自动的去 构建 C/C++代码,也就是说要在 gradle 进行配置,以便让 Android Studio 能够识别 C/C++ 代码.要将下面的代码添加到 module 的 build.gradle 文件中.

android {
  ...
  defaultConfig {
    ...
    // This block is different from the one you use to link Gradle
    // to your CMake build script.
    externalNativeBuild {
      cmake {
        ...
        // Use the following syntax when passing arguments to variables:
        // arguments "-DVAR_NAME=ARGUMENT".
        arguments "-DANDROID_ARM_NEON=TRUE",
        // If you're passing multiple arguments to a variable, pass them together:
        // arguments "-DVAR_NAME=ARG_1 ARG_2"
        // The following line passes 'rtti' and 'exceptions' to 'ANDROID_CPP_FEATURES'.
                  "-DANDROID_CPP_FEATURES=rtti exceptions"
       // add filter to api
       adbFilters "armeabi-v7a", "armeabi-v8a", "x86", "x86-64"
      }
    }
  }
  buildTypes {...}

  // Use this block to link Gradle to your CMake build script.
  externalNativeBuild {
    cmake {
        path file('../CmakeLists.txt')
    }
  }
}

Cmake语法

从 Android studio2.2 开始支持Cmake编译 C/C++代码,而且目前最新版的 Android studio,使用 cmake 作为默认的构建工具.接下来我们看一下最简单的CmakeLists.txt.

cmake_minimum_required(VERSION 3.4.1)
add_library( # 生成的so库名称,此处生成的so文件名称是libnative-lib.so
             native-lib
             # SHARED是动态库,会被动态链接,在运行时被加载
             # STATIC:静态库,是目标文件的归档文件,在链接其它目标的时候使用
             # MODULE:模块库,是不会被链接到其它目标中的插件
             SHARED
             # 资源路径是相对路径,相对于本CMakeLists.txt所在目录
             src/main/cpp/native-lib.cpp )
# 从系统查找依赖库
find_library( # android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中
              log-lib
              # android系统在c环境下打log到logcat的库
              log )
# 配置库的链接(依赖关系)
target_link_libraries( # 目标库
                       native-lib
                       # 依赖于
                       ${log-lib} )

如果想在CmakeList s.txt 中添加另一个 so 库的依赖, CmakeListts.txt 如下所示:

cmake_minimum_required(VERSION 3.4.1)
add_library( 
            pre-build-lib
            STATIC
            src/main/cpp/pre-build-lib
)
add_library( native-lib
             SHARED
             src/main/cpp/native-lib.cpp 
             src/main/cpp/native-lib2.cpp)
# 从系统查找依赖库
find_library( # android系统每个类型的库会存放一个特定的位置,而log库存放在log-lib中
              log-lib
              # android系统在c环境下打log到logcat的库
              log )
# 配置库的链接(依赖关系)
target_link_libraries( # 目标库
                       native-lib
                       # 依赖于
                       ${log-lib} 
                       pre-build-lib
                       )

JNI注册的方式

java 要想调用 C/C++代码,java 必须找到相应的 C/C++代码,所以 C/C++代码需要进行注 册,以便 java 代码能够找到相应的 C/C++代码.JNI 支持的注册方式有两种:静态注册方式 和动态注册方式.先看静态的注册方式:

extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_lingxiao_ndkproject_Hello_callJavaStaticMethod(JNIEnv *env, jclass thiz) {
    jclass clazz = NULL;
    jmethodID method_id = NULL;
    jstring str_log = NULL;

    clazz = env->FindClass("com/pacakge/Hello");
    if (clazz == NULL){
        return;
    }
    method_id = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;)V");
    if (method_id == NULL){
        return;
    }
    str_log = env->NewStringUTF("c++ 调用java的静态方法");
    env->CallStaticVoidMethod(clazz,method_id,str_log);

    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_log);
    return ;
}

在静态注册的方式的函数定义部分是比较固定的写法,我们以此来解释一下:

  1. extern “C”:表示C++代码能够调用其他 C 代码,主要用来兼容 C 语言.
  2. JNIEXPORT: 这个宏的定义如下,这个宏表示这函数对外是可见的,以便于 java 代码能 够找到这个函数.

       #define JNIEXPORT  __attribute__ ((visibility ("default")))
    
  3. JavacomndklingxiaondkprojectHellocallJavaStaticMethod:函数的名称,这个 函数名是有固定的格式的,格式为:
Java_类的全路径名_方法名(在类的全路径名中要将"/"装换成"_", 这个类定义这个 native 方法所在的类)
  1. JNIEnv env*: JNI调用的运行环境的指针,每个函数的第一个参数都是一个 JNIEnv*类 型.
  2. jclass thiz: 如果注册的类是一个非静态类,这个thiz 指的的是调用这个方法的类实 例化后的对象.如果注册的类是一个静态类,这个 thiz 指的是就是这个类.

接下来我们看动态注册的方式,在动态注册的方式有又分为两种形式:C 形式 和 C++形式,接 下来我们会分别对这两种形式作介绍. 先看 C 语言版本的动态注册方式:

static const char *mainClass = "test/com/jstest/MainActivity"; // path of Java file

#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

JNIEXPORT jstring JNICALL get_hello(JNIEnv *env, jclass clazz) {
    return (*env)->NewStringUTF(env, "hello from jni");
}

static JNINativeMethod g_methods[] = {
        {"getHello",     "()Ljava/lang/String;", (void *) get_hello},
};

JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = JNI_ERR;

    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    assert(env != NULL);
    
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_ERR;
    }
    // C 与 C++ 不同之处,
    if ((*env)->RegisterNatives(env, clazz, gMethods, NELEM(gMethods)) < 0) {
        return JNI_ERR;
    }
    // 代表 JNI 不同的版本.
    result = JNI_VERSION_1_6;

    return result;
}

C++版本的动态注册如下所示:

static const char *mainClass = "test/com/jstest/MainActivity"; // path of Java file

#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))



JNIEXPORT jstring JNICALL get_hello(JNIEnv *env, jclass clazz) {
    return env->NewStringUTF("hello from jni");
}


static JNINativeMethod g_methods[] = {
        {"getHello",     "()Ljava/lang/String;", (void *) get_hello},
};


JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = JNI_FALSE;

    //获取env指针
    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return result;
    }
    if (env == NULL) {
        return result;
    }
    //获取类引用,写类的全路径(包名+类名)。FindClass等JNI函数将在后面讲解
    jclass clazz = env->FindClass(mainClass);
    if (clazz == NULL) {
        return result;
    }
    //注册方法
    if (env->RegisterNatives(clazz, g_methods, NELEM(g_methods)) < 0) {
        return result;
    }
    //成功
    result = JNI_VERSION_1_6;
    return result;
}

垃圾回收

在 Java 代码中的对象是有垃圾回收机制的,但是对于 Native 中的代码应该如何进行垃圾 回收呢?以及如何同 Java 代码进行配合呢? 首先来看一段代码片段:

void Java_package_name_className(JNIEnv* env, jobject thiz) {
    ...
    //将由 Java 层传入的对象保存下来,已备后期使用.
    jboject save_thiz = thiz;
    ...
    return;
}

查看上面的代码,我们能否在 native 方法的其他位置正常的使用 savethiz 代表的对象呢?结论是在其他 位置使用 savethiz 会出现问题,因为由于 Java 层有垃圾回收机制,当在 native 层使用 savethiz 对象时,有可能 Java 已经把 savethiz 指向的对象回收了,所以不能正常的使 用 savethiz.造成这个问题的核心原因是:thiz 对 savethiz的赋值操作*没有触发对象的 引用计数*.

如何处理上面的情况呢?

JNI 提供了三种类型的应用解决上面的问题:

  1. Local Reference: 本地引用,在 JNI 层的函数中使用的*非全局*引用对象都是 Local Reference, 它包含函数调用时传入的 jobject 和在 JNI 层中创建的 jobject.上面的 例子中的 thiz 和 savethiz 都是 Local Reference.它最大的特点是*一旦 JNI 层函 数返回,这些 jobject 就可能会被垃圾回收*.
  2. Global Reference: 全局引用,这种对象如果不主动的去释放,它永远不会被垃圾回收.
  3. Weak Global Reference: 弱全局引用,一种特殊的 Global Reference,在运行过程中有 可能被垃圾回收,所以在使用它之前要调用 JNIEnv 的 isSameObject 来判断它是否被回 收了.

我们看下面的例子:

MyMediaScannerClient(JNIEnv* env, jobject, client)
: mEnv(env),
// 调用 NewGlobalRef 创建一个 Global Reference
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
...
{
....
}

// 析构函数
virtual ~MyMediaScannerClient(){
    // 调用 DeleteGlobalRef 是否对这个对象的全局引用.
    mEvn->DeleteGlobalRef(mClient);
}

Search

    Table of Contents