概念整理
JNI是Java native interface 的簡稱,它是Jave功能組建和native C或者C++協(xié)同工作的一種方式强缘。Android提供了NDK(Native Development Kit)用來編譯打包C或者C++編寫的代碼克滴,以供Java端調(diào)用罐栈。這些功能在Android Studio中有比較好的圖形話界面支持豪嗽,當(dāng)然也可以通過命令行來運(yùn)行外厂。JNI是雙向的冕象,可以由Java端來invoke C/C++代碼編譯出來的binary,也可以由C/C++端來invoke Java代碼酣衷。一篇不錯(cuò)的快速入門交惯。總結(jié)一下穿仪,核心主要在于跨語言的類型轉(zhuǎn)換席爽,在轉(zhuǎn)換成當(dāng)前語言數(shù)據(jù)類型之后,就是常規(guī)編程了啊片,然后在返回時(shí)再轉(zhuǎn)換回去對方語言的數(shù)據(jù)類型只锻。
從Java端invoke C++的話,就是由javah/javac通過Java method的native modifier生成.h頭文件紫谷,再由程序員來實(shí)現(xiàn)頭文件里的interface齐饮。具體實(shí)現(xiàn)中需要用到JNIEnv這個(gè)pointer來運(yùn)行其指向method table里的method,從而做到一些類型轉(zhuǎn)換笤昨。從C++端invoke Java主要是用類似reflection的操作祖驱,jclass,jmethodID和jfieldID瞒窒。
NativeActivity是一個(gè)Android提供的Helper class捺僻,用來使得開發(fā)者更方便開發(fā)native activity。所謂的Native activity就是程序員把UI的邏輯全部寫在Native C++層崇裁。這樣的好處是可以更方便地進(jìn)行OpenGL的rendering匕坯。事實(shí)上,開發(fā)者只需要實(shí)現(xiàn)native_activity.h頭文件里的一些callback方法就可以了拔稳。Android官方也給出了android_native_app_glue這樣的interface來進(jìn)一步簡化開發(fā)葛峻。付一個(gè)比較簡單的例子和一個(gè)官方樣例。
ABI和API的對比巴比。首先API大家比較熟悉术奖,是Application Programming Interface,是一種調(diào)用外部函數(shù)或功能組件的方式轻绞, 包括protocol腰耙,tools或者OS功能組建。這種Interface是基于source code(源代碼)铲球。ABI是Application Binary Interface的簡稱,這種interface則是基于binary code的晰赞。在程序員寫代碼調(diào)用Library時(shí)稼病,是針對API編程的选侨,而在source code編譯之后,程序則是調(diào)用ABI來實(shí)現(xiàn)功能然走。API設(shè)計(jì)時(shí)盡量保證穩(wěn)定性援制,但是功能擴(kuò)展或者業(yè)務(wù)邏輯變動,則無法避免改變原來的interface芍瑞。而ABI晨仑,由于它所定義的是比較底層的功能,本身的操作比較簡單拆檬。在設(shè)計(jì)時(shí)要求有更高的穩(wěn)定性洪己,很多時(shí)候只允許增加新功能,而不能改變現(xiàn)有功能竟贯。
數(shù)據(jù)類型
JNI的Java端就是基本的Java數(shù)據(jù)類型答捕,在C/C++端則專門定義了一些數(shù)據(jù)類型,主要有以下這些屑那。
對于Java對象拱镐,有相對應(yīng)的native類型。
在C中其他的Java類都被定義為jobject類型持际;在C++中沃琅,這些基本類型都被用類型定義,例如:
class _jobject {};
class _jclass : public _jobject {};
typedef _jobject *jobject;
typedef _jclass *jclass;
而MethodID和FieldID則是普通的C語言指針蜘欲。
struct _jfieldID; /* opaque structure */
typedef struct _jfieldID *jfieldID; /* field IDs */
struct _jmethodID; /* opaque structure */
typedef struct _jmethodID *jmethodID; /* method IDs */
在C/C++端來讀取從Java端傳遞過來的對象時(shí)益眉,需要用到GetFieldID和GetMethodID方法來獲取引用。這兩個(gè)方法都需要傳入一個(gè)字符串來描述filed和method的signature芒填。以下是字符串里元素和具體Java類型的一個(gè)映射表呜叫。
大家對比上面的映射表,再看下面這個(gè)例子就很容易看懂了殿衰。
// 對應(yīng)的Java類里有“name”field他的類型是java.lang.string
(*env)->GetFieldID(env, class, "name", "Ljava/lang/String;");
// 對應(yīng)的Java類里有“setName” method朱庆,它的signature是void setName(String name, int[] accounts)
(*env)->GetMethodID(env, class, "setInfo", "(Ljava/lang/String;[I)V");
JNI編寫過程
這里以上面提到的快速入門里的代碼為例,整理一下JNI編寫過程闷祥。
Java端
public class Hello {
// native 這個(gè)關(guān)鍵詞是用來聲明native方法的娱颊。意思就是在C/C++端會有這個(gè)方法的實(shí)現(xiàn)。
// 那么在Java端凯砍,就可以執(zhí)行這個(gè)方法箱硕,具體使用跟Java的一般方法并無區(qū)別。
public native void sayHi(String who, int times);
// 通常我們用static代碼段來加載native庫悟衩。作為參數(shù)的字符串則是庫名稱剧罩。
static {
System.loadLibrary("HelloImpl");
}
public static void main (String[] args) {
Hello hello = new Hello();
// 執(zhí)行native代碼,傳入Java端的參數(shù)座泳。與一般的Java并無區(qū)別惠昔。
hello.sayHi(args[0], Integer.parseInt(args[1]));
}
}
說明一下幕与,對于native庫的命名,使用在不同的平臺中镇防,相同的native代碼被編譯成不同的文件類型啦鸣,上面那個(gè)庫:
- Unix: libHelloImpl.so
- Windows:HelloImpl.dll
- Mac:libHelloImpl.jnilib
但是在Java代碼中l(wèi)oadLibrary時(shí),統(tǒng)一引用成“HelloImpl”来氧。另外诫给,lib前綴自動產(chǎn)生,在命名C/C++庫時(shí)并不需要刻意加上lib啦扬。
C/C++端
可以使用JDK中自帶的javah工具來自動生成頭文件中狂。
例如,對于Hello.java文件執(zhí)行以下命令考传。
## 編譯Java源代碼 ./classes是目標(biāo)文件夾
javac -d ./classes/ ./src/com/marakana/jniexamples/Hello.java
cd classes
## 在classes文件夾下運(yùn)行
javah -jni com.marakana.jniexamples.Hello
于是會產(chǎn)生一個(gè)如下com_marakana_jniexamples_Hello.h頭文件吃型。
// 包括一些JNI中會用到的macro和接口。
#include <jni.h>
...
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi (JNIEnv *, jobject, jstring, jint);
接著就可以實(shí)現(xiàn)這個(gè)接口了僚楞。
#include <stdio.h>
#include "com_marakana_jniexamples_Hello.h"
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_Hello_sayHi(JNIEnv *env, jobject obj, jstring who, jint times) {
jint i;
jboolean iscopy;
const char *name;
name = (*env)->GetStringUTFChars(env, who, &iscopy);
for (i = 0; i < times; i++) {
printf("Hello %s\n", name);
}
}
接著就是編譯這個(gè)代碼成庫文件勤晚。
# Linux
gcc -o libHelloImpl.so -lc -shared \
-I/usr/local/jdk1.6.0_03/include \
-I/usr/local/jdk1.6.0_03/include/linux com_marakana_jniexamples_Hello.c
# Mac
gcc -o libHelloImpl.jnilib -lc -shared \
-I/System/Library/Frameworks/JavaVM.framework/Headers com_marakana_jniexamples_Hello.c
測試
把LD_LIBRARY_PATH指向庫文件所在目錄。
# 庫文件在當(dāng)前目錄
export LD_LIBRARY_PATH=.
執(zhí)行
java com.marakana.jniexamples.Hello Student 5
Hello Student
Hello Student
Hello Student
Hello Student
Hello Student
至此泉褐,這個(gè)helloworld程序編寫完成赐写。
native庫的載入
載入方法有兩種:
- 用
System.load
,參數(shù)是庫文件的絕對路徑膜赃,例如Windows下:
System.load("C://Documents and Settings//TestJNI.dll");
- 用
System.loadLibrary
挺邀,參數(shù)是庫的名稱,例如:
System.loadLibrary ("TestJNI");
第二種方式下跳座,庫文件必須在庫索路徑下端铛,可以通過System.getProperty("java.library.path");
打印出搜索路徑。默認(rèn)的搜索路徑因系統(tǒng)而異疲眷,一般包括:
- JRE目錄禾蚕。
- 操作系統(tǒng)庫文件目錄。
可以通過兩種方法改變其值:
- 改寫
java.library.path
的值狂丝。這樣做會完全覆蓋路徑换淆,包括系統(tǒng)的路徑。所以不推薦這么做几颜。
java -Djava.library.path=/jni/library/path
- 通過設(shè)置環(huán)境變量倍试。這樣修改的僅僅是用戶的庫文件路徑,并不會影響系統(tǒng)的路徑蛋哭。
export LB_LIBRARY_PATH=$LB_LIBRARY_PATH:/jni/library/path
進(jìn)階:在C/C++端access Java對像
這個(gè)在之前已經(jīng)有過舉例县习。下面還是以代碼來舉一個(gè)完整的例子來解釋一下。
package com.marakana.jniexamples;
public class InstanceAccess {
// 加載native庫
static {
System.loadLibrary("instanceaccess");
}
// public,會在native代碼中access
public String name;
// public躁愿,會在native代碼中access
public void setName(String name) {
this.name = name;
}
public native void propertyAccess();
public native void methodAccess();
public static void main(String args[]) {
InstanceAccess instanceAccessor = new InstanceAccess();
...
// 這是一個(gè)native方法的call哈蝇,可以跳轉(zhuǎn)到下面的native代碼查看
instanceAccessor.propertyAccess();
// 這是一個(gè)native方法的call,可以跳轉(zhuǎn)到下面的native代碼查看
instanceAccessor.methodAccess();
static {
System.loadLibrary("instanceaccess");
}
}
}
下面是native的代碼
#include <stdio.h>
#include "com_marakana_jniexamples_InstanceAccess.h"
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_propertyAccess(JNIEnv *env, jobject object){
jfieldID fieldId;
jstring jstr;
const char *cString;
// 1. 獲得類引用
jclass class = (*env)->GetObjectClass(env, object);
// 2. 獲得fieldId引用
fieldId = (*env)->GetFieldID(env, class, "name", "Ljava/lang/String;");
if (fieldId == NULL) {
return;
}
// 3. access field值
jstr = (*env)->GetObjectField(env, object, fieldId);
// 4. 數(shù)據(jù)類型轉(zhuǎn)換Java->C/C++
cString = (*env)->GetStringUTFChars(env, jstr, NULL);
if (cString == NULL) {
return;
}
printf("C: value of name before property modification = \"%s\"\n", cString);
(*env)->ReleaseStringUTFChars(env, jstr, cString);
jstr = (*env)->NewStringUTF(env, "Brian");
if (jstr == NULL) {
return;
}
(*env)->SetObjectField(env, object, fieldId, jstr);
}
JNIEXPORT void JNICALL Java_com_marakana_jniexamples_InstanceAccess_methodAccess(JNIEnv *env, jobject object){
// 1. 獲得類引用
jclass class = (*env)->GetObjectClass(env, object);
// 2. 獲得methodId引用
jmethodID methodId = (*env)->GetMethodID(env, class, "setName", "(Ljava/lang/String;)V");
jstring jstr;
if (methodId == NULL) {
return;
}
// 3. 數(shù)據(jù)類型轉(zhuǎn)換Java->C/C++
jstr = (*env)->NewStringUTF(env, "Nick");
// 4. access method
(*env)->CallVoidMethod(env, object, methodId, jstr);
}
可以發(fā)現(xiàn)native兩種access的方法步驟相似:
- GetObjectClass獲得類引用
1.1. Optional:類型轉(zhuǎn)換 - GetFieldID/GetMethodID
- access field/method
3.1. Optional:類型轉(zhuǎn)換
獲取field和method有個(gè)比較方便的工具
// ClassName是一個(gè)類名
javap -s -p ClassName
Android中引用native庫
這里簡單地說一下攘已,具體的可以參考Android官方文檔,細(xì)節(jié)實(shí)在非常龐雜怜跑,就不再贅述了样勃。大致就是
Android.mk 定義native組件;Application.mk 定義怎么在App中使用這些native組件性芬。ndk-build 是一個(gè)官方的腳本文件來編譯源代碼峡眶。更高端的可以使用 toolchain 來自定義編譯過程。
兩個(gè)簡單的方法來添加第三方native庫植锉。
- 直接把so文件拷貝到默認(rèn)文件夾辫樱,src/main/jniLibs
- 在build.gradle里指定位置。
android {
...
source_set {
main {
# so所在文件夾是libs
jniLibs.srcDirs = ["libs"]
...
...
參考資料
JNI Types and Data Structures
Java Fundamentals Tutorial: Java Native Interface (JNI)
Android官方NDK開發(fā)文檔
關(guān)于Android的.so文件你所需要知道的