后续可能为需要加入一些特定的模块到android中,所以JNI还需继续熟悉起来大笑大笑大笑

本文转自:http://www.cnblogs.com/MarsGG/articles/2057433.html

首先说明,Android系统不允许一个纯粹使用C/C++的程序出现,它要求必须是通过Java代码嵌入Native C/C++——即通过JNI的方式来使用本地(Native)代码。因此JNI对Android底层开发人员非常重要。


如何将.so文件打包到.APK

让我们从最简单的情况开始,假如已有一个JNI实现——libxxx.so文件,那么如何在APK中使用它呢?

在我最初写类似程序的时候,我会将libxxx.so文件push到/system/lib/目录下,然后在Java代码中执行System.loadLibrary(xxx),这是个可行的做法,但需要取得/system/lib目录的写权限(模拟器通过adb remount取得该权限)。但模拟器重启之后libxxx.so文件会消失。现在我找到了更好的方法,把.so文件打包到apk中分发给最终用户,不管是模拟器或者真机,都不再需要system分区的写权限。实现步骤如下:

1、在你的项目根目录下建立libs/armeabi目录;

2、将libxxx.so文件copy到libs/armeabi/下;

3、此时ADT插件自动编译输出的.apk文件中已经包括.so文件了;

4、安装APK文件,即可直接使用JNI中的方法;

我想还需要简单说明一下libxxx.so的命名规则,沿袭Linux传统,lib<something>.so是类库文件名称的格式,但在Java的System.loadLibrary("something")方法中指定库名称时,不能包括前缀——lib,以及后缀——.so。

准备编写自己的JNI模块

你一定想知道如何编写自己的xxx.so,不过这涉及了太多有关JNI的知识。简单的说:JNI是Java平台定义的用于和宿主平台上的本地代码进行交互的“Java标准”,它通常有两个使用场景:1.使用(之前使用c/c++、delphi开发的)遗留代码;2.为了更好、更直接地与硬件交互并获得更高性能。你可以通过以下链接了解JNI的更多资料:

  • Java Native Interface Developer Guides
  • Java Native Interface Specification
  • Java本地接口(JNI)基本功能
  • Book:JNI Programmer's Guide and Specification

JNI之Hello World

1、首先创建含有native方法的Java类:

Java代码
  1. packagecom.okwap.testjni;
  2. publicfinalclassMyJNI{
  3. //native方法,
  4. publicstaticnativeStringsayHello(Stringname);
  5. }

2、通过javah命令生成.h文件,内容如下(com_okwap_testjni.h文件):

C代码
  1. /*DONOTEDITTHISFILE-itismachinegenerated*/
  2. #include<jni.h>
  3. /*Headerforclasscom_okwap_testjni_MyJNI*/
  4. #ifndef_Included_com_okwap_testjni_MyJNI
  5. #define_Included_com_okwap_testjni_MyJNI
  6. #ifdef__cplusplus
  7. extern"C"{
  8. #endif
  9. /*
  10. *Class:com_okwap_testjni_MyJNI
  11. *Method:sayHello
  12. *Signature:(Ljava/lang/String;)Ljava/lang/String;
  13. */
  14. JNIEXPORTjstringJNICALLJava_com_okwap_testjni_MyJNI_sayHello
  15. (JNIEnv*,jclass,jstring);
  16. #ifdef__cplusplus
  17. }
  18. #endif
  19. #endif

这是一个标准的C语言头文件,其中的JNIEXPORT、JNICALL是JNI关键字(事实上它是没有任何内容的宏,仅用于指示性说明),而jint、jstring是JNI环境下对int及java.lang.String类型的映射。这些关键字的定义都可以在jni.h中看到。

3、在 com_okwap_testjni.c文件中实现以上方法:

