Chinaunix首页 | 论坛 | 博客
  • 博客访问: 459421
  • 博文数量: 40
  • 博客积分: 1410
  • 博客等级: 军士长
  • 技术积分: 1396
  • 用 户 组: 普通用户
  • 注册时间: 2011-03-22 19:26
个人简介

嵌入式系统工程师,从事视频、图像、网络、虚拟化等方面的底层软件开发与优化。

文章存档

2014年(4)

2013年(10)

2012年(14)

2011年(12)

分类: Android平台

2013-09-20 21:21:17


1. Android NDK开发过程记录

(1). 创建JNI头文件

    在工程目录下输入:

点击(此处)折叠或打开

  1. javah -classpath bin/classes -d jni com.example.rgbir.rgbirJNI
遇到的问题:

点击(此处)折叠或打开

  1. error: cannot access com.example.rgbir.rgbirJNI
  2. class file for com.example.rgbir.rgbirJNI not found
  3. javadoc: error - Class com.example.rgbir.rgbirJNI not found.
  4. Error: No classes were specified on the command line. Try -help.
点击运行工程,重新在命令行输入命令即可。


(2). Eclipse中配置NDK路径
    首先,将工程转换为C/C++工程。否则无法在项目属性里找到C/C++配置选项。在Eclipse界面左边的Package Explorer中展开工程目录,找到jni文件夹,右键点击文件夹:"New" --> "Other..." ,在弹出界面中双击"C/C++",选择"Convert to a C/C++ Project" ,按照引导界面勾选相应选项,这样就能编译C/C++了。
   接着,配置NDK路径。右键点击项目名,"Properties" --> "C/C++ Build",把Use default bulid commad选项去掉,bulid command输入android ndk包的路径,如:/home/work/android-ndk-r9b/ndk-build NDK_DEBUG=1(如果配置好了ndk环境,直接输入"ndk-build  NDK_DEBUG=1")

参考博客:《ubuntu下搭建eclipse + ndk编译JNI库》

(3). 屏蔽语法错误检查
   对代码分析(code analysis)进行屏蔽, 位置: Project -> Properties -> C/C++ General -> Code Analysis;
在选择"Using Project Setting"下面的"Syntax and Semantic Errors", 进行屏蔽。

(4). 解决JNI层函数及头文件>下的黄线
    因为CDT索引器没有找到路径,而NDK知道。进入项目属性页面,"C/C++ General" --> "Paths and Symbols",点击右边的"Add...",输入JNI相关头文件所在目录,类似"${env_var:ANDROID_NDK}/platforms/android-18/arch-arm/usr/include"。

参考博客:使用Eclipse编译本地代码

(5). C++文件包含.h头文件
   jni文件加下的C++程序包含的.h文件,.h文件中所有函数声明都必须在如下宏定义之中。否则,会报undefined reference to的错误。

点击(此处)折叠或打开

  1. #ifdef __cplusplus
  2. extern "C" {
  3. #endif

  4. //一段代码

  5. #ifdef __cplusplus
  6. }
  7. #endif

(6). java.lang.UnsatisfiedLinkError:Native method not found
   so文件编译生成后,运行时,有时候会遇到java.lang.UnsatisfiedLinkError: Native method not found问题。
   a. JNI方法头部大小写问题     
      在C++中,方法名:Java_com_XXX,而不是java_com_XXX。建议直接从生成的.h头文件直接复制方法名到C或者C++文件中。
   b. C++文件问题
        如果是C++文件(.cpp或者.cc),要使用extern "C" {   } 本地方法括进去
   c. 往JNI方法中传值问题
         如,调用native方法sendSomeThing(Object object),如果传入的object为null,有可能会报上面错误。

(7). SD卡中创建文件、写入文件、删除文件操作的支持
    在AndroidManifest.xml中加入访问SDCard的权限如下:

点击(此处)折叠或打开

  1. <!-- 在SDCard中创建与删除文件权限 -->
  2. <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
  3. <!-- 往SDCard写入数据权限 -->
  4. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    要往SDCard存放文件,程序必须先判断手机是否装有SDCard,并且可以进行读写。

