Windows下搭建Android NDK开发环境
更新:比较新的版本的Android NDK都自带基本的GNU工具链,所以不用安装庞大的cygwin或者MSYS了,直接解压NDK然后在Eclipse里配置编译器就可以了。
————————————————————————————————————
Android NDK需要使用Linux下的make、gdb等开发工具,因此要安装一个模拟的Linux环境。这里选择最常用的cygwin。MSYS应该也可以,不过没有亲自试过,留给有求证精神并鄙视cygwin的庞大和缓慢的Coder去验证!
cygwin有自己的安装器,相当于Linux发行版下的包管理器,用来管理软件。打开后选择从网络安装,选择一个合适的镜像,偷懒的话直接把Devel分类下的软件全部选上,点击下一步后这个包管理器会自己解决各种乱七八糟的依赖关系,给你下载安装几个G的软件包。如果有洁癖或者网络不给力,可以自己慢慢选择要装哪些软件,这样装的东西会少很多。顺便吐槽一句,cygwin的这个图形化包管理器体验真是渣,快装完的时候有个选项没看清,手贱点了一下上一步,我再次点击下一步的时候它就给我卸载又重新安装配置了一遍,于是又多花了十几分钟。。。有没有省事点的像aptitude这样牛气哄哄的工具?
装完cygwin后还要简单地配置一下。首先请下载最新版本的Android NDK并解压,解压后的路径名不能包含空格!这是NDK自己的硬性规定,不然就等着出错吧……这些准备好后打开cygwin terminal。这货虽然不如Linux下各种功能强大的Terminal,但比Windows的cmd顺眼多了。首先看看你的home目录准备好了没有,方法很简单,输入echo ~即可。如果输出的目录类似于/home/xxx,那就没问题了;如果输出是Windows用户的home目录,那就打开Windows环境变量设置,删掉home变量(我以为会对Windows系统有影响的,删掉后发现没有可见的变化……),然后在cygwin根目录的home目录下建立一个与你当前登录Windows的用户名同名的文件夹。重启cygwin terminal后,再次输入echo ~检查,可以看到cygwin已经将home目录重定向到你新建的文件夹。
接下来把/etc/defaults/etc/skel目录下的.bash_profile、.bashrc、.inputrc这几个文件复制到自己的home目录下,然后在.bash_profile文件结尾添加类似的两句:
NDK=/cygdrive/d/android-ndk/android-ndk-r9/ ##这里是ndk的目录,我是注释,你看不见我!
export NDK
这几句是给cygwin设置环境变量。/cygdrive/d/对应Windows的D盘,其他盘依此类推。熟悉Linux环境的朋友用terminal可以轻松完成这几步,不熟悉也没关系,默默地复制粘贴文件然后用记事本打开文件编辑吧,哈哈~
做完上面的几步,NDK环境基本搞定,接下来可以简单测试一下。在terminal中执行cd $NDK/samples/hello-jni/,然后再执行../../ndk-build,如果看到这样的
那么恭喜你,你的环境搭建完成~
Eclipse环境下使用Android NDK
环境搭建好后我还惆怅了一阵子,Google就给了hello-jni这么一个破例子,NDK到底怎么使?继续查了很多资料终于有了眉目。在hello-jni工程的jni目录下,有Android.mk这样一个文件,这其实就是一个Makefile文件。这个文件可谓麻雀虽小,五脏俱全,该有的都有了,可以成为很好的范本和copy对象。接下来在Eclipse下打开我们自己的Android工程,已有的或者新建的都行。在Project Explorer下右键选中工程打开Properties,然后找到Builders。这里列举了一个Android工程在编译时用到的一些工具。接下来我们选中New,选择Program类型,接下来就是具体的设置了。贴图以示清白:
Name可以随便取,具体的Location就要按照自己的安装路径设置。最后的Arguments比较长,我的是这样的--login -c "cd '${project_loc}' && $NDK/ndk-build NDK_DEBUG=1",其中NDK_DEBUG=1是设置编译出来的so文件是Debug版本,不需要的可以去掉这一句。
接下来在Android工程目录下新建一个文件夹jni,这个文件夹主要是存放mk文件和C/CPP的代码文件。于是别犹豫,赶紧把hello-jni给的Android.mk文件复制进去。
在Android工程下新建一个Java文件TestJni.java:
public class TestJni {
    public native void test();
   
