开发JNI项目前提是需要有NDK(Native Development Kit)的支持。因此,在开发前需要先安装和配置NDK。步骤如下:
点击菜单"Tools" -> "Android" -> "SDK Manager"打开SDK管理器。
选中右边面板的"SDK Tools"页签,勾选"NDK"一栏,然后点击"Apply"来下载并安装NDK(如下图)。
Android SDK面板点击菜单“File”-“New”-“New Project...”打开新建项目界面, 输入项目名称:
新建项目选择支持的平台及最低支持的系统版本
选择支持平台及系统版本选择项目模版Basic Activity
添加Activity设置Activity的命名
设置Activity至此,创建项目完成。
打开gradle.properties文件,在该文件下添加:
android.useDeprecatedNdk=true打开local.properties文件,在该文件下添加:
ndk.dir=NDK的路径再打开模块的build.gradle文件,在android/defaultConfig下面添加ndk节点,如下所示:
apply plugin: 'com.android.application' android { compileSdkVersion 23 buildToolsVersion "23.0.3" defaultConfig { applicationId "vimfung.cn.jnisample" minSdkVersion 15 targetSdkVersion 23 versionCode 1 versionName "1.0" ndk { moduleName "JNISample" stl "stlport_static" ldLibs "log" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:23.4.0' compile 'com.android.support:design:23.4.0' }其中ndk节点下面的字段说明如下:
名称 说明 moduleName 模块名称,即编译出来的.so的名字 stl 默认情况下JNI中是无法使用STL标准库的,加入此字段表示使用STL标准库。 ldLibs "log" 表示加入Android的调试日志,只要在导入 #include <android/log.h> 就可以使用__android_log_print方法打印日志到logcat中。创建一个叫JNIUtil的Java类,并声明一个使用native关键字修饰的test方法,代码如下:
package vimfung.cn.jnisample; /** * Created by vimfung on 16/9/8. */ public class JNIUtil { public native String test (); }注:声明jni的方法必须带有native关键字,否则将视为一般的方法。设置native的方法允许为静态/非静态方法(即加或不加static关键字)。
Command+F9(或菜单“Build”-“Make Project”)进行编译,成功后点击界面最下面的Terminal按钮打开终端面板(终端会自动定位到当前项目目录非常方便^o^)。使用cd命令跳转到app/build/intermediates/classes/debug/目录下,输入脚本如下:
$ cd app/build/intermediates/classes/debug/使用javah命令生成刚才创建的JNIUtil类的JNI的头文件(.h文件),如:
javah vimfung.cn.jnisample.JNIUtil这里要注意的是javah要根据包名来对应目录路径来查找对应的.class文件,所以定位的目录必须要在包目录结构的上一级(即这里的debug目录),否则会提示找不到对应的类。
执行成功后(执行成功是不会输出任何信息的,错误了才会提示-_-#)会在debug目录下多出一个vimfung_cn_jnisample_JNIUtil.h的头文件,如下所示:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class vimfung_cn_jnisample_JNIUtil */ #ifndef _Included_vimfung_cn_jnisample_JNIUtil #define _Included_vimfung_cn_jnisample_JNIUtil #ifdef __cplusplus extern "C" { #endif /* * Class: vimfung_cn_jnisample_JNIUtil * Method: test * Signature: ()Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_vimfung_cn_jnisample_JNIUtil_test (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif可以看到刚才定义的test方法在头文件中被声明为Java_vimfung_cn_jnisample_JNIUtil_test 方法(其命名规则我猜应该是Java+“”+“包名1”+“” +“包名2”+“”...+“”+“类名”+“_”+“方法名”)。其中有jstring、JNIEnv、jobject这些就是JNI提供给C/C++调用的类型和接口,利用这些东西可以跟Java层进行一些交互。
回到Android Studio中,右键app目录,在弹出菜单中选择“New”-“Folder”-“JNI Folder”建立一个JNI的目录。如图:
创建JNI目录然后把刚才生成的头文件拷贝到这个目录下,最终效果如图:
拷贝头文件到JNI目录创建一个c++的文件,名字叫vimfung_cn_jnisample_JNIUtil.cpp,然后Java_vimfung_cn_jnisample_JNIUtil_test方法进行实现,如:
// // Created by vimfung on 16/9/8. // #include "vimfung_cn_jnisample_JNIUtil.h" JNIEXPORT jstring JNICALL Java_vimfung_cn_jnisample_JNIUtil_test(JNIEnv *env, jobject obj) { return env -> NewStringUTF("Hello World!"); }该方法直接通过env构造一个Java的字符串返回值并赋值为“Hello World!”进行返回。关于JNI提供的接口功能在后面的章节会进行介绍,我们这里只要知道是返回字符串就可以了。
现在再打开JNIUtil的Java类,让外部一旦使用该类时即加载JNISample.so这个库,修改如下:
package vimfung.cn.jnisample; /** * Created by vimfung on 16/9/8. */ public class JNIUtil { static { System.loadLibrary("JNISample"); } public native String test (); }最后,打开MainActivity.java文件,添加一个JNIUtil的属性,并且在onCreate的时候初始化并调用test方法。这一步骤主要是验证我们的JNI接口是否正常运行,修改的代码如下:
public class MainActivity extends AppCompatActivity { private JNIUtil jniUtil; @Override protected void onCreate(Bundle savedInstanceState) { //...此处忽略已有代码 jniUtil = new JNIUtil(); Log.v("test", jniUtil.test()); } }编译运行App,如果正常编译运行,可以看到logcat中输出Hello World!信息。如图:
logcat输出这是Demo的地址:http://git.oschina.net/vimfung/JNISample
JNI中定义了一系列的以j开头的类型,下面以表格形式对其进行说明:
类型 说明 jboolean 布尔类型,对应一个无符号的char类型 jbyte 字节类型,对应一个有符号的char类型 jchar 字符类型,对应一个16位无符号整型 jshort 16位整型 jint 32位整型 jlong 64位整型 jfloat 32位浮点型 jdouble 64位浮点型 jsize 等同jint,用于表示大小,长度。 jobject 表示Java中的Object类型 jclass 表示Java中的Class类型,可以通过env的FindClass方法取得。 jstring 表示Java中的String类型 jarray 表示Java中的数组类型 jobjectArray 表示Java中的对象数组,如:Object arr[]; jbooleanArray 表示Java中的布尔数组,如:boolean arr[]; jbyteArray 表示Java中的字节数组,如:byte arr[]; jshortArray 表示Java中16位整型数组,如:short arr[]; jintArray 表示Java中32位整型数组,如:int arr[]; jlongArray 表示Java中64位整型数组,如:long arr[]; jfloatArray 表示Java中32位浮点型数组,如:float arr[]; jdoubleArray 表示Java中64位浮点型数组,如:double arr[]; jthrowable 表示Java中的异常对象Exception jweak 表示Java中的弱引用对象 jfieldID 表示Java类中的属性标识符,可以通过env的GetFieldID或GetStaticFieldID等系列方法取得。 jmethodID 表示Java类中的方法标识符,可以通过env的GetMethodID或者GetStaticMethodID方法取得对于每个JNI方法来说通常会有两个参数,第一个参数是JNIEnv类型,代表了VM里面的环境,本地的代码可以通过该参数与Java代码进行操作。第二个参数是定义JNI方法的类的一个本地引用(this)。
接下来介绍一下JNIEnv会提供一些什么样的方法(这里只列举我用过的方法,往后再慢慢补充^o^):
jclass FindClass(const char* name)说明: 查找Java的类型,该方法可以通过传入一个类名称来查找对应的类型。
参数: name - 类名称(名称是以斜杆分隔包名的方式,如:vimfung/cn/jnisample/JNIUtil)
返回: 一个类型对象。使用该对象可以取得其属性、方法。
说明: 创建一个全局引用对象。一旦对象在全局引用,则可以在多个方法中使用。因为Java有其自己的回收机制,因此在JNI中不可以使用static来维持对象的生命周期,所以,需要使用该方法来变更对象的生命周期。如果要释放全局对象请使用DeleteGlobalRef方法。
参数: obj - 需要全局引用的对象。
返回:全局的对象引用
说明: 删除一个全局的对象引用。主要用于删除NewGlobalRef创建的对象引用。
参数: globalRef - 全局的对象引用
说明: 删除一个本地的对象引用。一般情况下env创建的对象都属于本地引用,如果不调用该方法,在JNI接口执行完毕后也会被自动回收掉。
参数: localRef - 本地对象引用。
说明: 创建一个新的本地对象引用。
参数: ref - 对象引用返回:本地对象引用
说明: 创建一个新的Java对象实例。
参数: clazz - 需要创建实例的类型 methodID - 这里需要传入构造方法的标志 ... - 构造方法的一个或者多个参数
返回: 对象的实例
说明: 获取Java对象所属的类型。相当于Java中的getClass方法。
参数: obj - 对象
返回: 返回对象的类型
说明: 判断对象是否是指定类型的实例
参数: obj - 要判断的对象引用 clazz - 要检查的类型
返回: 1 表示对象为该类型的实例,0 表示对象不是该类型的实例
说明: 由于在调用Java类某个方法时需要传递方法标识,而该方法就是用于获取指定类的方法的标识。
参数: clazz - 类型 name - 方法名称,如:value,注:构造方法用<init>表示 sig - 方法的签名,如:(Ljava/lang/Object;)V。 对于方法签名,其组织形式如:(参数类型1 参数类型2 ...)返回值类型。其中的类型参考下面章节:JNI的方法/属性签名中的类型对照表
返回: 方法标识(jmethodID)
说明: 调用无返回值的方法时使用该方法。 其中CallVoidMethodV与CallVoidMethod的区别是,前者只有三个参数,其第三个参数是一个va_list类型,用于接收一个参数列表,该方法常用于被不定项参数方法嵌套时使用(即调用它的方法定义了...参数)。而后者是一个不定项参数方法。
参数: obj - 要调用方法的对象实例 methodID - 方法标识,通过GetMethodID获得。 ... - 方法的参数,可以传入一个或多个。args - 参数列表
说明: 获取类型的属性标识。在获取或设置某个属性值前,需要先得到属性的标识(jfieldID),就需要调用此方法获取。
参数: clazz - 类型 name - 属性名称 sig - 属性签名,其描述方法为:属性类型。类型请参考下面章节:JNI的方法/属性签名中的类型对照表
返回:属性标识
说明: 获取指定对象的属性值。该系列方法的声明形式遵循“Get+类型+Field”。可以根据在Java中的不同类型属性,选择不同的方法来获取属性值。
参数: obj - 要获取属性值的对象实例 fieldID - 属性标识,可通过GetFieldID获得。
返回: 属性值
说明: 设置指定对象的属性值。该系列方法的声明形式遵循“Set+类型+Field”方式,可以根据在Java中的不同类型,选择不同的方法进行调用。
参数: obj - 要设置属性值的对象实例 fieldID - 属性标识,可通过GetFieldID获得。 value - 属性值
说明: 获取指定类型的静态方法(即带static关键字描述)的标识。
参数: clazz - 要获取方法标识的类型 name - 方法的名称 sig - 方法的签名,如:(Ljava/lang/Object;)V。组织形式跟GetMethodID相同。
返回: 方法标识(fmethodID)
说明: 调用类型的静态方法。所调用的静态方法无返回值时使用。 其中CallStaticVoidMethodV与CallStaticVoidMethod的区别是,前者只有三个参数,其第三个参数是一个va_list类型,用于接收一个参数列表,该方法常用于被不定项参数方法嵌套时使用(即调用它的方法定义了...参数)。而后者是一个不定项参数方法。
参数: clazz - 调用方法的类型 methodID - 方法标识 ... - 方法的参数,可以是一个或者多个。 args - 参数列表。
说明: 获取类型的静态属性(即带static关键字描述)的标识。
参数: clazz - 要获取属性标识的类型 name - 属性名称 sig - 属性签名,与GetFieldID相同。
说明: 获取指定类型的静态属性值。该系列方法的声明形式遵循“GetStatic+类型+Field”。可以根据在Java中的不同类型属性,选择不同的方法来获取属性值。
参数: clazz - 要获取属性的类型 fieldID - 属性标识,可通过GetStaticFieldID方法获得。
返回: 属性值
说明: 设置指定类型的属性值。该系列方法的声明形式遵循“SetStatic+类型+Field”方式,可以根据在Java中的不同类型,选择不同的方法进行调用。
参数: clazz - 要设置属性的类型 fieldID - 属性标识,可通过GetStaticFieldID方法获得。 value - 属性值
说明: 创建一个Unicode格式的字符串
参数: unicodeChars - 字符串内容 len - 字符串长度
返回: 字符串对象
说明: 获取Unicode格式的字符串长度
参数: string - 字符串
返回: 字符串长度
说明: 将jstring转换成为Unicode格式的char*字符串
参数: string - 要转换的字符串 isCopy - 该参数用于获取当前的char*字符串是否为字符串对象的一份拷贝。http://blog.sina.com.cn/s/blog_78eb91cd0102uzv6.html 这篇博文里面对其有描述:
当从 JNI 函数 GetStringChars 中返回得到字符串B时,如果B是原始字符串java.lang.String 的拷贝,则isCopy被赋值为 JNI_TRUE。如果B和原始字符串指向的是JVM中的同一份数据,则 isCopy被赋值为 JNI_FALSE。当 isCopy值为JNI_FALSE时,本地代码决不能修改字符串的内容,否则JVM中的原始字符串也会被修改,这会打破 JAVA语言中字符串不可变的规则。 通常,因为你不必关心 JVM 是否会返回原始字符串的拷贝,你只需要为 isCopy传递NULL作为参数。
返回: Unicode格式的char*字符串
说明: 释放指向Unicode格式字符串的char*字符串指针
参数: string - 与char*关联的Unicode格式字符串 chars - 要释放的char *字符串
说明: 创建一个UTF-8格式的字符串
参数: bytes - char*字符串
返回: UTF-8格式的字符串对象
说明: 获取UTF-8格式字符串的长度
参数: string - UTF-8格式的字符串
返回: 字符串长度
说明: 将jstring转换成为UTF-8格式的char*字符串
参数: string - 要转换的字符串 isCopy - 该参数用于获取当前的char*字符串是否为字符串对象的一份拷贝。
返回: UTF-8格式的char*字符串
说明: 释放指向UTF-8格式字符串的char*字符串指针
参数: string - 与char*关联的UTF-8格式字符串 utf - 要释放的char *字符串
说明: 获取一个数组所包含的元素数量
参数: array - 数组对象
返回: 数组长度
说明: 创建一个对象数组,该系列方法声明形式遵循“New+类型+Array”。可以根据自己需要保存的类型来调用不同的方法创建不同类型的数组。
参数: length - 数组长度 elementClass - 元素类型,ObjectArray特有 initialElement - 元素的初始值,ObjectArray特有
返回: 数组对象
说明: 获取对象数组的元素
参数: array - 数组对象 index - 要获取元素的下标索引
返回: 数组元素对象
说明: 设置对象素组的元素
参数: array - 数组对象 index - 插入元素的下标索引 value - 要设置的元素
说明: 将基础类型数组转化为类型指针,返回的指针对象可以通过指针偏移来获取不同下标索引的元素(如:(ptr+1)、(ptr+2)....)。该系列方法声明遵循“Get+类型+ArrayElements”形式,可以根据数组的类型调用相对应的方法。
参数: array - 基础类型数组对象 isCopy - 该参数用于获取当前的指针是否为数组的一份拷贝。
返回: 数组指针
说明: 释放指向基础类型数组的数组指针。该系列方法声明遵循“Release+类型+ArrayElements”形式,可以根据数组的类型调用相对应的方法。
参数: array - 与指针关联的基础类型数组 elems - 数组指针 mode - 释放模式,官网对其的说明如下:
The last argument to the ReleaseByteArrayElements function above can have the following values:
0: Updates to the array from within the C code are reflected in the Java language copy.
JNI_COMMIT: The Java language copy is updated, but the local jbyteArray is not freed.
JNI_ABORT: Changes are not copied back, but the jbyteArray is freed. The value is used only if the array is obtained with a get mode of JNI_TRUE meaning the array is a copy.
说明: 获取指定基础类型数组的多个元素。该系列方法声明遵循“Get+类型+ArrayRegion”形式,可以根据数组的类型选择对应的方法进行调用。取得元素后可以从buf中获取相应的元素。
参数: array - 基础类型数组 start - 要获取元素的起始下标索引 len - 要获取元素的个数 buf - 用来存储元素的指针变量,必须要为指针变量申请内存并且内存长度要大于获取内容的长度。
说明: 设置基础类型数组的多个元素值。该系列方法声明遵循“Set+类型+ArrayRegion”形式。可以根据数组的类型选择对应的方法进行调用。
参数: array - 基础类型数组 start - 要替换元素对应在array中的起始下标索引 len - 替换元素的数量 buf - 替换元素的指针变量,从该指针中读取输入放入数组中。
说明: 创建一个全局的弱引用对象
参数: obj - 需要创建弱引用的对象引用
返回: 弱引用对象
说明: 删除一个全局弱引用
参数: obj - 全局的弱引用对象
一般情况下,JNI接口都会与Java层进行一些交互,那么,就必须要用到env中提供的一些方法实现这类的需求。下面我再添加一个JNI的接口,该接口主要功能是实现在原生代码中创建一个HashMap并填充数据,最后返回给Java层。
先打开JNIUtil定义一个新的方法:
public native HashMap createHashMap (String key1, String value1, String key2, String value2);Command+F9编译项目,成功后再次使用Terminal对新编译的JNIUtil生成头文件(是的,每次添加新接口都需要这样做),然后拷贝到JNI目录下覆盖原有文件。生成的头文件会多出一个新的接口声明,如下所示:
JNIEXPORT jobject JNICALL Java_vimfung_cn_jnisample_JNIUtil_createHashMap (JNIEnv *, jobject, jstring, jstring, jstring, jstring);然后再vimfung_cn_jnisample_JNIUtil.cpp中写入它的方法实现,代码如下:
JNIEXPORT jobject JNICALL Java_vimfung_cn_jnisample_JNIUtil_createHashMap (JNIEnv *env, jobject thiz, jstring key1, jstring value1, jstring key2, jstring value2) { //为了方便日后获取其他地方需要使用HashMap的类型,在这里可以定义为static,然后对jclass创建全局的内存引用。 static jclass hashMapClass = NULL; if (hashMapClass == NULL) { jclass hashMapClassLocal = env -> FindClass("java/util/HashMap"); hashMapClass = (jclass)env -> NewGlobalRef(hashMapClassLocal); env->DeleteLocalRef(hashMapClassLocal); } //获取构造方法标识和插入数据方法标识 static jmethodID initMethodId = env -> GetMethodID(hashMapClass, "<init>", "()V"); static jmethodID putMethodId = env -> GetMethodID(hashMapClass, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); //创建HashMap对象 jobject hashMapInstance= env -> NewObject(hashMapClass, initMethodId); //放入对象到HashMap中,相当于调用HashMap的put方法 env -> CallObjectMethod(hashMapInstance, putMethodId, key1, value1); env -> CallObjectMethod(hashMapInstance, putMethodId, key2, value2); return hashMapInstance; }上面的代码完整地演示了如何在JNI中操作一个HashMap,从通过FindClass找到HashMap的类型,再到GetMethodID获取HashMap的方法,最后通过CallObjectMethod来调用HashMap的方法来创建对象并放入数据,这些过程对于操作每个Java类都是必须的。
这个方法同时也演示了如何使用NewGlobalRef来创建可跨方法的访问的对象,其中的hashMapClass正是这样的处理(只要把hashMapClass放到方法外面定义就可以实现跨方法访问了)。这里要谨记的一点是,只有static描述一个变量是不足以维持jobject的生命周期的,因为JVM会管理这些对象的生命周期,因此,需要使用NewGlobalRef来把一个本地的对象引用提升到全局的对象引用,就可以实现跨方法访问对象了。
最后再MainActivity的onCreate中调用createHashMap方法并输出它的内容:
Log.v("create HashMap = %s", jniUtil.createHashMap("Fist Name", "Vim", "Last Name", "Fung").toString());最后编译运行,如果一切顺利的话可以在logcat中看到下面的信息:
