【Android】NDK的基本使用,这一篇就够了

【Android】NDK的基本使用,这一篇就够了

JNI

概念

JNI是Java Native Interface的简写,它可以使Java与其他语言(如C、C++)进行交互。

它是Java调用Native语言的一种特性,属于Java语言的范畴,与Android无关。

为何需要JNI

  • Java的源文件非常容易被反编译,而通过Native语言生成的.so库文件则不容易被反编译。
  • 有时候会有一种情况就是当我们使用Java时需要使用到一些库来实现某些功能,但又由于这些库仅仅提供了一些Native语言的接口。
  • 使用Native语言编写的代码运行效率高,尤其体现在音频视频图片的处理等需要大量复杂运算的操作上。充分利用了硬件的性能。

由于上述原因,此时我们就需要让Java与Native语言交互。而由于Java的特点,与Native语言的交互能力很弱。因此在此时,我们就需要用到JNI特性增强Java与Native方法的交互能力。

实现的步骤

  1. 在Java中声明Native方法(需要调用的本地方法)
  2. 通过 javac 编译 Java源文件( 生成.class文件)
  3. 通过 javah 命令生成JNI头文件(生成.h文件)
  4. 通过Native语言实现在Java源码中声明的Native方法
  5. 编译成.so库文件
  6. 通过Java命令执行 Java程序,最终实现Java调用本地代码(借助so库文件)

NDK

概念

Native是Native Development Kit的简写,是Android的开发工具包,属于Android,与Java无关系。

它可以快速开发C/C++的动态库,自动将.so和应用一起打包为APK。因此我们可以通过NDK来在Android开发中通过JNI与Native方法交互。

使用方式

  1. 配置 Android NDK环境(在SDK Manager中下载NDK、CMake、LLDB)
  2. 创建 Android 项目,与 NDK进行关联(创建项目时选择C++ support)
  3. 在 Android 项目中声明所需要调用的 Native方法
  4. 用Native语言实现在Android中声明的Native方法
  5. 通过 ndk-bulid 命令编译产生.so库文件

将Android项目与NDK关联

配置NDK路径

local.properties中加入如下一行即可

ndk.dir=<ndk路径>

添加配置

在Gradle的 gradle.properties中加入如下一行,目的是对旧版本的NDK支持

android.useDeprecatedNdk=true

添加ndk节点

在build.gradle中的defaultConfigandroid中加入如下的externalNativeBuild节点

apply plugin: 'com.android.application'

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.n0texpecterr0r.ndkdemo"
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:27.1.1'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

开发Native代码

在Java文件中声明native方法

我们首先需要在Java代码的类中通过static块来加载我们的Native库。可以通过如下代码,其中loadLibrary的参数是在CMakeList.txt中定义的Native库的名称

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

之后,我们便可以在这个类中声明Native方法

public native String getStringFromJNI();

创建CMakeList.txt

我们还需要在src中创建一个CMakeList.txt文件,这个文件约束了Native语言源文件的编译规则。比如下面

cmake_minimum_required(VERSION 3.4.1)

add_library(native-lib SHARED src/main/cpp/native-lib.cpp)

find_library(log-lib log)

target_link_libraries(native-lib ${log-lib})

add_library方法中定义了一个so库,它的名称是native-lib,也就是我们在Java文件中用到的字符串,而后面则跟着这个库对应的Native文件的路径

find_library则是定义了一个路径变量,经过了这个方法,log-lib这个变量中的值就是Android中log库的路径

target_link_libraries则是将native-lib这个库和log库连接了起来,这样我们就能在native-lib中使用log库的方法。

创建Native方法文件

在前面的CMake文件中可以看到,我们把文件放在了src/main/cpp/,因此我们创建cpp这个目录,在里面创建C++源文件native-lib.cpp。

然后, 我们便可以开始编写如下的代码:

#include <jni.h>
#include <string>

extern "C"{
  JNIEXPORT jstring JNICALL
  Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI(
      JNIEnv* env,
      jobject) {
    std::string hello = "IG牛逼";
    return env->NewStringUTF(hello.c_str());
  }
}

此处我们使用的是C++语言,让我们来看看具体的代码。

首先我们引入了jni需要的jni.h,这个头文件中声明了各个jni需要用到的函数。同时我们引入了C++中的string.h。

然后我们看到extern “C”。为了了解这里为什么使用了extern “C”,我们首先需要知道下面的知识:

在C中,编译时的函数签名仅仅是包含了函数的名称,因此不同参数的函数都是同样的签名。这也就是为什么C不支持重载。

而C++为了支持重载,在编译的时候函数的签名除了包含函数的名称,还携带了函数的参数及返回类型等等。

试想此时我们有个C的函数库要给C++调用,会因为签名的不同而找不到对应的函数。因此,我们需要使用extern "C"来告诉编译器使用编译C的方式来连接。

接下来我们看看JNIEXPORT和JNICALL关键字,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数。

而jstring则对应了Java中的String类,JNI中有很多类似jstring的类来对应Java中的类,下面是Java中的类与JNI类型的对照表

img

我们继续看到函数名Java_com_n0texpecterr0r_ndkdemo_MainActivity_getStringFromJNI。其实函数名中的_相当于Java中的 . 也就是这个函数名代表了java.com.n0texpecterr0r.ndkdemo.MainActivity.java中的getStringFromJNI方法,也就是我们之前定义的native方法。

格式大概如下:

Java_包名_类名_需要调用的方法名

其中,Java必须大写,包名里的.要改成__要改成_1

接下来我们看到这个函数的两个参数:

  • JNIEnv* env:代表了JVM的环境,Native方法可以通过这个指针来调用Java代码
  • jobject obj:它就相当于定义了这个JNI方法的类 (MainActivity) 的this引用

然后可以看到后面我们创建了一个string hello,之后通过env->NewStringUTF(hello.c_str())方法创建了一个jstring类型的变量并返回。

在Java代码中调用native方法

接着,我们便可以在MainActivty中像调用Java方法一样调用这个native方法

TextView tv = findViewById(R.id.sample_text);
tv.setText(getStringFromJNI());

我们尝试运行,可以看到,我们成功用C++构建了一个字符串并返回给Java调用:

CMake

我们在NDK开发中使用CMake的语法来编写简单的代码描述编译的过程,由于这篇文章是讲NDK的,所以关于CMake的语法就不再赘述了。。。如果想要了解CMake语法可以学习这本书《CMake Practice

JNI与Java代码交互

方法签名

概念

在我们JNI层调用一个方法时,需要传递一个参数——方法签名。

为什么要使用方法签名呢?因为在Java中的方法是可以重载的,两个方法可能名称相同而参数不同。为了区分调用的方法,就引入了方法签名的概念。

签名规则

对于基本类型的参数,每个类型对应了一个不同的字母:

  • boolean Z
  • byte B
  • char C
  • short S
  • int I
  • long J
  • float F
  • double D
  • void V

对于类,则使用 L+类名 的方式,其中(.)用(/)代替,最后加上分号

比如 java.lang.String就是 Ljava/lang/String;

