android平台中编写jni模块的方法(2)

继续上一篇,目前android平台的sdk已经发布到了cupcake 1.5的版本(最新的开发版可能要比这个版本更高,期待android 2.0 :D)

对 于android 1.5版本的开发者而言,一个非常大的好消息是cupcake已经开始“官方”支持开发者编写自己的jni库了,这主要表现在google放出了一个叫做 android-ndk的开发包,这个开发包是专门为了开发jni而准备的一些必要的头文件以及一些运行时所需的库,为android应用开发者提供了比 较方便的脚本支持。这种方便和快捷是在android 1.0和1.1的sdk以及配套的工具里面是没有的。试用了ndk以后感觉良好,偶对于之前没有使用ndk,纯粹采用第三方编译工具进行jni开发尝试的 “土法”进行一次归纳和整理,并且发布一个偶做测试用的jni代码。

从ndk的声明里面偶可以看到,google在使用jni技术进行开发的时候提出的几点需要主意的地方:
The NDK is *not* a good way to write generic native code that runs on Android
devices. In particular, your applications should still be written in the Java
programming language, handle Android system events appropriately to avoid the
"Application Not Responding" dialog or deal with the Android application
life-cycle.

Note however that is is possible to write a sophisticated application in
native code with a small "application wrapper" used to start/stop it
appropriately.

A good understanding of JNI is highly recommended, since many operations
in this environment require specific actions from the developers, that are
not necessarily common in typical native code. These include:

  - not being able to directly access the content of VM objects through
    direct native pointers. E.g. you cannot safely get a pointer to a
    String object's 16-bit char array to iterate over it in a loop.

  - requiring explicit reference management when the native code wants to
    keep handles to VM objects between JNI calls.


The NDK only provides system headers for a very limited set of native
APIs and libraries supported by the Android platform. While a typical
Android system image includes many native shared libraries, these should
be considered an implementation detail that might change drastically between
updates and releases of the platform.

If an Android system library is not explicitely supported by the NDK
headers, then applications should not depend on it being available, or
they risk breaking after the next over-the-air system update on various
devices.

Selected system libraries will gradually be added to the set of stable NDK
APIs.

从 上述观点可以看出,google对于dalvik本身的稳定性以及android的framework的信心还是很足的,所以不建议开发者使用jni技术 (偶个人认为,他们是在担心开发者的水平高低不同,会影响到他们系统的稳定性),同时也告诫开发者即使使用了jni技术也未必会大幅度提升程序的运行效率 (对于这一点偶表示怀疑,java终究是java,在vm上面晃荡的脚本的运行效率怎么可能跟优化过的native程序相比呢?当然对于这个问题是仁者见 仁,智者见智了。)

总之,虽然google提供了ndk可以较为方便地构建android平台的jni模块,但是作为对于凡事喜欢刨根问底的偶来说,光知道运行几个脚本跟不懂没有任何区别,偶希望的是从原理上去了解android的jni技术。

于是,开始了这次“土法”建jni的过程:
(1)工具链的准备工作
第一步,当然是jdk以及android的sdk这些就不再赘述了。
第二不,登陆http://www.codesourcery.com的download页面,下载arm-2008q3-72-arm-none-linux-gnueabi.bin这个编译工具(偶是linux环境,不同的os可能稍微有些区别)
然后就是安装,设置环境变量,偶把自己的环境变量设定贴出来备份一下:
# for midnight commander default editor, i hate vi or vim (sorry for vi-ers)
export EDITOR=emacs
export VIEWER=emacs

# for splint
export LARCH_PATH=/usr/local/share/splint/lib
export LCLIMPORTDIR=/usr/local/share/splint/imports
export PATH=/usr/local/bin/splint:${PATH}

# for android jni developments
export PATH=/home/wayne/CodeSourcery/Sourcery_G++_Lite/bin:${PATH}

# for android sdk
export PATH=/home/wayne/android-sdk-linux_x86-1.5_r1/tools:${PATH}
export PATH=/home/wayne/jdk1.6.0_12/bin:${PATH}
export JAVA_HOME=/home/wayne/jdk1.6.0_12
export ANDROID_JAVA_HOME=${JAVA_HOME}
export ANDROID_HOME=/home/wayne/android-sdk-linux_x86-1.5_r1

# for symbian s60 v3 dev
export PATH=/home/wayne/epoc_sdk/s60_31_fp1/tools_wrapper:$PATH
export EPOCROOT=/home/wayne/epoc_sdk/s60_31_fp1/

# for android platform codes
export ANDROID_PRODUCT_OUT=/home/wayne/works/android_source/out/target/product/generic

# for android ndk
export ANDROID_NDK_ROOT=/home/wayne/android-ndk-1.5_r1/

# for apache ant
export PATH=/usr/local/apache-ant-1.7.1/bin:${PATH}
export ANT_HOME=/usr/local/apache-ant-1.7.1

# for splint
export LARCH_PATH=/usr/local/share/splint/lib
export LCLIMPORTDIR=/usr/local/share/splint/import

# for git
export PATH=/usr/local/bin:${PATH}

(2)建立JniTest的项目
具体的命令行建立过程,请参考第一篇文章的方法。
在JniTest的目录下建立一个叫做“jni”的子目录,这个目录将用来存放.c的文件。

