JNI 与 NDK 入门(三)

这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战

异常的处理

异常的例子

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

首先,我们在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的头文件,也就是。我们可以先看看这个头文件中包含了哪些信息:

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文件,从而避开模拟层,得到更好的性能。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享