Android JNI/NDK 开发实用技巧

2018-04-29  本文已影响0人  幽客

由于工作内容大多是和Camera相关的算法集成, 所以经常会用到JNI/NDK, 在此记录一下开发过程中一些注意事项以及一些小技巧.

动态注册JNI函数失败(no static or non-static method)

此类问题一般有两种情况:

  1. 包名,类名或者函数签名写错了, 这个解决方法就仔细检查一下就行, 当然如果一些签名不知道怎么写, 可以用静态注册方法通过javah生成头文件, 然后看下头文件里面注释, 把注释的签名复制过来就行
  1. 由于加入了混淆机制, 导致无法通过类名来找到对应方法了, 这种情况我遇到过两次, 表现稍有差异, 但本质问题都是混淆引起的.

    • Java中定义的native方法, 在代码中其他地方用到了就能注册上, 没用到的只声明了的函数就会注册失败, 原因是有些没有用到的函数, 混淆是会自动剔除, 所以实际运行的代码中就没有相关方法了, 动态注册就会失败

    • 所有方法都注册不上, 同时也确定相关包名类名签名没有错, 这种情况就是即使你用到了相关方法, 但经过过混淆后已经不是原来的名称了, 所以注册失败

这两种错误解决方法也简单:

  1. 禁用混淆, 这样做不是太好, 混淆对防止反编译和apk瘦身有很大帮助

  2. 添加混淆白名单, 和JNI相关的类或者方法不做混淆, 比如在项目proguard.flags(或者其他自定义的混淆文件中)加入如下代码:

-keep class com.android.gallery3d.jpegstream.JPEGOutputStream { *; }

64位系统中App调用32位so库

由于某些原因(比如使用比较老的算法库), 算法库或者其他C/C++库只有32位的, 但使用的Android系统是64位的, 因此我们需要在64位系统上调用32位算法库, Android 64位系统本身是既能使用64位库, 也能使用32位库.Android中对于调用32位还是64位算法库判断依据如下:

Apk本身(Java代码)不分32位和64位, 只有so库会分32位和64位, 当App首次运行时, 默认会去加载64位so库, 如果没有一个so库是64位的, 就加载32位so库, 只要有一个so库是64位, 则所有so库都是加载64位, 简单说64位系统中App加载32位so库做法就是只放32位so库, 不包含任何64位so库.

要达到上述目的, 要注意如下两点(分为通过Android编译系统编译的系统App和使用IDE开发的第三方App):

wenzhe@ubuntucomp:~/code/HLOS$ ll out/target/product/msm8909w/system/app/SnapdragonGallery/lib/arm/
total 8
drwxrwxr-x 2 wenzhe wenzhe 4096  4月 25 20:52 ./
drwxrwxr-x 3 wenzhe wenzhe 4096  4月 25 20:52 ../
lrwxrwxrwx 1 wenzhe wenzhe   38  4月 25 20:52 libjni_gallery_eglfence.so -> /system/lib/libjni_gallery_eglfence.so
lrwxrwxrwx 1 wenzhe wenzhe   37  4月 25 20:52 libjni_gallery_filters.so -> /system/lib/libjni_gallery_filters.so
lrwxrwxrwx 1 wenzhe wenzhe   40  4月 25 20:52 libjni_gallery_jpegstream.so -> /system/lib/libjni_gallery_jpegstream.so

可以看到一些so库是通过软链接映射到system/lib/对应的so库的, 之所以要这样做, 因为系统源码方式编译的App是不会将so打包到apk中的, 当Java中loadLibraries时就默认加载64位的了, 所以要通过LOCAL_JNI_SHARED_LIBRARIES := libxxx建立软链接收, 就知道应该加载那种类型的so了.

注意事项: 通过源码编译的方式调试的时候, 需要清除App数据后, 删除system目录下对应的文件夹, 重新push apk, 然后重启, 不然关于加载32位还是64位的so库修改可能不生效. 对应IDE开发的App, 我自己没试过, 但应该是要卸载原有apk然后重新安装, 这样可避免一些不必要的坑.

通过JNI传递byte数组

图像/视频处理就少不了数据传递, 图像/视频处理的数据都是用数组存储的, 一张图片的数据一般在一块连续内存中(也有不连续分开的, 比如YUV中Y和UV不是同一块内存区域), Java中用byte数组存储(0~255)这些图像像素信息, C/C++中则用unsigned char数组, 如果是在App中集成算法, 就需要把Java中bype[]传到C/C++中, 常用的方式有如下两种:

GetByteArrayElements()

这是比较常用的方式, Java中以byte[]作为native方法参数, C/C++中通过如下方式进行获取和释放:

jbyte* data = env->GetByteArrayElements(array, NULL);
// 将data转为unsigned char* 传给算法 ...
// 释放
env->ReleaseByteArrayElements(array, data, 0);
}

但是大多数人可能没有注意到这两个方法的最后一个参数, GetByteArrayElements(jbyteArray array, jboolean* isCopy)最后一个参数是指获取到的数组是否是copy的, 由于VM实现不同, 从Java层传下来的数组有可能是虚拟机重新分配内存, 然后copy Java数组中的数据, 最后返回指针到C/C++中, 在比较低的Android版本中可能存在这个问题, 现在都2018年了, 基本上都不是copy的, 都是直接获取原始数组指针, 当我们需要确定是否是copy的时候, 可以传递一个bool类型指针, 通过判断bool值就知道了, 即:

jboolean isCopy = JNI_FALSE;
jbyte* data = env->GetByteArrayElements(array, &isCopy);
if (isCopy) {/*do something*/}

另外如果不是copy的方式, 有两个关于虚拟机(VM)的操作叫做pinned down和un-pinned, 我理解意思是Java层的数组被native层获得后相当于被占用, un-pinned后才表明被释放.

同样由于获得的数组后可能并不是原始数组的指针, 所以释放的时候有个参数来指定相关数据是否要回写到Java层

 void ReleaseByteArrayElements(jbyteArray array, jbyte* elems, jint mode)

释放的时候最后一个参数 mode有下面三种取值:

上面的说法可能不是太好理解, 但我们需要知道有这么个事情, 同时大多数场景mode的值一般为0, 另外上面所讲的内容是在Android Developer上看到的, 有兴趣可以读一下 https://developer.android.google.cn/training/articles/perf-jni
如果有不同理解或者我的理解有误的话, 欢迎指正并讨论.

我们常见的大多数应用场景下, release最后一个参数设置为0即可.

GetDirectBufferAddress()

通过此方法获取数据有些局限性, 但在一些应用场景效率非常高, 基本没有额外的开销, 使用方法如下:

  1. Java层通过ByteBuffer.allocateDirect()申请内存空间.

  2. 将ByteBuffer作为native方法参数传到C/C++中

  3. C/C++代码中通过env->GetDirectBufferAddress(jobject)可直接获取到数组指针进行操作

之所以可以这样操作, 是因为ByteBuffer.allocateDirect()函数申请的内存是通过系统(OS)级别操作来分配, 所以就可以方便的获取地址进行操作, 一个典型的应用场景: 使用Camera API2获取预览数据用进行算法处理后, 用OpenGL ES进行绘制并显示.
用OpenGL ES的人应该知道很多OpenGL ES的接口很多是用java.nio.Buffer作为参数的, 并且申请的时候要使用allocateDirect()也是这个道理, 同时Camera API2中预览拍照返回的数据也都是ByteBuffer类型, 这样的处理方式以后也会用的更多.
关于使用Camera API2获取预览数据用进行算法处理后, 用OpenGL ES进行绘制并显示这个应用场景我后续会单独写一篇博客, 敬请关注.

C/C++中打印Log

这个比较常见, 教程也比较多, 我这里也做下记录:

  1. 在C/C++中引入系统Log头文件 #include <android/log.h>
  2. 通过宏定义的方式定义不同等级Log和Log标题
#define TAG "HelloTAG"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__)
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__)
  1. 在编译C/C++文件的Android.mk中加入LOCAL_LDLIBS :=-llog然后就可以使用LOGE("Hello World")打印Log了

定位native层的crash

crash问题是开发中比较常见的, Java由于其特性, Crash问题我们直接看AndroidRuntime的Log就行, C/C++的就要麻烦些了.
说明: 如果Crash对应的so没有符号表, so库是别人编译的, 额外去除了符号表(Releas版本), 这种情况是没法定位crash代码具体是在哪个位置的.
对于有源码并且是自己可以编译的情况下, 可通过如下方式对Crash代码进行定位:

注意事项: 如果源码和so库不是完全对应的, 即so库发布后, 源码有过修改,这样会导致定位的行数有些偏移, 不完全准确, 需要额外注意下.

小技巧: 只看native层crash log, 可直接 adb logcat *:F

上一篇下一篇

猜你喜欢

热点阅读