“这是我参与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代码可以正常执行下去,我们需要:
- 在Native层手动清空异常信息(ExceptionClear),保证代码可以运行。
- 实行补救措施保证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 toAndroidBitmap_getInfo
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:59: undefined reference toAndroidBitmap_lockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:64: undefined reference toAndroidBitmap_lockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:86: undefined reference toAndroidBitmap_unlockPixels
E:\test\NdkBitmap\app\src\main\cpp/native-lib.cpp:87: undefined reference toAndroidBitmap_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依赖库的方式有下面的两种:
- 如果JNI_Lib定义了JNI_Onload函数,则通过JNI_Onload函数
- 如果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文件,从而避开模拟层,得到更好的性能。