对于数组,则在前面加 [ ,然后加类型的签名,几维数组就加几个。

比如 int[]对应的就是[Iboolean[][]对应的则是[[Z,而java.lang.String[]就是[Ljava/lang/String;

打印方法签名

我们可以通过 javap -s 命令来打印方法的签名。

例子

比如下面的方法

public native String getMessage();

public native String getMessage(String id,long i);

对应的方法签名分别为:

()Ljava/lang/String;
(Ljava/long/String;J)Ljava/lang/String;

可以看到,前面括号中表示的是方法的参数列表,后面表示的则是返回值。

调用Java方法

调用静态方法

调用思路

在Java中,我们调用一个方法的思路一般是先要找到对应的类,然后找到类中对应的方法,之后进行方法的调用。

在JNI中,其实也是一致的:

  1. 我们先需要同JVM环境的函数找到对应的jclass,对应Java中的类。
  2. 拿到了对应的类之后,我们可以通过JVM环境中的函数拿到需要调用的方法的jmethodID类型的变量。
  3. 获取到jmethodID之后,我们就可以调用JVM环境中的对应函数调用对应方法来进行函数的调用。
  4. 调用完成后,释放相关资源即可。
具体过程

假设我们在MainActivity定义了如下两个方法:

public static void logMessage(String msg){
    Log.d("NDK", msg);
}
public native void callStaticMethod();

然后我们为native方法生成了如下的C++代码:

JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_callStaticMethod(JNIEnv *env, jobject instance) {

}

我们首先通过env的FindClass方法找到对应的类:

jclass cls_main = env->FindClass("com/n0texpecterr0r/ndkdemo/MainActivity");

可以看到,FindClass需要的参数是这个类的全路径。获取到的jclass我们最好判下空

接下来我们需要拿到我们要调用的方法对应的jmethodID

jmethodID mth_static_method = env->GetMethodID(cls_main,"logMessage","(Ljava/lang/String;)V");

由于logMessage方法是static的,因此我们调用了env的GetStaticMethodID方法,需要传入的参数分别是 jclass 方法名 以及方法对应的签名。

仍然判下空,之后我们可以通过env->CallXXXXXMethod方法来调用对应方法。

由于我们的logMessage方法是static,返回值为void的方法,因此我们使用CallStaticVoidMethod方法即可

jstring str = env->NewStringUTF("这是从JNI调用的Log");
env->CallStaticVoidMethod(cls_main,mth_static_method,str);

调用完成后,我们可以通过env的DeleteLocalRef方法来释放刚刚声明的变量。

env->DeleteLocalRef(cls_main);
env->DeleteLocalRef(str);

这样,在MainActivity中调用callStaticMethod方法后,便会打印这样一条log

D/NDK: 这是从JNI调用的Log

完整代码如下:

JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_callStaticMethod(JNIEnv *env, jobject instance) {
  // 找到对应的类
  jclass cls_main = env->FindClass("com/n0texpecterr0r/ndkdemo/MainActivity");
  if(cls_main == NULL)  return;
  // 获取methodId
  jmethodID mth_static_method = env->GetStaticMethodID(cls_main,"logMessage","(Ljava/lang/String;)V");
  if(mth_static_method == NULL) return;
  // 构建String变量
  jstring str = env->NewStringUTF("这是从JNI调用的Log");
  // 调用static方法
  env->CallStaticVoidMethod(cls_main,mth_static_method,str);
  // 释放内存
  env->DeleteLocalRef(cls_main);
  env->DeleteLocalRef(str);
}

有了之前的jclass之后,我们还可以通过获取jfieldID,之后调用env的SetStaticObjectField方法来修改static变量。与前面类似。

调用实例方法

假设我们有了下面这样一个类

public class Adder {
    private int arg1;
    private int arg2;

    public Adder(int arg1, int arg2) {
        this.arg1 = arg1;
        this.arg2 = arg2;
    }

    public int doAdd(){
        return arg1+arg2;
    }
}
调用思路

调用实例方法相比调用静态方法会复杂一些

  1. 找到对应的jclass
  2. 调用其构造方法
  3. 创建其对象
  4. 调用其方法
具体过程
JNIEXPORT jint JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_addNative(JNIEnv *env, jobject instance, jint arg1, jint arg2) {
  // 找到对应类
  jclass cls_adder = env->FindClass("com/n0texpecterr0r/ndkdemo/Adder");
  // 获取构造方法
  jmethodID mth_constructor = env->GetMethodID(cls_adder,"<init>","(II)V");
  // 调用构造方法构建jobject
  jobject adder = env->NewObject(cls_adder, mth_constructor,arg1,arg2);
  // 获取add方法
  jmethodID mth_add = env->GetMethodID(cls_adder,"doAdd","()I");
  // 调用add方法获取返回值
  jint result = env->CallIntMethod(adder,mth_add);
  // 回收资源
  env->DeleteLocalRef(cls_adder);
  env->DeleteLocalRef(adder);
  // 返回结果
  return result;
}

可以看到,在获取构造函数的id时,指定的方法名为<init>

对象引用的处理

为什么要处理对象的引用

当我们在Java中使用new创建了一个对象后,可以随意使用这个对象,不需要关注它什么时候被回收,因为这个对象的回收托管给了GC。但是当使用JNI传递给Native层的对象时该如何处理呢?

将对象传递给Native语言后,Native层会持有Java对象,如果我们不妥善处理会导致内存泄漏。 因此在Native层使用Java对象时,需要释放这个引用。

引用的处理

下面可以看看对于数组引用的处理

JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_useCppQSort(JNIEnv *env, jobject instance, jintArray jarray) {
  jint* arrayElemts = env->GetIntArrayElements(jarray, NULL);
  jsize arraySize = env->GetArrayLength(jarray);
  ...  // 排序算法
  env->ReleaseIntArrayElements(jarray, arrayElemts, JNI_COMMIT);
}

array对象传递给C++, C++中的变量将持有array这个引用,因为数组和对象在java中都是引用,都会在堆内存中开辟一块空间 , 但我们使用完对象之后需要将引用释放掉,不然会导致内存泄漏 。

释放数组元素时最后一个参数可以传入两个值:

  • JNI_ABORT, Java数组不进行更新,但是释放C/C++数组
  • JNI_COMMIT,Java数组进行更新,不释放C/C++数组(函数执行完,数组还是会释放)

其实,只要是Java对象,在Native层中都需要释放(包括在Native层创建的对象引用)。

引用分级

在Java中引用有强弱之分,在C/C++中也不例外,C/C++中也有一套全局引用局部引用弱全局引用等等 。

局部引用

局部引用指的是C/C++使用到或自行创建的Java对象,需要告知虚拟机在合适的时候回收对象。(通过DeleteLocalRef手动释放对象)

JNIEXPORT void JNICALL 
Java_com_n0texpecterr0r_ndkdemo_MainActivity_localRef(JNIEnv *env, jobject instance) {
    // 找到类
    jclass dateClass = env->FindClass("java/util/Date");
    // 得到构造方法ID
    jmethodID dateConstructorId = env->GetMethodID(dateClass, "<init>", "()V");
    // 创建Date对象
    jobject dateObject = env->NewObject(dateClass, dateConstructorId);
    // 创建一个局部引用
    jobject dateLocalRef = env->NewLocalRef(dateObject);
    ...
    // 不再使用对象,通知GC回收对象
    env->DeleteLocalRef(dateLocalRef);
    // 因为dateObject也是局部对象,可以直接回收dateObject对象
    // env->DeleteLocalRef(dateObject);
}
全局引用

全局引用的特点是共享(可以跨多个线程),手动控制内存使用

jstring globalStr;

/*创建全局引用*/
JNIEXPORT void JNICALL 
Java_com_n0texpecterr0r_ndkdemo_MainActivity_createGlobalRef(JNIEnv *env, jobject jobj) {
    jstring jStr = env->NewStringUTF("N0tExpectErr0r");
    // 创建一个全局引用
    globalStr = env->NewGlobalRef(jStr);
}

/*使用全局引用*/
JNIEXPORT jstring JNICALL 
Java_com_n0texpecterr0r_ndkdemo_MainActivity_useGlobalRef(JNIEnv *env, jobject jobj) {
    return globalStr;
}

/*释放全局引用*/
JNIEXPORT void JNICALL 
Java_com_n0texpecterr0r_ndkdemo_MainActivity_deleteGlobalRef(JNIEnv *env, jobject jobj) {
    // 释放全局引用
    env->DeleteGlobalRef(globalStr);
}

而弱全局引用的特点是节省内存,在内存不足时可以释放所引用的对象,可以用来引用一个不常用的对象,如果为NULL,临时创建。

创建:NewWeakGlobalRef

销毁:DeleteWeakGlobalRef

异常的处理

异常的例子

下面我们先展示一个出现错误的例子:

首先,我们在MainActivity中做出了如下改变:

private String key = "123";
public native void testException();

然后编写如下的Native方法:

JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_testException(JNIEnv *env, jobject instance) {
    jclass cls_this = env->GetObjectClass(instance);
    // 此处属性名称故意写错了
    jfieldID fid = env->GetFieldID(cls_this, "key1", "Ljava/lang/String;");
    // 此处会抛出异常,Java层可以捕获Error或者Throwable。
    // 但是下面的代码还是可以执行
    __android_log_print(ANDROID_LOG_DEBUG, "NDKException", "flag1");
    // 但是到了下面,程序crash了
    jstring key = static_cast<jstring>(env->GetObjectField(instance, fid));
    char* str = const_cast<char *>(env->GetStringUTFChars(key, NULL));
    __android_log_print(ANDROID_LOG_DEBUG, "NDKException", "flag2");
}

运行程序,发现程序crash了。我们查看logcat:

/com.n0texpecterr0r.ndkdemo D/NDKException: flag1

可以看到虽然抛出异常但是下面的代码仍然可以执行。可是当我们尝试使用fid时,就导致了程序crash。

我们看看logcat中的错误信息:

JNI DETECTED ERROR IN APPLICATION: JNI GetObjectField called with pending exception java.lang.NoSuchFieldError: no "Ljava/lang/String;" field "key1" in class "Lcom/n0texpecterr0r/ndkdemo/MainActivity;" or its superclasses
    at void com.n0texpecterr0r.ndkdemo.MainActivity.testException() (MainActivity.java:-2)
    at void com.n0texpecterr0r.ndkdemo.MainActivity.onCreate(android.os.Bundle) (MainActivity.java:20)
    ...

可以看到,这里抛出了NoSuchFieldError异常,信息为” no “Ljava/lang/String;” field “key1” in class “Lcom/n0texpecterr0r/ndkdemo/MainActivity;” or its superclasses”,提示我们MainActivity及其父类中没有key1这个filed

Native层在出现错误时会抛出Error类型的异常,可以通过Throwable或者Error来捕获,捕获异常后Java代码可以继续执行。

补救措施

为了确保Java、Native代码可以正常执行下去,我们需要:

  1. 在Native层手动清空异常信息(ExceptionClear),保证代码可以运行。
  2. 实行补救措施保证C/C++代码继续运行。

比如下面的方式:

JNIEXPORT void JNICALL
Java_com_n0texpecterr0r_ndkdemo_MainActivity_testException(JNIEnv *env, jobject instance) {
    jclass cls_this = env->GetObjectClass(instance);
    // 此处属性名称故意写错了
    jfieldID fid = env->GetFieldID(cls_this, "key1", "Ljava/lang/String;");
    jthrowable err = env->ExceptionOccurred();
    if (err != NULL){
        //手动清空异常信息,保证Java代码能够继续执行
        env->ExceptionClear();
        //提供补救措施,例如获取另外一个属性
        fid = env->GetFieldID(cls_this, "key", "Ljava/lang/String;");
    }

    jstring key = static_cast<jstring>(env->GetObjectField(instance, fid));
    char* str = const_cast<char *>(env->GetStringUTFChars(key, NULL));
}

运行程序可以发现,我们成功地补救了这个问题,Java代码成功执行了。

手动抛出异常

我们其实也可以通过ThrowNew方法来手动抛出异常。如下:

if (strcmp(str,"efg") != 0){
    // 获取异常Class
    jclass cls_err = env->FindClass("java/lang/IllegalArgumentException");
    // 抛出对应异常
    env->ThrowNew(cls_err, "key value is invalid!");
}

可以看到,我们成功抛出了一个IllegalArgumentException。之后就可以在Java中捕获这个异常:

try {
    testException();
}catch (IllegalArgumentException e){
    Log.e("Exception", e.getMessage());
}

查看Logcat可以看到我们所捕获的异常:

E/Exception: key value is invalid!

NDK使用Java中的Bitmap对象

简述

Bitmap是Android开发中非常常用的图片操作类,它包含了图像的宽、高、格式、以及每个像素点的信息。NDK提供了bitmap.h来给我们使用Android中的Bitmap类。

bitmap.h

NDK为我们提供了Bitmap的头文件,也就是<android/bitmap.h>。我们可以先看看这个头文件中包含了哪些信息:

AndroidBitmapInfo

AndroidBitmapInfo是一个包含了Bitmap常用的信息的类,它的定义如下:

// 可以通过AndroidBitmap_getInfo()获取 
typedef struct {
    // Bitmap的像素宽度信息
    uint32_t    width;
    // Bitmap的像素高度信息
    uint32_t    height;
    // 每一行包含的像素数量
    uint32_t    stride;
    // Bitmap像素的格式
    int32_t     format;
    // 一个弃用的flag,恒为0
    uint32_t    flags;
} AndroidBitmapInfo;
AndroidBitmapFormat

AndroidBitmapFormat是一个枚举,表示了Bitmap的图片格式,与Java端一一对应。它的定义如下:

enum AndroidBitmapFormat {
    ANDROID_BITMAP_FORMAT_NONE      = 0,
    ANDROID_BITMAP_FORMAT_RGBA_8888 = 1,
    ANDROID_BITMAP_FORMAT_RGB_565   = 4,
    ANDROID_BITMAP_FORMAT_RGBA_4444 = 7,
    ANDROID_BITMAP_FORMAT_A_8       = 8,
};
接口返回码

这个地方定义了对Bitmap进行操作的结果,分别对应成功,错误参数,JNI异常,内存分配错误。可能大家会发现最后还有一个异常,其实是因为Google的程序员在写代码的时候把Result给写错了导致的。

#define ANDROID_BITMAP_RESULT_SUCCESS            0
#define ANDROID_BITMAP_RESULT_BAD_PARAMETER     -1
#define ANDROID_BITMAP_RESULT_JNI_EXCEPTION     -2
#define ANDROID_BITMAP_RESULT_ALLOCATION_FAILED -3

#define ANDROID_BITMAP_RESUT_SUCCESS ANDROID_BITMAP_RESULT_SUCCESS
Bitmap操作函数声明

这里声明了对Bitmap操作的函数,它们分别是

  • AndroidBitmap_getInfo:获取当前位图信息。
  • AndroidBitmap_lockPixels:锁定当前位图像素,在锁定期间该Bitmap对象不会被回收,使用完成之后必须调用AndroidBitmap_unlockPixels函数来解除对像素的锁定。
  • AndroidBitmap_unlockPixels:解除像素锁定。
int AndroidBitmap_getInfo(JNIEnv* env, jobject jbitmap, AndroidBitmapInfo* info);

int AndroidBitmap_lockPixels(JNIEnv* env, jobject jbitmap, void** addrPtr);

int AndroidBitmap_unlockPixels(JNIEnv* env, jobject jbitmap);
编译问题

我们需要注意的是,如果我们要使用Bitmap,需要链接jnigraphics库,否则可能出现下面的错误

E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:52: undefined reference to AndroidBitmap_getInfo
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:53: undefined reference to AndroidBitmap_getInfo
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:59: undefined reference to AndroidBitmap_lockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:64: undefined reference to AndroidBitmap_lockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:86: undefined reference to AndroidBitmap_unlockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:87: undefined reference to AndroidBitmap_unlockPixels

因此我们需要在cmake中添加-ljnigraphics

target_link_libraries(native-lib -ljnigraphics ${log-lib})

动态注册

其实,我们之前所采用的注册Java中的native方法的方式叫做静态注册,也就是通过Java_<包名>_<类名>_<方法名>这样的函数名来使得该native方法可以被找到。这样的函数名非常长,不便于管理,如果我们使用动态注册,就可以不受这种命名的限制。

同时我们需要清楚,Java类是通过VM来调用Native方法,调用时需要通过VM在so库中寻找Native函数,如果该函数需频繁调用,会有大量的时间消耗。因此我们可以通过动态注册,在JNI_Onload函数中把native函数注册到VM中,减少寻找花费的时间。

JNI_Onload

在介绍动态注册之前

我们在调用System.loadLibrary()方法时,会自动在该库中查找并调用一个叫JNI_Onload的函数。它的函数原型如下:

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    ...
}

由于这个函数是在JNI被加载时调用,所以它有如下的几个用途:

  • 在JNI_Onload通知VM当前so库使用的JNI版本(默认会返回最老的版本1.1)
  • 在JNI_Onload中进行数据的初始化
  • 在JNI_Onload中对Java类中的Native方法进行动态注册。

Android系统加载JNI依赖库的方式有下面的两种:

  1. 如果JNI_Lib定义了JNI_Onload函数,则通过JNI_Onload函数
  2. 如果JNI_Lib没有定义JNI_Onload函数,则dvm调用dvmResolveNativeMethod进行动态解析。

动态注册的实现

我们可以通过在JNI_Onload中调用registerNativeMethods方法来进行Native方法的动态注册。

比如假设我们有如下的一个静态注册的Native文件

#include <string>  
#include <jni.h>  

extern "C"
JNIEXPORT jstring JNICALL  Java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv* env, jobject thiz )  
{  
    return env->NewStringUTF("Hello from JNI !");  
}  

我们可以将其改写为如下的方式,即可完成方法的动态注册

extern "C"
jstring native_hello(JNIEnv* env, jobject thiz )  
{  
    return env->NewStringUTF("Hello from JNI !");  
}  

JNINativeMethod gMethods[] = {  
    {"stringFromJNI", "()Ljava/lang/String;", (void*)native_hello},//绑定  
};  

int registerNativeMethods(JNIEnv* env, const char* className, JNINativeMethod* gMethods, int numMethods) {  
    jclass clazz;  
    clazz = env->FindClass(className);  
    if (clazz == NULL) {  
        return JNI_FALSE;  
    }  
    if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {  
        return JNI_FALSE;  
    }  

    return JNI_TRUE;  
}  


/* 
* 为所有类注册本地方法 
*/  
int registerNatives(JNIEnv* env) {  
    const char* kClassName = "com/example/hellojni/HelloJni";//指定要注册的类  
    return registerNativeMethods(env, kClassName, gMethods,  
            sizeof(gMethods) / sizeof(gMethods[0]));  
}  

/* 
* System.loadLibrary("lib")时调用 
* 如果成功返回JNI版本, 失败返回-1 
*/  
extern "C"
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {  
    JNIEnv* env = NULL;  
    jint result = -1;  

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {  
        return -1;  
    }  
    assert(env != NULL);  

    if (!registerNatives(env)) {//注册  
        return -1;  
    }  
    //成功  
    result = JNI_VERSION_1_4;  

    return result;  
}  

可以看到,在上述方法中,我们创建了一个方法对应表,将Java中的Native方法与Native层中的函数一一对应。

其中,JNINativeMethod是一个JNI中定义的结构体

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

其中,name代表了Java中的函数的名字,而signature代表了这个函数的函数签名,fnPtr则是一个函数指针,指向一个C函数,也就是native方法对应的函数。

.so文件

我们都知道,在项目中使用NDK时,它会生成so文件供我们的Java代码调用。我们使用某些Java语言编写的库时,它可能内部也用到了.so文件。由此可见so文件对我们Android开发者十分重要,那么它到底是什么呢?

ABI

目前Android系统支持了七种不同的CPU架构:ARMv5、ARMv7、x86、MIPS、ARMv8、MIPS64、x86_64。而这些CPU架构每个都关联了一个ABI。

ABI,即应用程序二进制接口(Application Binary Interface)。它里面定义了二进制文件(如.so文件)如何在相应平台上运行。从使用的指令集、内存对齐到可用系统函数库都有涉及。每个CPU架构对应了一个API:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64

什么是.so文件

so是shared object的缩写,也就是共享对象的意思。它是一种二进制文件,里面存放的是机器可以直接运行的二进制代码。正是因为如此,所以反编译so库的难度会比反编译普通Java代码的难度更大。so主要应用在Unix和Linux操作系统中,大到操作系统,小到一个专用的软件,都离不开so。其实Windows中有和so类似的东西,也就是我们常见的dll(动态链接库),其实它们是相同的事物,只是名字不同而已。

使用so的好处

  • 让开发者最大化利用已有的C和C++代码,达到重用的效果,利用软件世界积累了几十年的优秀代码
  • 二进制,没有解释编译的开销,实现同样的功能比纯粹Java实现要快
  • 内存不受VM单个应用限制,可以减少OOM。

需要注意的问题

虽然很多设备都支持多于一种的ABI(比如ARM64和x86设备也可以同时运行armeabi-v7a和armeabi的二进制包),但这种往往是要经过一种模拟层,(如x86设备模拟arm的模拟层),导致性能有所损耗。因此我们最好是针对特定的平台提供特定的so文件,从而避开模拟层,得到更好的性能。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

%d 博主赞过: