当前位置: 代码迷 >> Android >> Android JNI调用(2)
  详细解决方案

Android JNI调用(2)

热度:24   发布时间:2016-05-01 18:12:36.0
Android JNI调用(二)
1.jni的基本工作原理  
       (1)java的本质

  想搞明白jni的本质,还要从java的本质说起.从本质上来说,java这门语言就是一门脚本语言(这是偶的个人理解,希望java大侠们不要用板砖拍我),它的运行完全依赖于脚本引擎对java的代码进行解释和执行(当然了,现代的java已经先进许多,可以从源代码编译成.class之类的中间格式的二进制文件,这种处理会大大地加快java脚本的运行速度,但是基本的执行方式仍然不变,由脚本引擎(我们称之为JVM)来执行,与python、perl之类的纯脚本相比,它只是把脚本变成了二进制格式而已.另外就是java本身对面向对象的概念支持得很好,拥有完善的功能库可供调用,把这个脚本引擎移植到所有平台上,那么这个脚本自然就实现所谓的“跨平台”了).绝大多数的脚本引擎都支持一个很显著的特性,就是可以通过c/c++编写模块,在脚本中调用这些模块,以此来类比java,也是一样的,java一定要提供一种在脚本中调用c/c++编写的模块的机制,才能称得上是一个相对完善的脚本引擎.

  (2)android中的java

  android平台从本质上是 由arm-linux操作系统 和一个叫做dalvik的java虚拟机组成的.所有在android模拟器上面看到的那些华丽的界面,都是用java语言编写的(参见android平台源代码的frameworks/base目录).目前看来dalvik只是提供了一个标准的支持jni调用的java虚拟机环境.android平台中所有的硬件相关的操作均是采用jni技术进行了封装,由java去调用jni模块,而jni模块使用c/c++调用android本身的arm-linux底层驱动.

  例如,frameworks/base/libs/ui目录下面有一个叫做“EGLDisplaySurface.cpp”的文件,里面的:

  status_t EGLDisplaySurface::mapFrameBuffer()函数中,就有直接对android的arm-linux中的framebuffer的初始化代码.

  这也更加印证了,android其实是依靠java+jni建立起来的王国.hoho,如此一来,就凸显出jni在android开发中的重要性(当然,一些简单的小程序是完全可以只用java就搞定的).

  “jni”的子目录,这个目录将用来存放.c的文件.

  (3)编写jni模块的java调用类

  这是必然的了,jni嘛,一定要有调用者才能够工作在src的最内层目录里面添加一个叫做JniModule.java的原文件,看上去如下所示:

java代码:
  1. public class JniModule {

  2. static {
  3. System.loadLibrary("aaaa") ; 
  4. }
  5. public native static int jni_add(int a, int b) ; 
  6. }
复制代码
注意,偶们最终会生成一个叫做libaaaa.so的arm兼容的二进制动态库,但是在使用System.loadLibrary动态载入的时候,只需要填写lib和.so之间的名字aaaa即可,在此实验的功能仅仅是两个数字a和b的求和计算以及如何在jni的c语言模块中把log日志打印到logcat中.

        在JniTest.java中,偶们可以如下调用这个类:

java代码:
  1. public void onClick(View v) {

  2. String ss ; 
  3. int a = 3 ; 
  4. int b = 4 ; 

  5. ss = "" ; 
  6. switch(v.getId()) {
  7. case R.id.button1:
  8. ss = "a="+String.valueOf(a)+","+"b=" + String.valueOf(b) + "," + "a+b=" + 
  9. String.valueOf(JniModule.jni_add(a, b)); 

  10. setTitle(ss) ;

  11. break ; 
  12. case R.id.button2:
  13. setTitle("button2 click") ; 
  14. break ; 
  15. case R.id.button3:
  16. int pid = android.os.Process.myPid(); 
  17. android.os.Process.killProcess(pid);
  18. break ; 


  19. }
复制代码
注意,这里的button3是很重要的,功能是得到当前程序的进程id,然后显示地杀掉它!

       为什么要这么做呢?原因在于,android里面的常规退出函数并没有真正地关闭当前运行的进程,而是切换到后台去了。这对普通的java应用看上去很平常,而且可以加速再次启动该程序的速度,但是对于带有jni模块的java程序而言就是恶梦,因为程序没有真的关闭。所以那个libaaaa.so库,会一直停留在内存中,这时候如果你希望把旧的so库替换成新的库,那就要重启手机才行。。。很痛苦,所以想到了这种办法,直接杀掉自己,那么下一次启动的时候就会自动重新载入最新的so库。


生成java程序与c程序的接口文件

  谈到这里,自然就会联想到是c语言的。h文件了,现在的问题在于如何从。java文件生成我们需要的。h格式的c/c++文件。答案就是javah这个小工具基本上所有的jdk都会提供:

  javah -classpath "java类的地址" <你的java模块位置>

  利用javah就可以很容易地将JniModule。java代码的native标记的部分转换为c/c++的。h文件中定义的导出函数。

  以下是偶用于测试的makefile,相信懂makefile语法的朋友可以很容易就看明白偶在做什么,

  为了实验能够非常“精确”地进行,在这个makefile中的全部路径都采用了绝对路径,其实用相对路径也是可以的。

java代码:

  1. CC=arm-none-linux-gnueabi-gcc

  2. LD=arm-none-linux-gnueabi-ld

  3. MV=mv

  4. JH=javah

  5. JHFLAGS=-classpath "/home/wayne/works/workspace/JniTest/bin"

  6. LDFLAGS=-T "/home/wayne/CodeSourcery/Sourcery_G++_Lite/arm-none-linux-gnueabi/lib/ldscripts/armelf_linux_eabi.xsc" -shared

  7. CFLAGS=-I. -I/home/wayne/works/workspace/JniTest/jni/include -I/home/wayne/works/workspace/JniTest/jni/include/linux -I/home/wayne/works/workspace/JniTest/jni -fpic 

  8. all: libaaaa.so
  9. com_hurraytimes_jnitest_JniModule.h: 

  10. $(JH) $(JHFLAGS) com.hurraytimes.jnitest.JniModule
  11. aaaa.o: aaaa.c com_hurraytimes_jnitest_JniModule.h
  12. $(CC) $(CFLAGS) -c -o aaaa.o aaaa.c

  13. libaaaa.so: aaaa.o
  14. $(LD) $(LDFLAGS) -o libaaaa.so aaaa.o libcutils.a
  15. $(RM) ../libs/armeabi/libaaaa.so
  16. $(MV) libaaaa.so ../libs/armeabi/
  17. clean:

  18. $(RM) *.o *.so *~
复制代码
这里需要特别提一点的,就是关于arm-none-linux-gnueabi-gcc的使用问题,这个编译器自从到了2008版本就开始琢磨着实现更加方便地“cross compiler”的功能了.以往的版本是arm-xxx-linux-gcc,就是为了编译arm-linux平台的软件的,如果你的芯片从三星的变为菲利普的,那么整条工具链就要重新编译.现在的这个2008版的为了让广大开发者(尤其是多种不同芯片平台的嵌入式开发者)的计算机里面不要安装好多套for不同芯片组的gcc工具链,弄了一个-T的参数,这里就可以让开发者使用一个gcc工具链生成不同平台和格式的可执行代码以及链接的库.虽然如此,但是偶还是觉得不大习惯,总之谢谢CodeSourcery很贴心的功能,让偶花了半个多小时在琢磨和查资料,到底是什么原因导致生成的jni模块无法在android上工作.

  jni模块的打包问题
  再次声明,在android 1.5 cupcake以后的版本才可以用偶下文提到的打包方法.
  在查看了ndk的脚本以后,我才知道原来android 1.5版本在打包apk的时候,是完全可以支持直接将.so的jni库打包到apk安装包中去的,解决了偶们这种铁杆c/c++开发者开发自己的jni组件的发布问题,java脚本嘛,做个事件啥的中转就完成它的使命了.
  其实具体操作起来非常简单,在当前项目的跟目录下创建如下目录:
  /libs/armeabi
  然后把自己生成好的so库拷贝到这个armeabi目录下面即可,运行ant生成apk发布包的时候,就会自动地将/libs/armeabi目录下的so库打包到apk文件中,然后就是直接安装就好了!非常简单方便.

  关于ant里面实现jni的makefile调用的方法
  首先肯定一下,ant是个不错的东西.但是如果说它要取代makefile的地位,偶个人固执地认为很难.makefile语法简单,随手就可以敲一个,但回头看看ant的build.xml,第一眼看上去就头晕.
  xml很不错,但是就是他大爷的乱七八糟,而且居然宣称说是给人看的东西...凡事真正有些实质性的用处,用xml存储的数据(用于演示hello world之类的xml就免了),让人看起来都会头晕.
  ant采用xml作为基本输入,偶个人认为还不如仿效makefile弄一套相对简单的语法来得方便.
  好了不再发牢骚了,开始看一下,如何为android的build.xml添加ant支持的xml实现自动调用jni的makefile文件.
  以下是偶用ant来编译jni模块的xml,稍加修改就可以用于开发和实验中,把这些加到</project>之前就可以了:

java代码:
  1. <target name="mk" >

  2. <exec dir="./jni" executable="make" os="Linux" failonerror="true">
  3. </exec>
  4. </target> 
  5. <target name="mkclr" >
  6. <exec dir="./jni" executable="make" os="Linux" failonerror="true">
  7. <arg line=" clean" />
  8. </exec>
  9. </target>
复制代码
使用方法就是ant mk和ant mkclr一个是相当于调用make,另一个是相当于调用make clean。其余的操作都放到makefile里面去了。


最后需要说的就是,在偶传上来的代码中,可能会发现有一个叫做libcutils.a的编译好的静态库,这个东西就“说来话长”了,主要原因是偶在做实验的时候,还没有ndk发布出来,android手机里面也没个gdbserver之类的工具,调试起来十分痛苦。偶认为再怎么弱,也要输出点东西到logcat吧?!因此,从android-platform的平台源代码中提取了cutils的头文件,直接把android平台编译出来的二进制.a文件拷贝出来,链接到偶自己的“土法”生成的so库里面,这样就可以调用libcutils.a中定义的log函数,就可以直接通过联机的logcat查看jni中的log日志输出,很爽!ndk的文档中承诺,在未来的android ndk开发包中会提供在线调试的功能。

    到此为止,“土法”编译和编写jni的方法已经基本记录和讲解完毕。一定能够对ndk的本质有了新的认识。而不是那里面readme和howto文档中的几行字,修改android。mk之类云云。。。
  当然有了上面的这些底层编译的探索,加上ndk里面提供的。h和若干运行时库,甚至android平台源代码里面编译出来的静态二进制包,jni几乎可以实现任何功能。
  还是那句话,“潘多拉”的盒子一旦打开,能否控制得住,就不是google这样的公司能够左右的了。
  等有时间再来写写关于使用google的ndk来编写和调试jni模块的方法。。。
  其实主要是关于ndk的一些编译选项的研究和翻译(其实人家google的文档已经说的很清楚了)。偶选用的测试环境是slackware 12。0 + android 1。5 r1 for linux + jdk 1。6。0_12,ndk选用的是android 1。5 ndk r1这个版本的(直接解压就行,免安装的)。
  1、从ndk安装说起
  ndk安装的时候需要运行一个~/android-ndk-1。5_r1/build/目录下面的一个叫做host-setup。sh的脚本。大略读了一下这个脚本,发现这个主要是用来生成out/host/host/config。mk文件的。主要用于指定用户操作系统的判断以及支持的编译器类型(设置makefile中的cc,ar,ld之类的变量)
  ndk的目录介绍。
  2、ndk的目录结构分析
  进入android-ndk-1。5_r1目录,看到如下目录结构:
  GNUmakefile: 标准的makefile格式的文件,用于引用build/core/main。mk的编译脚本。
  README。TXT:基本的说明,没啥大用,真正有用的文档都在docs目录下面。
  apps/:存放带有jni接口的android工程目录(工程里面有利用native关键字定义的java函数)
  build/:存放着几乎所有的ndk编译相关的脚本以及必要的静态链接库。
  docs/:存放这ndk的所有“官方”文档,每一篇文档对于jni编写者来说这里面的任何一点点资料都是无价的。
  out/:存放一些中间的临时文件,例如jni的。c/。cpp文件编译过程中产生的。o文件等。
  sources/:存放jni文件的。c/。cpp的源代码文件。
  3、基本的使用方法
  (1)创建一个android工程
  进入apps目录,运行如下命令:
  android create project --target 2 --package com。TWM --activity NDKTest --path 。/NDKTest/project
  通过命令行创建一个叫做NDKTest的activity,注意这里的--path需要设置为。/XXXXX/project这个目录,这个XXXXX目录主要是为了ndk的make区分不同项目和工程使用的。编写Application。mk文件的时候,一定要把Application。mk写到这个XXXXX目录下面。
  $NDK/apps/<myapp>/Application。mk
  另外,编译jni库的时候使用的命令也是如此:
  make APP=<your app name>
  这里的<your app name>实际上也是这个XXXXX目录。
  (2)为工程添加一个jni的java调用接口
  进入app/NDKTest/project/src/com/TWM/NdkTest目录,建立一个新的java文件(例如:NDKJni。java),然后把代码写成类似下面这个样子:

java代码:

  1. package com.TWM.NdkTest ; 
  2. public class NDKJni {
  3. public native int MyFunc(int a, int b) ;
  4. static {
  5. System.loadLibrary("NDKjni") ; 
  6. }
  7. }
复制代码
这里的MyFunc由于是使用native修饰,因此,这个MyFunc函数是一个调用jni的函数。
  (3)为java工程编写Application。mk文件
  该文件主要放在app/NDKTest目录下,用于告知ndk的编译脚本,当前的程序需要哪个jni模块。
  看上去应该是这个样子的:
  APP_PROJECT_PATH := $(call my-dir)/project   ---> 当前目录下的project目录包含了jni模块的java接口
  APP_MODULES      := NDKTest                               --->当前模块的名字叫做NDKTest
  (4)弄清楚java程序的包层次
  以当前的这个project为例,就是上面代码中的package com。TWM。NdkTest,定义的类名为NDKJni。因此,根据这个包的层次,可以根据jni文件的函数命名规则定义函数:
  JNIEXPORT jint JNICALL Java_com_TWM_NdkTest_NDKJni_MyFunc(JNIEnv * env, jobject thiz, jint a, jint b) ;
  当然,手工根据包层次定义jni函数还是很痛苦的,可以借助于javah工具:
  mkdir -p apps/NDKTest/project/jni
  cd apps/NDKTest/project/jni
  javah -classpath "/bin/classes" com。TWM。NdkTest。NDKJni
  然后就会自动生成一个叫做com_TWM_NdkTest_NDKJni。h的文件,里面的内容基本上跟手工生成的差不多:

java代码:
  1. /* DO NOT EDIT THIS FILE - it is machine generated */

  2. #include <jni.h>

  3. /* Header for class com_TWM_NdkTest_NDKJni */
  4. #ifndef _Included_com_TWM_NdkTest_NDKJni
  5. #define _Included_com_TWM_NdkTest_NDKJni
  6. #ifdef __cplusplus
  7. extern "C" {

  8. #endif

  9. /*
  10. * Class: com_TWM_NdkTest_NDKJni
  11. * Method: MyFunc
  12. * Signature: (II)I
  13. */
  14. JNIEXPORT jint JNICALL Java_com_TWM_NdkTest_NDKJni_MyFunc
  15. (JNIEnv *, jobject, jint, jint);

  16. #ifdef __cplusplus
  17. }
  18. #endif
复制代码



  相关解决方案