(8). adb连接手机
    a. 在终端输入"lsusb",获取VID和PID。
    b. "gedit /etc/udev/rules.d/51-android.rules",添加下列规则: (记得替换其中的VID和PID)

点击(此处)折叠或打开

  1. SUBSYSTEM=="usb", ATTR{idVendor}=="0a5c", ATTR{idProduct}=="e688", MODE="0666"
   c. "gedit ~/.android/adb_usb.ini",添加VID

点击(此处)折叠或打开

  1. 0x0a5c
   d. 重启adb

点击(此处)折叠或打开

  1. adb kill-server
  2. adb start-server

(9). Eclipse项目名左边有红感叹号
    原因:工程中classpath中指向的包路径错误。
    办法:右键单击你的项目工程,找到“Build Path” --> “Configure Build Path...” ,然后上面有几个选项卡找到Libraries。这里看到的就是你工程里面引用的所有的 jar,看看是不是在某个jar图标上有个很小的黄色的感叹号? 如果有的话就没错了,先选中这个jar,点击右边的 “Remove” > 点击 “OK ”。

(10). omp.h arm_neon.h stdbool.h float.h等头文件路径添加

    右键点击项目名 --> C/C++ General --> Paths and Symbols --> GNU C++,点击右边的“Add...”,添加下述路径:

点击(此处)折叠或打开

  1. /home/work/android/android-ndk-r9/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86/lib/gcc/arm-linux-androideabi/4.6/include
    为支持OpenMP,还需在jni文件夹下的Android.mk中添加如下:

点击(此处)折叠或打开

  1. LOCAL_CFLAGS += -fopenmp
  2. LOCAL_LDFLAGS += -fopenmp

(11). OpenCV for Android NDK的支持

     添加头文件路径:/home/work/android/workspace/OpenCV-2.4.8-android-sdk/sdk/native/jni/include,配置同上,具体如下图所示。OpenCV-2.4.8-android-sdk的下载地址:
    OpenCV for Android除了对C语言的支持,OpenCV支持也支持Java,具体方法参考文章:《在Android中使用JNI调用Opencv本地代码 配置方式 边缘检测 范例代码》
    另外,源代码上编译:。注意记得编译为Release版本,并且使能TBB,运行速度较快,其它配置根据自己需要进行配置。

(12). Android.mk与Application.mk(支持OpenCV4Android与OpenMP4Android)
    a. Android.mk
       其中,与OpenCV相关的有第4、6-11、15行;与OpenMP相关的有第13、14行。

点击(此处)折叠或打开

  1. LOCAL_PATH := $(call my-dir)
  2. include $(CLEAR_VARS)

  3. OPENCV_LIB_TYPE:=STATIC

  4. ifeq ("$(wildcard $(OPENCV_MK_PATH))","")
  5. #try to load OpenCV.mk from default install location
  6. include /home/work/android/workspace/OpenCV-2.4.8-android-sdk/sdk/native/jni/OpenCV.mk
  7. else
  8. include $(OPENCV_MK_PATH)
  9. endif

  10. LOCAL_CFLAGS += -fopenmp
  11. LOCAL_LDFLAGS += -fopenmp
  12. LOCAL_LDLIBS := -lz

  13. LOCAL_MODULE := ImageProcess
  14. LOCAL_SRC_FILES := ImageProcess.cpp

  15. include $(BUILD_SHARED_LIBRARY)
    b. Application.mk
注意:对于OpenCV,支持ARMv7是很有必要的,测试表明,同样的运算过程,使用armeabi-v7a与使用armeabi相比,运算时间由原来的7.3ms较少到3.4ms。当然在Application.mk中,同时支持两种格式让应用加载时自动选择合适的格式去加载是最稳妥的。如果系统支持ARMv7,就会首先使用v7的库。

点击(此处)折叠或打开

  1. APP_STL := gnustl_static
  2. APP_CPPFLAGS := -frtti -fexceptions
  3. APP_ABI := armeabi armeabi-v7a

(13). ADB工具显示打印信息
   
有时候Eclipse的LogCat不能正常显示打印信息,可以通过如下命令在终端里显示,可以同时开多个窗口显示多种打印信息。