Java代码
  1. #include<string.h>
  2. #include<jni.h>
  3. #include"com_okwap_testjni.h"
  4. JNIEXPORTjstringJNICALLJava_com_okwap_testjni_MyJNI_sayHello(JNIEnv*env,jclass,jstringstr){
  5. //从jstring类型取得c语言环境下的char*类型
  6. constchar*name=(*env)->GetStringUTFChars(env,str,0);
  7. //本地常量字符串
  8. char*hello="你好,";
  9. //动态分配目标字符串空间
  10. char*result=malloc((strlen(name)+strlen(hello)+1)*sizeof(char));
  11. memset(result,0,sizeof(result));
  12. //字符串链接
  13. strcat(result,hello);
  14. strcat(result,name);
  15. //释放jni分配的内存
  16. (*env)->ReleaseStringUTFChars(env,str,name);
  17. //生成返回值对象
  18. str=(*env)->NewStringUTF(env,"你好JNI~!");
  19. //释放动态分配的内存
  20. free(result);
  21. //
  22. returnstr;
  23. }

4、编译——两种不同的编译环境

以上的C语言代码要编译成最终.so动态库文件,有两种途径:

  • Android NDK:全称是Native Developer Kit,是用于编译本地JNI源码的工具,为开发人员将本地方法整合到Android应用中提供了方便。事实上NDK和完整源码编译环境一样,都使用Android的编译系统——即通过Android.mk文件控制编译。NDK可以运行在Linux、Mac、Window(+cygwin)三个平台上。有关NDK的使用方法及更多细节请参考以下资料:
    • eoe特刊第七期《NDK总结》http://blog.eoemobile.com/?p=27
    • http://androidappdocs.appspot.com/sdk/ndk/index.html;
  • 完整源码编译环境:Android平台提供有基于make的编译系统,为App编写正确的Android.mk文件就可使用该编译系统。该环境需要通过git从官方网站获取完整源码副本并成功编译,更多细节请参考:http://source.android.com/index.html

不管你选择以上两种方法的哪一个,都必须编写自己的Android.mk文件,有关该文件的编写请参考相关文档。

JNI组件的入口函数——JNI_OnLoad()、JNI_OnUnload()

JNI组件被成功加载和卸载时,会进行函数回调,当VM执行到System.loadLibrary(xxx)函数时,首先会去执行JNI组件中的JNI_OnLoad()函数,而当VM释放该组件时会呼叫JNI_OnUnload()函数。先看示例代码:

C代码
  1. //onLoad方法,在System.loadLibrary()执行时被调用
  2. jintJNI_OnLoad(JavaVM*vm,void*reserved){
  3. LOGI("JNI_OnLoadstartup~~!");
  4. returnJNI_VERSION_1_4;
  5. }
  6. //onUnLoad方法,在JNI组件被释放时调用
  7. voidJNI_OnUnload(JavaVM*vm,void*reserved){
  8. LOGE("callJNI_OnUnload~~!!");
  9. }

JNI_OnLoad()有两个重要的作用:

  • 指定JNI版本:告诉VM该组件使用那一个JNI版本(若未提供JNI_OnLoad()函数,VM会默认该使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,则必须由JNI_OnLoad()函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中) 来告知VM。
  • 初始化设定,当VM执行到System.loadLibrary()函数时,会立即先呼叫JNI_OnLoad()方法,因此在该方法中进行各种资源的初始化操作最为恰当。

JNI_OnUnload()的作用与JNI_OnLoad()对应,当VM释放JNI组件时会呼叫它,因此在该方法中进行善后清理,资源释放的动作最为合适。

使用registerNativeMethods方法

对Java程序员来说,可能我们总是会遵循:1.编写带有native方法的Java类;--->2.使用javah命令生成.h头文件;--->3.编写代码实现头文件中的方法,这样的“官方” 流程,但也许有人无法忍受那“丑陋”的方法名称,RegisterNatives方法能帮助你把c/c++中的方法隐射到Java中的native方法,而无需遵循特定的方法命名格式。来看一段示例代码吧:

C代码
  1. //定义目标类名称
  2. staticconstchar*className="com/okwap/testjni/MyJNI";
  3. //定义方法隐射关系
  4. staticJNINativeMethodmethods[]={
  5. {"sayHello","(Ljava/lang/String;)Ljava/lang/String;",(void*)sayHello},
  6. };
  7. jintJNI_OnLoad(JavaVM*vm,void*reserved){
  8. //声明变量
  9. jintresult=JNI_ERR;
  10. JNIEnv*env=NULL;
  11. jclassclazz;
  12. intmethodsLenght;
  13. //获取JNI环境对象
  14. if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_4)!=JNI_OK){
  15. LOGE("ERROR:GetEnvfailed\n");
  16. returnJNI_ERR;
  17. }
  18. assert(env!=NULL);
  19. //注册本地方法.Load目标类
  20. clazz=(*env)->FindClass(env,className);
  21. if(clazz==NULL){
  22. LOGE("Nativeregistrationunabletofindclass'%s'",className);
  23. returnJNI_ERR;
  24. }
  25. //建立方法隐射关系
  26. //取得方法长度
  27. methodsLenght=sizeof(methods)/sizeof(methods[0]);
  28. if((*env)->RegisterNatives(env,clazz,methods,methodsLenght)<0){
  29. LOGE("RegisterNativesfailedfor'%s'",className);
  30. returnJNI_ERR;
  31. }
  32. //
  33. result=JNI_VERSION_1_4;
  34. returnresult;
  35. }

建立c/c++方法和Java方法之间映射关系的关键是 JNINativeMethod 结构,该结构定义在jni.h中,具体定义如下:

C代码
  1. typedefstruct{
  2. constchar*name;//java方法名称
  3. constchar*signature;//java方法签名
  4. void*fnPtr;//c/c++的函数指针
  5. }JNINativeMethod;

参照上文示例中初始化该结构的代码:

C代码
  1. //定义方法隐射关系
  2. staticJNINativeMethodmethods[]={
  3. {"sayHello","(Ljava/lang/String;)Ljava/lang/String;",(void*)sayHello},
  4. };

其中比较难以理解的是第二个参数——signature字段的取值,实际上这些字符与函数的参数类型/返回类型一一对应,其中"()" 中的字符表示参数,后面的则代表返回值。例如"()V" 就表示void func(),"(II)V" 表示 void func(int, int),具体的每一个字符的对应关系如下:

字符 Java类型 C/C++类型
V void void
Z jboolean boolean
I jint int
J jlong long
D jdouble double
F jfloat float
B jbyte byte
C jchar char
S jshort short

数组则以"["开始,用两个字符表示:

字符 java类型 c/c++类型
[Z jbooleanArray boolean[]
[I jintArray int[]
[F jfloatArray float[]
[B jbyteArray byte[]
[C jcharArray char[]
[S jshortArray short[]
[D jdoubleArray double[]
[J jlongArray long[]

上面的都是基本类型,如果参数是Java类,则以"L"开头,以";"结尾,中间是用"/"隔开包及类名,而其对应的C函数的参数则为jobject,一个例外是String类,它对应C类型jstring,例如:Ljava/lang /String; 、Ljava/net/Socket; 等,如果JAVA函数位于一个嵌入类(也被称为内部类),则用$作为类名间的分隔符,例如:"Landroid/os/FileUtils$FileStatus;"。

使用registerNativeMethods方法不仅仅是为了改变那丑陋的长方法名,最重要的是可以提高效率,因为当Java类别透过VM呼叫到本地函数时,通常是依靠VM去动态寻找.so中的本地函数(因此它们才需要特定规则的命名格式),如果某方法需要连续呼叫很多次,则每次都要寻找一遍,所以使用RegisterNatives将本地函数向VM进行登记,可以让其更有效率的找到函数。

registerNativeMethods方法的另一个重要用途是,运行时动态调整本地函数与Java函数值之间的映射关系,只需要多次调用registerNativeMethods()方法,并传入不同的映射表参数即可。

JNI中的日志输出

你一定非常熟悉在Java代码中使用Log.x(TAG,“message”)系列方法,在c/c++代码中也一样,不过首先你要include相关头文件。遗憾的是你使用不同的编译环境(请参考上文中两种编译环境的介绍),对应的头文件略有不同。。

如果是在完整源码编译环境下,只要include <utils/Log.h>头文件,就可以使用对应的LOGI、LOGD等方法了,同时请定义LOG_TAG,LOG_NDEBUG等宏值,示例代码如下:

C代码
  1. #defineLOG_TAG"HelloJni"
  2. #defineLOG_NDEBUG0
  3. #defineLOG_NIDEBUG0
  4. #defineLOG_NDDEBUG0
  5. #include<string.h>
  6. #include<jni.h>
  7. #include<utils/Log.h>
  8. jstringJava_com_inc_android_ime_HelloJni_stringFromJNI(JNIEnv*env,jobjectthiz){
  9. LOGI("CallstringFromJNI!\n");
  10. return(*env)->NewStringUTF(env,"HellofromJNI(中文)!");
  11. }

与日志相关的.h头文件,在以下源码路径:

  • myeclair\frameworks\base\include\utils\Log.h
  • myeclair\system\core\include\cutils\log.h

如果你是在NDK环境下编译,则需要#include <android/log.h>,示例代码如下:

C代码
  1. #defineLOG_TAG"HelloJni"
  2. #include<string.h>
  3. #include<jni.h>
  4. #include<utils/Log.h>
  5. jstringJava_com_inc_android_ime_HelloJni_stringFromJNI(JNIEnv*env,jobjectthiz){
  6. __android_log_print(ANDROID_LOG_INFO,LOG_TAG,"CallstringFromJNI!\n");
  7. return(*env)->NewStringUTF(env,"HellofromJNI(中文)!");
  8. }

很可惜,其中用于日志输出的方法是:__android_log_print(....)并不是我们熟悉的LOG.x(...)系列方法。不过好的一点是android/log.h文件在完整源码环境下也是可用的,因此,可以用一下的头文件来统两种环境下的差异:

C代码
  1. /*
  2. *jnilogger.h
  3. *
  4. *Createdon:2010-11-15
  5. *Author:INC062805
  6. */
  7. #ifndef__JNILOGGER_H_
  8. #define__JNILOGGER_H_
  9. #include<android/log.h>
  10. #ifdef_cplusplus
  11. extern"C"{
  12. #endif
  13. #ifndefLOG_TAG
  14. #defineLOG_TAG"MY_LOG_TAG"
  15. #endif
  16. #defineLOGD(...)__android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
  17. #defineLOGI(...)__android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)
  18. #defineLOGW(...)__android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
  19. #defineLOGE(...)__android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
  20. #defineLOGF(...)__android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)
  21. #ifdef__cplusplus
  22. }
  23. #endif
  24. #endif/*__JNILOGGER_H_*/

你可以下载以上头文件,来统一两种不同环境下的使用差异。另外,不要忘了在你的Android.mk文件中加入对类库的应用,两种环境下分别是:

Java代码
  1. ifeq($(HOST_OS),windows)
  2. #NDK环境下
  3. LOCAL_LDLIBS:=-llog
  4. else
  5. #完整源码环境下
  6. LOCAL_SHARED_LIBRARIES:=libutils
  7. endif

。未完待续》。。。。。。。。。。。。。。

Android为JNI提供的助手方法

myeclair\dalvik\libnativehelper\include\nativehelper

在完整源码编译环境下,Android在myeclair\dalvik\libnativehelper\include\nativehelper\JNIHelp.h头文件中 提供了助手函数 ,用于本地方法注册、异常处理等任务,还有一个用于计算方法隐射表长度的宏定义:

C代码
  1. #ifndefNELEM
  2. #defineNELEM(x)((int)(sizeof(x)/sizeof((x)[0])))
  3. #endif
  4. //有了以上宏定义后,注册方法可以按如下写,该宏定义可以直接copy到NDK环境下使用:
  5. (*env)->RegisterNatives(env,clazz,methods,NELEM(methods));

更多相关文章

  1. Android代码混淆指南
  2. 《第一行代码--Android》读书笔记之多线程与服务
  3. 升级代码的大概设计
  4. 下载Android Sdk源码方法
  5. OpenCore 的代码结构
  6. 图解Android源代码下载指南
  7. android 源代码获取
  8. Android 自定义属性时TypedArray的使用方法

随机推荐

  1. Android(安卓)Studio TV开发教程(一)处理电
  2. Android获取CPU使用率的几种方式
  3. Android中实用小知识
  4. Android占位符,Java占位符
  5. Android(安卓)TextView 单行文本的坑
  6. Android(安卓)Contacts(二)—— SMS 短信
  7. android启动--深入理解zygote
  8. android 夜间模式
  9. Android(安卓)API 人脸检测(Face Detect)
  10. Android(安卓)and Linux suspend and res