Android中JNI编程的那些事儿

http://www.cnblogs.com/keis/archive/2011/04/12/2013174.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类:

    1.package com.okwap.testjni;     2. public final class MyJNI {    3.    //native方法,    4.     public static native String sayHello(String name);    5.}   

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

    1./* DO NOT EDIT THIS FILE - it is machine generated */     2. #include <jni.h>      3. /* Header for class com_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. JNIEXPORT jstring JNICALL Java_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文件中实现以上方法:

    1.#include <string.h>     2. #include <jni.h>     3. #include "com_okwap_testjni.h"     4. JNIEXPORT jstring JNICALL Java_com_okwap_testjni_MyJNI_sayHello(JNIEnv* env, jclass, jstring str){     5.     //从jstring类型取得c语言环境下的char*类型     6.      const char* 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.    return str;    23. }   

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

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

    AndroidNDK :全称是Native DeveloperKit,是用于编译本地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()函数。先看示例代码:

    1.//onLoad方法,在System.loadLibrary()执行时被调用     2.jint JNI_OnLoad(JavaVM* vm, void* reserved){     3.    LOGI("JNI_OnLoad startup~~!");     4.        return JNI_VERSION_1_4;     5.}        6.     7.//onUnLoad方法,在JNI组件被释放时调用     8.void JNI_OnUnload(JavaVM* vm, void* reserved){     9.    LOGE("call JNI_OnUnload ~~!!");    10.}   

    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方法,而无需遵循特定的方法命名格式。来看一段示例代码吧:

    1.//定义目标类名称     2.static const char *className = "com/okwap/testjni/MyJNI";     3.//定义方法隐射关系    4.static JNINativeMethod methods[] = {     5.  {"sayHello", "(Ljava/lang/String;)Ljava/lang/String;", (void*)sayHello},     6.};     7.jint JNI_OnLoad(JavaVM* vm, void* reserved){    8.  //声明变量    9.  jint result = JNI_ERR;    10.  JNIEnv* env = NULL;    11.  jclass clazz;    12. int methodsLenght;    13.  //获取JNI环境对象    14.  if ((*vm)->GetEnv(vm, (void**) &env, JNI_VERSION_1_4) != JNI_OK) {    15. LOGE("ERROR: GetEnv failed\n");    16.    return JNI_ERR;    17.  }    18.  assert(env != NULL);    19.  //注册本地方法.Load 目标类    20.  clazz = (*env)->FindClass(env,className);    21.  if (clazz == NULL) {    22.    LOGE("Native registration unable to find class '%s'", className);    23.   return JNI_ERR;    24.  }    25.  //建立方法隐射关系    26.  //取得方法长度    27.  methodsLenght = sizeof(methods) / sizeof(methods[0]);    28.  if ((*env)->RegisterNatives(env,clazz, methods, methodsLenght) < 0) {    29.    LOGE("RegisterNatives failed for '%s'", className);    30.    return JNI_ERR;    31.  }    32.  //    33. result = JNI_VERSION_1_4;    34.  return result;     

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

    1.typedef struct {     2.   const char* name;//java方法名称    3.   const char* signature; //java方法签名    4.   void*       fnPtr;//c/c++的函数指针    5. } JNINativeMethod 

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

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

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

    方法签名就是一个方法参数和返回值的声明。A和B两个部分组成。规则如下(A)BA中是参数的类型,B是返回值类型。

    字符 Java类型 C/C++类型V void voidZ jboolean booleanI jint intJ jlong longD jdouble doubleF jfloat floatB jbyte byteC jchar charS 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等宏值,示例代码如下:

    /*   * jnilogger.h   *   *  Created on: 2010-11-15   *      Author: INC062805   */      #ifndef __JNILOGGER_H_   #define __JNILOGGER_H_      #include <android/log.h>      #ifdef _cplusplus   extern "C" {   #endif      #ifndef LOG_TAG   #define LOG_TAG    "MY_LOG_TAG"   #endif      #define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)   #define LOGI(...)  __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)   #define LOGW(...)  __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)   #define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)   #define LOGF(...)  __android_log_print(ANDROID_LOG_FATAL,LOG_TAG,__VA_ARGS__)         #ifdef __cplusplus   }   #endif      #endif /* __JNILOGGER_H_ */

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

    ifeq ($(HOST_OS),windows)   #NDK环境下       LOCAL_LDLIBS := -llog   else   #完整源码环境下       LOCAL_SHARED_LIBRARIES := libutils   endif

    Android为JNI提供的助手方法

    myeclair\dalvik\libnativehelper\include\nativehelper

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

    #ifndef NELEM   # define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))   #endif      //有了以上宏定义后,注册方法可以按如下写,该宏定义可以直接copy到NDK环境下使用:   (*env)->RegisterNatives(env,clazz, methods,NELEM(methods));

    更多关于JNI在Android中的信息:

    JNI系列(1):基础篇

    JNI系列(2):jstring操作

    JN系列(3):如何得到JavaVM,JNIEnv接口

    JNI系列(4):如何访问自定义类对象




更多相关文章

  1. C语言函数的递归(上)
  2. Android(安卓)Studio 2.0--如何使用新模拟器以及Instant Run
  3. Binder框架 -- android AIDL 的使用
  4. Android开发之WebView的使用(1)
  5. Android(安卓)应用保存状态
  6. Android(安卓)NDK学习笔记4-Android.mk篇
  7. Android使用addView动态添加组件
  8. android避免service被杀 博客分类: android 1.在service中重写下
  9. Android软键盘弹出遮挡EidtText的解决方法

随机推荐

  1. Android(安卓)断点续传下载
  2. android中的提示方法
  3. Android(安卓)Log日志规则打印
  4. android 入门学习笔记 取得文件架构、打
  5. 软键盘弹出后ScrollView设置为滚动
  6. 【IMOOC学习笔记】多种多样的App主界面Ta
  7. android BroadcastReceiver 注册方式
  8. HTC XV6850 Android系统移植资源索引
  9. Oprofile分析(android oprofile性能分析)
  10. Windows下搭建Eclipse+Android4.0开发环