消失了几个月我又回来了,距离上一次承诺更新NDK的知识依旧过了好久,我想说我真的没有太监。。。最近换了工作,来到了魔都混日子,因为找工作耽误很多写博文的时间。不得不说现在安卓开发的工作真难找啊,找了一个多月才找到一个6,7k的- -希望大家不要裸辞,慎重跳槽。。。不过这家公司的需求都比较复杂,属于之前接触较少的,而且对NDK开发有很大的要求,也可以趁机锻炼一下自己!
本文的标题中包含自虐的字眼,没错,NDK开发就是个自虐的玩意儿,有时候真的让人抓狂,可能搞一整天也都是徒劳。。。不过今天介绍一种可以把重度自虐变成轻度自虐的简便方法,那么我们现在进入正题!
Android Studio 2.2后,加入了对CMake的支持,从此AS开发NDK的能力和简便性得到了巨大的飞跃,本文就来介绍一下操作流程。
首先需要下载NDK的开发环境,打开Default Setting->Android SDK->SDK Tools,选中CMake,LLDB,NDK下载,如图所示:
NDK即Native Development Kit,我们使用C/C++来进行开发的必要环境,LLDB是NDK的调试工具,而CMake则是我们今天的主角。
安装完成后,我们新建一个工程,本文将用JNI实现YUV2RGB这个经典算法,所以将工程命名为YUV2RGB,特别注意的是在此处勾选上include c++ support,这样studio就会给我们生成一个已经搭建好基础的NDK工程。
在一路Next时,不要手快,在这一步一点要勾上Exception Support和Runtime Type Information Support,来支持rtti和exception,否则某些机型也许无法跑你的程序。
工程创建完毕后,我们来看看目录和之前的普通工程有什么不一样。在App目录下多了一个CMakeLists.txt,这个可不是单纯的文本文档,这里就是配置CMake的地方,在main目录下多了一个Cpp文件夹,这里面用来存放用C/C++写的代码。
默认的情况下CMakeLists.txt是存放在app/目录下,当然我们可以改变它的位置,这里我们就看一看这个工程的gradle文件又有什么特别之处:
在defaultConfig下面多了一个externalNativeBuild {cmake {cppFlags "-frtti -fexceptions"}},这里就是我们一开始勾选的rtti支持,如果一开始忘记勾选,在此加入也可以。
在android下面也多了一个externalNativeBuild {cmake {path "CMakeLists.txt"}},这里就是指定的CMakeLists文件路径,如果你要把该文件放到其他位置,只需要在此修改即可。
那么我们先来分析CMakeLists这个文件。一打开你也许会略微蛋疼,充斥着没有颜色提示的文本,各种奇怪的符号,比较AS对CMake支持的时间不长,也许以后谷歌会对这一块更加完善。在自动生成的CMakeLists里,充满了各种注释,如果我们整理一下,一切就很清晰了,比如这样:
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} )
这下就清楚多了吧,其实对于一个比较基本的NDK程序,这几行就足够了,先声明出CMake的版本,然后添加你自己编写的Cpp文件和文件的位置,将log support library的位置储存到log-lib中,最后连接所有存在的动态库文件就大功告成。
为了保证本文讲的更加清楚,我们不使用已经自动创建好的部分,重新在Cpp文件夹里创建一个cpp文件yuv2rgb-lib.cpp,然后在cMakeLists中这样配置:
cmake_minimum_required(VERSION 3.4.1)
add_library(native-lib SHARED src/main/cpp/native-lib.cpp )
add_library(yuv2rgb-lib SHARED src/main/cpp/yuv2rgb-lib.cpp )
find_library(log-lib log)
target_link_libraries(native-lib yuv2rgb-lib ${log-lib} )
这样就可以轻而易举的把新创建的文件放入CMake的编译队列中。那么我们自己创建的这个Cpp文件里应该怎么写呢?里面是一个YUV->RGB的算法,当然这个算法用java也可以实现,但是执行效率就不言而喻,使用Android原生的YuvImage也可以对其进行转换,这里底层也是使用了native方法,但是这个方法据说在某些机型里是无法使用的。那么最稳的方式就是我们自己用C/C++来写一个。
#include <jni.h>
extern "C"{
jintArray Java_com_lbw_camerapreviewcallback_NdkLoader_yuv2Rgb(JNIEnv *env,
jobject thiz, jbyteArray buf,
jint width, jint height) {
jbyte *yuv420sp = (env)->GetByteArrayElements(buf, 0);
int frameSize = width * height;
jint rgb[frameSize];
int i = 0, j = 0, yp = 0;
int uvp = 0, u = 0, v = 0;
for (j = 0, yp = 0; j < height; j++) {
uvp = frameSize + (j >> 1) * width;
u = 0;
v = 0;
for (i = 0; i < width; i++, yp++) {
int y = (0xff & ((int) yuv420sp[yp])) - 16;
if (y < 0)
y = 0;
if ((i & 1) == 0) {
v = (0xff & yuv420sp[uvp++]) - 128;
u = (0xff & yuv420sp[uvp++]) - 128;
}
int y1192 = 1192 * y;
int r = (y1192 + 1634 * v);
int g = (y1192 - 833 * v - 400 * u);
int b = (y1192 + 2066 * u);
if (r < 0) r = 0; else if (r > 262143) r = 262143;
if (g < 0) g = 0; else if (g > 262143) g = 262143;
if (b < 0) b = 0; else if (b > 262143) b = 262143;
rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | ((b >> 10) & 0xff);
}
}
jintArray result = (env)->NewIntArray(frameSize);
(env)->SetIntArrayRegion(result, 0, frameSize, rgb);
(env)->ReleaseByteArrayElements(buf, yuv420sp, 0);
return result;
}
}
YUV和RGB的转换公式如下:
其中的数学原理我们这里不再探究,毕竟这里只是拿他当一个小例子,有兴趣的可以去这篇博客里学习。
我们来看这个函数里的几个参数JNIEnv *env, jobject thiz, jbyteArray buf, jint width, jint height。
也许最让人疑惑的就是JNIEnv *env和jobject thiz,JNIEnv*是指向JNI函数表的接口指针,可以通过它对Java端的代码进行操作。jobject thiz,如果native方法没用用static修饰的话,它就是native方法的类实例。如果是static修饰的话,就代表native方法的类的class对象实例。
以上这两个参数都是必须带有的,而后面的则是我们可以自定义的。我们先来看下这个native方法的声明:
public static native int[] yuv2Rgb(byte[] buf, int width, int height);
没错,我们在java代码中只需要传入这3个自定义的参数,一个包含原始YUV数据的byte数组,和int型的图像宽高,他们在JNI中对应的类型为jbyteArray和jint。在NDK中有些类型是无法直接使用的,我们需要调用NDK的方法来将他们转换为C++中可用的类型。具体的这里我们不细说,因为很多前辈们已经讲了很多遍了,想学习的可以点击这里。
要注意的是C/C++里可没有像Java GC这样好用的垃圾回收机制,在代码的最后记得回收相关的变量。
最重要的一点是,在代码的开头有一段类似Java包名的信息:Java_com_lbw_camerapreviewcallback_NdkLoader_yuv2Rgb,这里就是程序寻找Java部分声明的指引,这里程序就会去com.lbw.camerapreviewcallback.NdkLoader这个类里寻找native方法的声明。这里的包名并没有写成本程序的包名,因为我们接下来要在其他程序里去使用它,这里我不再去封装jar或者aar包,所以直接写成这个包名。
那么如何把cpp编译为SO文件呢?我们只需要Build->Rebuild project,重新构建一下工程,然后在app/build/intermediates/cmake目录下就可以找到生成好的SO文件。
然后我们新建一个工程,用来试验这个SO库到底能不能用。而包名就用我们刚才定义的那个,让JNI程序可以直接适应这个新的程序,那么我们就创建一个对应的NdkLoader类,内容如下:
public class NdkLoader {
static {
System.loadLibrary("yuv2rgb-lib");
}
public static native int[] yuv2Rgb(byte[] buf, int width, int height);
}
至于YUV数据的来源,当然还是从相机的预览帧里取了,所以我这里简单的写了一个相机预览的程序,加入了预览回调,当点击按钮时,截取当前帧,转换为RGB数据,并存到SD卡里。只贴出关键代码:
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
camera.addCallbackBuffer(data);
if (isCatch) {
isCatch = false;
int[] result = NdkLoader.yuv2Rgb(data,camera.getParameters().getPreviewSize().width,camera.getParameters().getPreviewSize().height);
Bitmap bitmap = Bitmap.createBitmap(result,camera.getParameters().getPreviewSize().width,camera.getParameters().getPreviewSize().height, Bitmap.Config.RGB_565);
try {
FileOutputStream fileOutputStream = new FileOutputStream(new File("sdcard/result.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
bos.flush();
fileOutputStream.close();
bos.close();
bitmap.recycle();
bitmap = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
这里就不在讲述怎么导入SO文件,当我们点击按钮后,就通过NdkLoader这个类调用yuv2Rgb这个native方法,将byte[] data转换成一个RGB的byte[] result,再将它存入SD卡。
点击按钮后,去SD卡里寻找这张图片。果然,这里已经转换成功了,到此我们的这个SO库也就做好了。
当然为了确保每个程序都可以使用,肯定不能靠这样一直修改包名,所以这里建议将NdkLoader做成jar包,这样不管在任何程序里,只要导入这个jar包就可以使用了。不过我更推荐使用aar,这样就把SO文件也引入了进去,aar的制作方法可以参见我之前的博客。
本篇博文只是讲述了NDK开发的入门之入门的知识,希望可以帮到一些刚刚接触这一块的朋友。而真正的NDK开发绝对是非常博大精深的,绝对要比我们平时用Android SDK写的东西难之又难,而资料也更加稀少与晦涩。所以我以后还会继续分享一些NDK开发的心得,由于我本人并不是专业开发C/C++的,博文中难免会有许多错误,希望大家及时指正!下一篇博客的内容其实已经构思好了,就是导入OpenCV的native库进行NDK二次开发!希望大家都可以在这条自虐的道路上越走越远...敬请期待!
NDK的项目和相机预览的项目我都进行了上传,本着“技术来源于分享“的原则,依旧是0分,需要的同学可以自行下载。