点击(此处)折叠或打开

  1. // xxx为Java代码中的Log.d()等函数中的标识字符串,或C++代码中的__android_log_print()标识字符串,不需要引号引起来
  2. adb logcat -v threadtime | grep xxx

(14). Java与C/C++之间传递变量与数组的方法

a. Java端的数组拷贝到本地

点击(此处)折叠或打开

  1. unsigned char array[TEST_BUFFER_SIZE];

  2. //直接将Java端的数组拷贝到本地的数据中,建议使用这种方式,更加安全
  3. (*env)->GetByteArrayRegion(env, buffer, 0, len, array);
b. 本地指针指向含有Java端数组的内存地址

点击(此处)折叠或打开

  1. //将本地指针指向含有Java端数组的内存地址
  2. unsigned char * pBuffer = (*env)->GetByteArrayElements(env,buffer,NULL);
  3. if( pBuffer == NULL ) {
  4.     LOG("GetByteArrayElements Failed!");
  5.     return;
  6. }

  7. //可以通过pBuffer指针来访问这段数组的值
  8. ...

  9. //最后不要忘记释放指针(减小引用计数)
  10. (*env)->ReleaseByteArrayElements(env,buffer,pBuffer,0);
c. 直接获取与Java端共享的内存块

点击(此处)折叠或打开

  1. unsigned char * pBuffer = (unsigned char *)(*env)->GetDirectBufferAddress(env,buffer);
  2. if( pBuffer == NULL ) {
  3.     LOG("GetDirectBufferAddress Failed!");
  4.     return;
  5. }
d. 传递JNI层的数组数据到Java端

点击(此处)折叠或打开

  1. /传递JNI层的数组数据到Java端,有两种方法,一种是本例所示的通过返回值来传递
  2. //另一种是通过回调Java端的函数来传递(多用于jni线程中回调java层)
  3. unsigned char buffer[TEST_BUFFER_SIZE];

  4. ...

  5. //分配ByteArray
  6. jbyteArray array = (*env)->NewByteArray(env,TEST_BUFFER_SIZE);

  7. //将传递数据拷贝到java端
  8. (*env)->SetByteArrayRegion(env, array, 0, TEST_BUFFER_SIZE, buffer);
e. Java层对应Native函数

点击(此处)折叠或打开

  1. private byte[] mTestBuffer1;
  2. private byte[] mTestBuffer2;
  3. private ByteBuffer mDirectBuffer;
  4.     
  5. private native void nativeSetBuffer1(byte[] buffer,int len);
  6. private native void nativeSetBuffer2(byte[] buffer,int len);
  7. private native void nativeSetDirectBuffer(Object buffer, int len);
  8. private native byte[] nativeGetByteArray();
f. JNI层为C++函数时API的不同
上诉JNI层的函数针对C语言源码,如果后缀为.cpp的C++程序,则有所不同。以上述b与d为例,如下修改:

点击(此处)折叠或打开

  1. // 1. C语言版本
  2. // 1.1 Java ---> JNI
  3. unsigned char * pBuffer = (*env)->GetByteArrayElements(env,buffer,NULL);
  4. // 1.2 JNI ---> Java
  5. jbyteArray array = (*env)->NewByteArray(env,TEST_BUFFER_SIZE);
  6. (*env)->SetByteArrayRegion(env, array, 0, TEST_BUFFER_SIZE, buffer);

  7. // 2. C++版本
  8. // 2.1 Java ---> JNI
  9. unsigned char * pSrcBuffer = (unsigned char *)env->GetByteArrayElements(buffer,NULL);
  10. // 2.2 JNI ---> Java
  11. jbyteArray array = env->NewByteArray(TEST_BUFFER_SIZE2);
  12. env->SetByteArrayRegion(array, 0, TEST_BUFFER_SIZE, buffer);

参考51CTO上tickTick的博客:《Android开发实践:Java层与Jni层的数组传递》 博客所附源码:JNI_Buffer.zip

(15). 使能Eclipse的并行化编译

(16). 头文件包含目录的导入与导出
      有时候导入其他人的工程时,所配置的包含目录不一致,导致Eclipse找不到正确的头文件,可以通过在工程属性里导出已有工程的头文件所在目录,导入到新工程。