    static {
        System.loadLibrary("testjni");
    }
}
然后把Android.mk文件的LOCAL_MODULE对应的值改成testjni。LOCAL_SRC_FILES这个选项是指所用到的C/CPP代码文件。Jni的C/CPP代码有其固定格式,得先用javah工具生成头文件,然后按照头文件的函数声明来写具体代码。关于javah命令的使用,可以查阅jni的有关资料,这里不再赘述。这里有篇很实用的文章,提到的错误基本人人都会犯一遍:《用javah 导出类的头文件, 常见的错误及正确的使用方法》。
正确生成头文件,然后按照头文件中的声明格式写C/CPP文件,把头文件和C/CPP文件添加到Android工程的jni目录里,最后点Run As Android Application,这些代码就会被编译成so文件,在Java代码中通过native方法可以方便调用。
  
YUV420SP格式图像的旋转及矩形区域的截取
YUV420SP,即所谓的NV21格式,是Android系统中摄像设备的预览数据的默认格式。Android API中有个方法Camera.Parameters.setPreviewFormat(int pixel_format),用来设置预览数据的格式。尝试过设置其他如jpeg、png等更省事的格式,但在运行中都会报错,仔细查阅API文档,有这么一句:It is strongly recommended that either NV21
or YV12
is used, since they are supported by all camera devices.这才知道不同Android设备支持的格式可能不一样,但NV21和YV12是所有设备都支持的格式。接下来将以默认的格式NV21为例。
在ARGB颜色模式下的图像操作都非常轻松,因为每个像素的颜色都用一个32位的整型数表示,按空间顺序排列,非常符合人的正常思维。但NV21格式的图像数据就有点绕了,把Y和UV放到了两个平面上,UV的采样频率的总和还只有Y的一半,平均下来一个像素占用12位。NV21格式的具体介绍可以从下面的参考文章找到,这里不再赘述,总之这个格式折腾了我好久…
下面是一段在YUV420SP格式下截取矩形图像区域的代码,逻辑简单明了,但细节处容易出错。仅供参考:
void getTargetRect(byte[] src, byte[] dst, int srcW, int srcH, int startW,
            int startH, int dstW, int dstH) {
        if (src == null || dst == null || srcW < 1 || srcH < 1 || startW < 0
                || startH < 0 || dstW < 1 || dstH < 1 || startW + dstW > srcW
                || startH + dstH > srcH)
            return;
        int srcFrameSize = (srcW * srcH * 3) >> 1;
        int dstFrameSize = (dstW * dstH * 3) >> 1;
        for (int j = startH; j < startH + dstH; ++j) {
            int jOffset = j - startH;
            int srcUv = srcFrameSize + (j >> 1) * srcW;
            int dstUv = dstFrameSize + (jOffset >> 1) * dstW;
            for (int i = startW; i < startW + dstW; i += 2) {
                int iOffset = i - startW;
                dst[jOffset * dstW + iOffset] = src[j * srcW + i];
                dst[jOffset * dstW + iOffset] = src[j * srcW + i + 1];
                if ((jOffset & 1)==0) {
                    dst[dstUv + iOffset] = src[srcUv + i];
                    dst[dstUv + iOffset + 1] = src[srcUv + i + 1];
                }
            }
        }
    }
有关图像旋转,参考文章[2]已经给出实现代码,需要注意的问题是旋转的方向,旋转的方向可以通过改变循环系数的增减方向来改变。
有意义的参考文章:
[1]原YUV格式的解析 Android NV21 视频采集