Android中JNI编程详解

前几天在参加腾讯模拟考的时候,腾讯出了一道关于JNI的题,具体如下:

Android中JNI编程详解

JNI本身是一个非常复杂的知识,但是其实对于腾讯的这道题而言,如果你懂JNI,那么你可能会觉得这道题非常简单,就相当于C语言中的hello world级难度,但是事实上这道题一点都不简单,它涉及到JNI函数的调用的一些细节,很可能正是因为这些细节导致程序运行结果和你所预期的不一样,真的不得不说腾讯不愧为腾讯,出题不会出那种很难很难的但绝对不简单,这道题出的真的很有水平。下面就我自己翻看的一些参考书及网上的一些资料,综合自己的思考与理解,以腾讯的这道考题为例来详细讲解关于JNI的知识。

JNI是Java Native Interface的缩写,它提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。JNI的作用就是使用java语言与本地宿主语言通信,那么我们为何需要了解JNI呢?这就要从安卓系统的整个体系框架说起。安卓系统的整个体系框架如下图所示:

Android中JNI编程详解

可以看到Android上层的Application和ApplicationFramework都是使用Java编写,底层包括系统和使用众多的LIiraries都是C/C++编写的。所以上层Java要调用底层的C/C++函数库必须通过Java的JNI来实现。实际上我们不鼓励使用JNI,除非必须使用。

因为一旦使用JNI,JAVA程序就丧失了JAVA平台的两个优点:

1、程序不再跨平台。要想跨平台,必须在不同的系统环境下重新编译本地语言部分。

2、程序不再是绝对安全的,本地代码的不当使用可能导致整个程序崩溃。一个通用规则是,你应该让本地方法集中在少数几个类当中。这样就降低了JAVA和C/C++之间的耦合性。

一如何使用第三方为我们定义好的JNI接口:

可能对于绝大多数APP而言,自己实现JNI接口的情况比较少,除非是像BAT级别对产品性能要求非常高的企业,对于绝大多数企业开发人员来讲自己通常要完成某些高性能模块开发时只需要调用第三方为我们写好的SDK即可,如本人的仿腾讯QQ的即时通讯软件中就用到了环信的SDK,而在这个过程中就用到了.so动态库文件。即JNI本地接口文件。所以我们通常只需要掌握如何使用so文件即可。

假如已有一个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接口

以hello world为例,具体步骤如下:

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

package com.lucyfyr;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log; public class HelloWorld extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.main);
  Log.v("dufresne", printJNI("I am HelloWorld Activity"));
}
  static
  {
    //加载库文件
    System.loadLibrary("HelloWorldJni");
  }
  //声明原生函数 参数为String类型 返回类型为String
  private native String printJNI(String inputStr);
}



2通过javah命令生成生成共享库的头文件:

 进入到eclipse生成的Android Project中 :/HelloWorld/bin/classes/com/lucyfyr/ 下,可以看到里面后很多后缀为.class的文件,就是eclipse为我们自动编译好了的java文件,其中就有HelloWorld.class文件。退回到classes一级目录:/HelloWorld/bin/classes/。执行如下命令:

javah com.lucyfyr.HelloWorld 生成文件:com_lucyfyr_HelloWorld.h。

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_lucyfyr_HelloWorld */
#ifndef _Included_com_lucyfyr_HelloWorld
#define _Included_com_lucyfyr_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_lucyfyr_HelloWorld
* Method: printJNI
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_lucyfyr_HelloWorld_printJNI
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

可以看到自动生成对应的函数:Java_com_lucyfyr_HelloWorld_printJNI

Java_ + 包名(com.lucyfyr) + 类名(HelloWorld) + 接口名(printJNI):必须要按此JNI规范来操作;java虚拟机就可以在com.simon.HelloWorld类调用printJNI接口的时候自动找到这个C实现的Native函数调用。这是一个标准的C语言头文件,其中的JNIEXPORT、JNICALL是JNI关键字(事实上它是没有任何内容的宏,仅用于指示性说明),而jint、jstring是JNI环境下对int及java.lang.String类型的映射。这些关键字的定义都可以在jni.h中看到。

3实现JNI原生函数源文件,即在本地.c文件中实现上述java类中定义的native函数