(17). JNI动态库命名规则
      实际生成的动态库名会在Android.mk中指定的库名前添加lib之后添加.so,即Android.mk中指定名为xxx的库,最终生成libxxx.so。值得注意的是:在Java中的加载函数中必须使用xxx,而不带前缀lib也不带后缀.so,如下所示。另外,在Android.mk中指定的库名如果带lib前缀,那么下述加载函数中指定的库名仍然是省略lib的名字。

点击(此处)折叠或打开

  1. static {
  2.     System.loadLibrary("BinaryAlg");
  3. }

(18). 为JNI函数所在Java类添加同步机制

        在Android JNI层开发程序时,有时候在初始化函数中分配内存,在退出函数释放内存,在其它函数中使用这些内存。对于运算时间较长的函数,往往会遇到退出函数调用时,处理函数尚未执行完,因为两者由不同的线程执行。最终导致内存访问错误。处理函数访问已释放了的内存。
        可以在Java层的JNI函数所在类添加同步,使得部分或全部函数互斥访问。当然,也可以在JNI层添加同步机制。

点击(此处)折叠或打开

  1. public class MyAlg {
  2.    private Object mSync = new Object();

  3.    private native void myalg_init();
  4.    private native void myalg_exit();
  5.    private native byte[] myalg_process(byte[] inBuffer, int width, int height);

  6.    public void MyAlgExit() {
  7.       synchronized(mSync) {
  8.          myalg_init();
  9.       }
  10.    }

  11.    public void MyAlgExit() {
  12.       synchronized(mSync) {
  13.          myalg_exit();
  14.       }
  15.    }

  16.    public byte[] MyAlgProcess(byte[] inBuffer, int width, int height) {
  17.       synchronized(mSync) {
  18.          return myalg_process(inBuffer, width, height);
  19.       }
  20.    }
  21. }


2. 浮点优化选项 -ffast-math


C99 浮点环境支持科学和数学级别的应用,这些应用必须有相当高的精度,但是某些应用却不是如此,注重速度高于精度。对于这些以速度为重的应用, -ffast-math 选项定义了预处理器宏 __FAST_MATH__, 指示编译不必遵循 IEEE 和 ISO 的浮点运算标准。-ffast-math标记是一个群组选项,可以分别启用下面六个优化选项:

点击(此处)折叠或打开

  1. -fno-math-errno

  2. Disables the use of the global variable errno for math functions that represent a single floating-point instruction.

  3. -funsafe-math-optimizations

  4. The "unsafe math optimizations" are those that might violate floating-point math standards, or that do away with verification of arguments and results. Using such optimizations may involve linking code that modifies the floating-point processor's control flags.

  5. -fno-trapping-math

  6. Generates "nonstop" code, on the assumption that no math exceptions will be raised that can be handled by the user program.

  7. -ffinite-math-only

  8. Generates executable code that disregards infinities and NaN ("not a number") values in arguments and results.

  9. -fno-rounding-math

  10. This option indicates that your program does not depend on a certain rounding behavior, and does not attempt to change the floating-point environment's default rounding mode. This setting is currently the default, and its opposite, -frounding-math, is still experimental.

  11. -fno-signaling-nans

  12. This option permits optimizations that limit the number of floating-point exceptions that may be raised by signaling NaNs. This setting is currently the default, and its opposite, -fsignaling-nans, is still experimental.
测试结果:将此选项在Application.mk中打开后,运算时间由原来的910ms左右提高到700ms左右。编写应用程序对比生成后图像数据,发现在7558272(1944*2592*1.5)个点中,仅有175个点数不同,且最大相差值为6。因此在项目中可以打开该选项。

3. 指定NDK编译路径

默认的./ndk-build仅仅编译android-ndk-rXXX/jni/目录下的程序,如果要编译特定目录的程序,则可使用NDK_APP_APPLICATION_MK="绝对路径"。例如

点击(此处)折叠或打开

  1. ./ndk-build NDK_APP_APPLICATION_MK=/home/work/xxx-alg/demo/

4
. 仅保留Y数据,图像显示绿色而不是灰白

处理YUV格式数据时,仅保留Y数据,PC上运行的图像得到是灰白色,而手机上为绿色。原因是,PC上将UV数据初始化为128,而手机上未作初始化,为0。