(3)编写jni模块的java调用类
这是必然的了,jni嘛,一定要有调用者才能够工作在src的最内层目录里面添加一个叫做JniModule.java的原文件,看上去如下所示:
public class JniModule {
    static {
        System.loadLibrary("aaaa") ;
    }
    public native static int jni_add(int a, int b) ;
}
注 意,偶们最终会生成一个叫做libaaaa.so的arm兼容的二进制动态库,但是在使用System.loadLibrary动态载入的时候,只需要填 写lib和.so之间的名字aaaa即可,在此实验的功能仅仅是两个数字a和b的求和计算以及如何在jni的c语言模块中把log日志打印到logcat 中。

在JniTest.java中,偶们可以如下调用这个类:
    public void onClick(View v) {
        String ss ;
        int a = 3 ;
        int b = 4 ;
        
        ss = "" ;
        switch(v.getId()) {
        case R.id.button1:
            ss = "a="+String.valueOf(a)+","+"b=" + String.valueOf(b) + "," + "a+b=" +
                String.valueOf(JniModule.jni_add(a, b)); 
            setTitle(ss) ;
            break ;
        case R.id.button2:
            setTitle("button2 click") ;
            break ;
        case R.id.button3:
            int pid = android.os.Process.myPid();
            android.os.Process.killProcess(pid);
            break ;
        }   
    }
注意,这里的button3是很重要的,功能是得到当前程序的进程id,然后显示地杀掉它!
为 什么要这么做呢?原因在于,android里面的常规退出函数并没有真正地关闭当前运行的进程,而是切换到后台去了。这对普通的java应用看上去很平 常,而且可以加速再次启动该程序的速度,但是对于带有jni模块的java程序而言就是恶梦,因为程序没有真的关闭。所以那个libaaaa.so库,会 一直停留在内存中,这时候如果你希望把旧的so库替换成新的库,那就要重启手机才行。。。很痛苦,所以想到了这种办法,直接杀掉自己,那么下一次启动的时 候就会自动重新载入最新的so库。

(4)生成java程序与c程序的接口文件
谈到这里,自然就会联想到是c语言的.h文件了,现在的问题在于如何从.java文件生成我们需要的.h格式的c/c++文件。答案就是javah这个小工具基本上所有的jdk都会提供:
javah -classpath "java类的地址" <你的java模块位置>
利用javah就可以很容易地将JniModule.java代码的native标记的部分转换为c/c++的.h文件中定义的导出函数。

以下是偶用于测试的makefile,相信懂makefile语法的朋友可以很容易就看明白偶在做什么,
为了实验能够非常“精确”地进行,在这个makefile中的全部路径都采用了绝对路径,其实用相对路径也是可以的(省力多了,但在做实验的时候要求绝对正确无误。。。):
CC=arm-none-linux-gnueabi-gcc
LD=arm-none-linux-gnueabi-ld
MV=mv
JH=javah
JHFLAGS=-classpath "/home/wayne/works/workspace/JniTest/bin"
LDFLAGS=-T "/home/wayne/CodeSourcery/Sourcery_G++_Lite/arm-none-linux-gnueabi/lib/ldscripts/armelf_linux_eabi.xsc" -shared
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

all: libaaaa.so

com_hurraytimes_jnitest_JniModule.h:
    $(JH) $(JHFLAGS) com.hurraytimes.jnitest.JniModule

aaaa.o: aaaa.c com_hurraytimes_jnitest_JniModule.h
    $(CC) $(CFLAGS) -c -o aaaa.o aaaa.c

libaaaa.so: aaaa.o
    $(LD) $(LDFLAGS) -o libaaaa.so aaaa.o libcutils.a
    $(RM) ../libs/armeabi/libaaaa.so
    $(MV) libaaaa.so ../libs/armeabi/

clean:
    $(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上工作。

(5)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文件中,然后就是直接安装就好了!非常简单方便。
(6)关于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>之前就可以了:
    <target name="mk" >
    <exec dir="./jni" executable="make" os="Linux" failonerror="true">
    </exec>
    </target>

    <target name="mkclr" >
    <exec dir="./jni" executable="make" os="Linux" failonerror="true">
    <arg line=" clean" />
    </exec>
    </target>
使用方法就是ant mk和ant mkclr一个是相当于调用make,另一个是相当于调用make clean。
其余的操作都放到makefile里面去了(尽管偶的一位java朋友告诉我,makefile能做的事情ant都能做,makefile不能做的事情ant也能做,偶还是倾向于用makefile。。。除了顽固不化以外,最重要的一个原因是──懒得敲那么多东西。)。

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

到此为止,“土法”编译和编写jni的方法已经基本记录和讲解完毕。相信能够耐着性子看完偶这篇文章的朋友,一定能够对ndk的本质有了新的认识。而不是那里面readme和howto文档中的几行字,修改android.mk之类云云。。。

当然有了上面的这些底层编译的探索,加上ndk里面提供的.h和若干运行时库,甚至android平台源代码里面编译出来的静态二进制包,jni几乎可以实现任何功能。

还是那句话,“潘多拉”的盒子一旦打开,能否控制得住,就不是google这样的公司能够左右的了。
等有时间再来写写关于使用google的ndk来编写和调试jni模块的方法。。。

我珍贵的时间啊。。。眼看着生命在乱七八糟的代码中慢慢度过。。。


附件是JniTest的全部测试代码,希望能够对各位android的探索者有所帮助。

http://blog.chinaunix.net/link.php?url=http://blogimg.chinaunix.net%2Fblog%2Fupfile2%2F090709115111.gz

posted on 2012-02-13 11:46  小小博客小小员  阅读(304)  评论(0编辑  收藏  举报

导航