Android學(xué)習(xí)筆記: JNI

概念整理

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ù)類型,主要有以下這些屑那。

JNI基本數(shù)據(jù)類型

對于Java對象拱镐,有相對應(yīng)的native類型。


JNI引用類型

在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è)映射表呜叫。

Signature類型映射

大家對比上面的映射表,再看下面這個(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庫的載入

載入方法有兩種:

  1. System.load,參數(shù)是庫文件的絕對路徑膜赃,例如Windows下:
System.load("C://Documents and Settings//TestJNI.dll");
  1. System.loadLibrary挺邀,參數(shù)是庫的名稱,例如:
System.loadLibrary ("TestJNI");

第二種方式下跳座,庫文件必須在庫索路徑下端铛,可以通過System.getProperty("java.library.path");打印出搜索路徑。默認(rèn)的搜索路徑因系統(tǒng)而異疲眷,一般包括:

  1. JRE目錄禾蚕。
  2. 操作系統(tǒng)庫文件目錄。

可以通過兩種方法改變其值:

  1. 改寫java.library.path的值狂丝。這樣做會完全覆蓋路徑换淆,包括系統(tǒng)的路徑。所以不推薦這么做几颜。
java -Djava.library.path=/jni/library/path
  1. 通過設(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的方法步驟相似:

  1. GetObjectClass獲得類引用
    1.1. Optional:類型轉(zhuǎn)換
  2. GetFieldID/GetMethodID
  3. 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庫植锉。

  1. 直接把so文件拷貝到默認(rèn)文件夾辫樱,src/main/jniLibs
  2. 在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文件你所需要知道的

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末俊庇,一起剝皮案震驚了整個(gè)濱河市狮暑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辉饱,老刑警劉巖搬男,帶你破解...
    沈念sama閱讀 222,681評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異彭沼,居然都是意外死亡缔逛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評論 3 399
  • 文/潘曉璐 我一進(jìn)店門姓惑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褐奴,“玉大人,你說我怎么就攤上這事于毙《囟” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評論 0 362
  • 文/不壞的土叔 我叫張陵望众,是天一觀的道長匪补。 經(jīng)常有香客問我,道長烂翰,這世上最難降的妖魔是什么夯缺? 我笑而不...
    開封第一講書人閱讀 60,114評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮甘耿,結(jié)果婚禮上踊兜,老公的妹妹穿的比我還像新娘。我一直安慰自己佳恬,他們只是感情好捏境,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評論 6 398
  • 文/花漫 我一把揭開白布于游。 她就那樣靜靜地躺著,像睡著了一般垫言。 火紅的嫁衣襯著肌膚如雪贰剥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評論 1 312
  • 那天筷频,我揣著相機(jī)與錄音蚌成,去河邊找鬼。 笑死凛捏,一個(gè)胖子當(dāng)著我的面吹牛担忧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播坯癣,決...
    沈念sama閱讀 41,170評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼瓶盛,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了示罗?” 一聲冷哼從身側(cè)響起惩猫,我...
    開封第一講書人閱讀 40,116評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鹉勒,沒想到半個(gè)月后帆锋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡禽额,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評論 3 342
  • 正文 我和宋清朗相戀三年锯厢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脯倒。...
    茶點(diǎn)故事閱讀 40,865評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡实辑,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出藻丢,到底是詐尸還是另有隱情剪撬,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布悠反,位于F島的核電站残黑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏斋否。R本人自食惡果不足惜梨水,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望茵臭。 院中可真熱鬧疫诽,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至摩钙,卻和暖如春罢低,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背胖笛。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評論 1 274
  • 我被黑心中介騙來泰國打工奕短, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留湘今,地道東北人瀑焦。 一個(gè)月前我還...
    沈念sama閱讀 49,299評論 3 379
  • 正文 我出身青樓梅鹦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親之斯。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評論 2 361

推薦閱讀更多精彩內(nèi)容