附录1 YUV格式
YUV分成两种格式:
紧缩格式(packed formats):将Y、U、V值储存成Macro Pixels阵列,和RGB的存放方式类似。
平面格式(planar formats):将Y、U、V的三个份量分别存放在不同的矩阵中。

大多数 YUV 格式平均使用的每像素位数都少于24位元。主要的抽样(subsample)格式有YCbCr 4:2:0、YCbCr 4:2:2、YCbCr 4:1:1和 YCbCr 4:4:4。YUV的表示法称为 A:B:C 表示法:
4:4:4 表示完全取样。
4:2:2 表示 2:1 的水平取样,没有垂直下采样。
4:2:0 表示 2:1 的水平取样,2:1 的垂直下采样。
4:1:1 表示 4:1 的水平取样,没有垂直下采样。
DVD-Video 是以 YUV 4:2:0 的方式记录,也就是我们俗称的I420

YUY2及常见表示方法
YUY2(和YUYV)格式
为像素保留 Y,而 UV 在水平空间上相隔二个像素采样一次(Y0 U0 Y1 V0)(Y2 U2 Y3 V2)… 其中,(Y0 U0 Y1 V0)就是一个macro-pixel(宏像素),它表示了2个像素,(Y2 U2 Y3 V2)是另外的2个像素。 以此类推,再如:Y41P(和Y411)格式为每个像素保留Y分量,而UV分量在水平方向上每4个像素采样一次。一个宏像素为12个字节,实际表示8个像素。图像数据中YUV分量排列顺序如下:(U0 Y0 V0 Y1 U4 Y2 V4 Y3 Y4 Y5 Y6 Y7) …

YV12格式与IYUV类似,每个像素都提取Y,在UV提取时,将图像2 x 2的矩阵,每个矩阵提取一个U和一个V。YV12格式和I420格式的不同处在V平面和U平面的位置不同。在YV12格式中,U平面紧跟在Y平面之后,然后才是V平面(即:YUV);但I420则是相反(即:YVU)。NV12与YV12类似,效果一样,YV12中 U 和 V 是连续排列的,而在NV12中,U 和 V 就交错排列的。
排列举例: 2*2图像 YYYYUV; 4*4图像 YYYYYYYYYYYYYYYYUUUUVVVV




5. 性能测试环境搭建(valgrind + gprof2dot.py + graphviz + kcachegrind
valgrind下载源码,编译安装;
gprof2dot.p
y Python脚本,下载即可;
graphviz是dot所在的库,apt-get安装;
kcachegrind也是apt-get安装。

在源代码目录下:

点击(此处)折叠或打开

  1. make all    //编译源代码,若要在kcahce里看到源代码,则在g++或gcc编译时添加选项“-g
  2. valgrind --tool=callgrind ./test    // 在valgrind环境下运行源代码,生成callgrind.out.xxx
  3. kcachegrind  //进入图像化界面

6. 代码对比工具meld

点击(此处)折叠或打开

  1. apt-get install meld

7. 优化方法记录
在优化过程中,C语言的优化比较常用的有效方法是:多线程NEON(SIMDGPU
最近发现一种:对双重循环内层循环中的函数调用进行展开
原来8核处理器中执行时间为119ms,对某一双重循环的内层循环中的一个函数调用,直接展开,耗时降至87ms!!!

8. 优化方法记录
a. 用查表法实现内层循环:对于输入数据比较少,数据位宽并不宽的多层循环,可以试着将内层循环,用查找表全部替代。
b. 减少内存拷贝:一些芯片内存拷贝耗时较大,对于一些频繁的内存拷贝、临时变量,应该尝试通过修改代码去掉拷贝,直接使用原始数据。

9. 优化方法记录
循环展开,等价运算替换,冗余计算提取,合并运算,除法改乘法等。

10. 使用arm-linux-objdump反汇编
在最内层循环内嵌标示指令,比如连续5个nop,使用反汇编工具查看最内层循环标示指令之间的指令数,从而确定是否需要手写汇编。

11. 优化方法记录
在算法性能无法提升且耗时为秒级时,可以尝试将过程拆分成几个部分,重用中间结果,以达到性能最大化。
阅读(12248) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~