这一步是整个JNI实现中最重要的步骤,本质上就是将java中传入的参数运用JNI函数先转换为C/C++能处理的数据结构,如字符串一般使用const char *GetStringUTFChars(JNIEnv *env, jstring string,jboolean * isCopy)函数将java格式转换为char
*格式,这样就可以调用本地C/C++函数来处理该字符串,处理完之后再调用JNI函数将C/C++中的数据结构转换为java数据类型,然后返回给调用该函数的java对象,如返回字符串一般使用jstring   NewStringUTF(JNIEnv *env, const char *bytes);将char *类型字符串转换为java中的String类型。

总之,该模块是核心,主要任务就是将java层数据结构转换为本地语言能处理的格式,然后调用本地API处理,然后将处理后的结果转换为java层能识别的数据类型,然后返回该结果。

#include <jni.h>
#define LOG_TAG "HelloWorld"
#include <utils/Log.h>
/* Native interface, it will be call in java code */
JNIEXPORT jstring JNICALL Java_com_lucyfyr_HelloWorld_printJNI(JNIEnv *env, jobject obj,jstring inputStr)
{
  LOGI("dufresne Hello World From libhelloworld.so!");
  // 从 instring 字符串取得指向字符串 UTF 编码的指针
  const char *str =
  (const char *)(*env)->GetStringUTFChars( env,inputStr, JNI_FALSE );
  LOGI("dufresne--->%s",(const char *)str);
  // 通知虚拟机本地代码不再需要通过 str 访问 Java 字符串。
  (*env)->ReleaseStringUTFChars(env, inputStr, (const char *)str );
  return (*env)->NewStringUTF(env, "Hello World! I am Native interface");
} /* This function will be call when the library first be load.
* You can do some init in the libray. return which version jni it support.
*/
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
  void *venv;
  LOGI("dufresne----->JNI_OnLoad!");
  if ((*vm)->GetEnv(vm, (void**)&venv, JNI_VERSION_1_4) != JNI_OK) {
    LOGE("dufresne--->ERROR: GetEnv failed");
    return -1;
  }
  return JNI_VERSION_1_4;
}

OnLoadJava_com_lucyfyr_HelloWorld_printJNI函数里面做一些log输出 注意JNI中的log输出的不同。

JNI_OnLoad函数JNI规范定义的,当共享库第一次被加载的时候会被回调,这个函数里面可以进行一些初始化工作,比如注册函数映射表,缓存一些变量等,最后返回当前环境所支持的JNI环境。本例只是简单的返回当前JNI环境,重点了解一下JNI组件的入口函数——JNI_OnLoad()、JNI_OnUnload()

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

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

//onLoad方法,在System.loadLibrary()执行时被调用

jint JNI_OnLoad(JavaVM* vm, void* reserved){

LOGI("JNI_OnLoad startup~~!");

return JNI_VERSION_1_4;

}

//onUnLoad方法,在JNI组件被释放时调用

void JNI_OnUnload(JavaVM* vm, void* reserved){

LOGE("call JNI_OnUnload ~~!!");

}

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组件时会呼叫它,因此在该方法中进行善后清理,资源释放的动作最为合适。

JNI常用的函数,主要是关于处理字符串的:

const char *GetStringUTFChars(JNIEnv
*env, jstring string,jboolean * isCopy
将java格式转换为char
*格式,这样就可以调用本地C/C++函数来处理该字符串

参数说明:

JNIEnv
*env:JNI接口指针,一般JNI函数中都存在这个参数,就像Android中的Context

jstring
string:java类型的字符串对象

jboolean
* isCopy:布尔类型指针,该值如果被传入JNI_TRUE则表示该函数返回的字符串将会是string字符串的一份拷贝,如果传入JNI_FALSE则表示返回的结果是和原字符串指向的是JVM中的同一份数据,当该值为JNI_FALSE时,本地代码绝不能更改字符串的内容,否则JVM中的原始字符串也会被修改,这不符合java语言中字符串一旦产生就是固定的这一原则,该值通常传入JNI_FALSE。

void
   ReleaseStringUTFChars(JNIEnv *env, jstring string,const char *str):
该函数通知虚拟机本地代码已经不再需要访问str字符串,这个字符串即上述GetStringUTFChars函数返回的字符串,这两个函数一般成对出现,类似于C中的malloc与free,该函数的作用就是防止内存泄漏,因为java层存在GC机制,而JNI中会调用本地代码,所以得自己释放不用的内存,来防止内存泄漏,就像C++中的析构函数。

参数说明:

JNIEnv *env:JNI接口指针,一般JNI函数中都存在这个参数,就像Android中的Context

jstring string:java类型的字符串对象

const char * str指向GetStringUTFChars函数返回的字符串的指针

jstring
  NewStringUTF(JNIEnv *env, const char *bytes);
将本地char* 类型的字符串构造为java层的String对象

参数说明:

JNIEnv *env:JNI接口指针,一般JNI函数中都存在这个参数,就像Android中的Context

const char *bytes:指向char *类型的指针,即用来操作本地字符串的指针

返回值:
Java 字符串对象。如果无法构造该字符串,则为 NULL

4编译生成so库:编译com_lucyfyr_HelloWorld.c成so库可以和app一起编译,也可以都单独编译。在当前目录下建立jni文件夹:HelloWorld/jni/

下建立Android.mk ,并将com_lucyfyr_HelloWorld.c和 com_lucyfyr_HelloWorld.h 拷贝到进去

编写编译生成so库的Android.mk文件:

LOCAL_PATH:= $(call my-dir)
# 一个完整模块编译
include $(CLEAR_VARS)
LOCAL_SRC_FILES:=com_lucyfyr_HelloWorld.c
LOCAL_C_INCLUDES := $(JNI_H_INCLUDE)
LOCAL_MODULE := libHelloWorldJni
LOCAL_SHARED_LIBRARIES := libutils
LOCAL_PRELINK_MODULE := false
LOCAL_MODULE_TAGS :=optional
include $(BUILD_SHARED_LIBRARY)

系统变量解析:

  LOCAL_PATH - 编译时的目录

  $(call 目录,目录….) 目录引入操作符

    如该目录下有个文件夹名称 src,则可以这样写 $(call src),那么就会得到 src 目录的完整路径

  include $(CLEAR_VARS) -清除之前的一些系统变量

  LOCAL_MODULE - 编译生成的目标对象

  LOCAL_SRC_FILES - 编译的源文件

  LOCAL_C_INCLUDES - 需要包含的头文件目录

  LOCAL_SHARED_LIBRARIES - 链接时需要的外部库

  LOCAL_PRELINK_MODULE - 是否需要prelink处理 

  include$(BUILD_SHARED_LIBRARY) - 指明要编译成动态库

这就涉及到另一块知识,即android.mk编译模块添加,不懂得请自行百度。

编译此模块:输入编译命令

  ./makeMtk mm packages/apps/HelloWorld/jni/

  上面是我的工程根目录编译命令。具体编译方式根据自己系统要求执行。

编译输出: libHelloWorldJni.so (system/lib中视具体而定)

  此时库文件编译好了可以使用,如果在eclipse中模拟器上使用,需要将 libHelloWorldJni.so导入到system/lib 下或者对应app的data/data/com.lucyfyr/lib/下;

看一下HelloWorld中Android.mk文件的配置

其中存在:include $(LOCAL_PATH)/jni/Android.mk 表示编译库文件

LOCAL_JNI_SHARED_LIBRARIES := libHelloWorldJni 表示app依赖库,打包的时候会一起打包。



5运行程序: 将编译好的apk安装到手机上,使用adb
push到手机上去需要自己去导入库文件libHelloWorldJni.so到data/data/com.lucyfyr/lib/使用adb install方式安装则会自动导入。

启动HelloWorld :输入命令 adb logcat |grep dufresne

输出log如下:

 I/HelloWorld(28500): dufresne Hello World From libhelloworld.so!

 I/HelloWorld(28500): dufresne--->I am HelloWorld Activity

 V/dufresne(28500): Hello World! I am Native interface

符合调用打印顺序正确。

讲到这里,腾讯的那道考题是不是很简单,其实腾讯的那道考题就是谷歌官方写的JNI的sample。

上一篇:Android中JNI 的一些常用Method说明


下一篇:Android中JNI编程的那些事儿(1)