JNI開發(fā)系列閱讀
1. JNI 簡介
1.1 什么是JNI
Java Native Interface(JNI),它允許Java 代碼和其他語言寫的代碼進(jìn)行交互茉兰。JNI 一開始是為了本地已編譯語言旦委,尤其是C 和C++而設(shè)計的,但是它并不妨礙你使用其他語言,只要調(diào)用約定受支持就可以了箍土。
1.2 為什么用JNI
- JNI 擴(kuò)展了Java 虛擬機(jī)的能力,因為Java 不能直接和硬件交互, 不能開發(fā)驅(qū)動
- Java 代碼效率一般要低于C 代碼,而Native code 效率高,因此在數(shù)學(xué)運算,實時渲染的游戲上以及音視頻處理上都需要用Java 調(diào)用C 語言
- 復(fù)用C/C++代碼推捐,C 語言經(jīng)過幾十年的發(fā)展卖陵,已經(jīng)形成了強(qiáng)大的類庫(比如文件壓縮遭顶,人臉識別opencv,7zip泪蔫,ffmpeg 等)棒旗,這些類庫我們沒必要用java 語言重新實現(xiàn)一遍,通過JNI 直接調(diào)用這些類庫即可
- 特殊的業(yè)務(wù)場景撩荣,比如電視铣揉、車載系統(tǒng)、微波爐等跟硬件直接相關(guān)的開發(fā)
2. NDK 簡介
2.1 NDK 產(chǎn)生的背景
Android 平臺從誕生起餐曹,就已經(jīng)支持C逛拱、C++開發(fā)。眾所周知台猴,Android 的SDK 基于Java 實現(xiàn)橘券,這意味著基于Android SDK 進(jìn)行開發(fā)的第三方應(yīng)用都必須使用Java 語言。但這并不等同于“第三方應(yīng)用只能使用Java”卿吐。在Android SDK 首次發(fā)布時,Google 就宣稱其虛擬機(jī)Dalvik 支持JNI 編程方式锋华,也就是第三方應(yīng)用完全可以通過JNI 調(diào)用自己的C 動態(tài)庫嗡官,即在Android 平臺上,“Java+C”的編程方式是一直都可以實現(xiàn)的毯焕。
不過衍腥,Google 也表示磺樱,使用原生SDK 編程相比Dalvik 虛擬機(jī)也有一些劣勢,Android SDK 文檔里婆咸,找不到任何JNI 方面的幫助竹捉。即使第三方應(yīng)用開發(fā)者使用JNI 完成了自己的C 動態(tài)鏈接庫(so)開發(fā),但是so 如何和應(yīng)用程序一起打包成apk 并發(fā)布尚骄?這里面也存在技術(shù)障礙块差。比如程序更加復(fù)雜,兼容性難以保障倔丈,無法訪問Framework API憨闰,Debug 難度更大等。開發(fā)者需要自行斟酌使用需五。
于是NDK 就應(yīng)運而生了鹉动。NDK 全稱是Native Development Kit。
NDK 的發(fā)布宏邮,使“Java+C”的開發(fā)方式終于轉(zhuǎn)正泽示,成為官方支持的開發(fā)方式。NDK 將是Android 平臺支持C 開發(fā)的開端蜜氨。
2.2 為什么使用NDK
- 代碼的保護(hù)械筛。由于apk 的java 層代碼很容易被反編譯,而C/C++庫反編譯難度較大
- 可以方便地使用現(xiàn)存的開源庫记劝。大部分現(xiàn)存的開源庫都是用C/C++代碼編寫的
- 提高程序的執(zhí)行效率变姨。將要求高性能的應(yīng)用邏輯使用C 開發(fā),從而提高應(yīng)用程序的執(zhí)行效率
- 便于移植厌丑。用C/C++寫得庫可以方便在其他的嵌入式平臺上再次使用
2.3 NDK 簡介
2.3.1 NDK 是一系列工具的集合
NDK 提供了一系列的工具定欧,幫助開發(fā)者快速開發(fā)C(或C++)的動態(tài)庫,并能自動將so 和java 應(yīng)用一起打包成apk怒竿。NDK 集成了交叉編譯器砍鸠,并提供了相應(yīng)的mk 文件隔離CPU、平臺耕驰、ABI 等差異爷辱,開發(fā)人員只需要簡單修改mk 文件(指出“哪些文件需要編譯”、“編譯特性要求”等)朦肘,就可以創(chuàng)建出so饭弓。
2.3.2 NDK 提供了一份穩(wěn)定、功能有限的API 頭文件聲明
Google 明確聲明該API 是穩(wěn)定的媒抠,在后續(xù)所有版本中都穩(wěn)定支持當(dāng)前發(fā)布的API弟断。從該版本的NDK中看出,這些API 支持的功能非常有限趴生,包含有:C 標(biāo)準(zhǔn)庫(libc)阀趴、標(biāo)準(zhǔn)數(shù)學(xué)庫(libm)昏翰、壓縮庫(libz)、Log 庫(liblog)刘急。
3. NDK 的安裝
3.1 NDK 的下載
3.2 將NDK 解壓到一個不包含空格和中文的目錄下
本人將NDK 解壓在D:\software\ndkr9\android-ndk-r9b 中棚菊。
3.3 NDK 目錄結(jié)構(gòu)說明
自定義好組合控件之后,之前的activity_setting.xml 中的代碼就可以進(jìn)行簡化叔汁,具體如下所示:
- build:該目錄存放的使用NDK 的mk 腳本统求,mk 腳本指定了編譯參數(shù)
- docs:該目錄存放的是NDK 的使用幫助文檔
- platforms:這里面存放的是與各個Android 版本相關(guān)的平臺(x86,arm攻柠,mips)相關(guān)C 語言庫和頭文件
- prebuilt:預(yù)編譯工作目錄
- samples:存放的是演示程序
- sources:存放的是NDK 工具鏈的C 語言源碼
- tests:測試相關(guān)的文件
- toolchains:工具鏈球订,存放了三種架構(gòu)的靜態(tài)庫等文件
- ndk-build.cmd:Window 平臺使用NDK 的命令
- ndk-build:Linux 平臺使用NDK 的命令
4. JNI 入門
下面通過一個簡單的JNI 案例來演示如何使用JNI 編程。
1)創(chuàng)建一個新的Android 工程《JNI 入門》瑰钮,工程的最終目錄結(jié)構(gòu)如下圖所示冒滩。
2)在MainActivity.java 類中定義一個native 方法
//定義一個native 方法,意思是該方法的具體實現(xiàn)交給C 語言實現(xiàn)
public native String helloC();
3)在工程跟目錄下創(chuàng)建一個文件夾jni浪谴,該目錄名稱是約定(約定優(yōu)于配置)好的开睡,不能是其他名字。
4)在jni 目錄下創(chuàng)建hello.c 源文件苟耻,文件名可以按照見名知意的規(guī)則來創(chuàng)建篇恒。hello.c 代碼清單如下。
#include<stdio.h>//引入頭文件
//引入jni.h jni.h 文件里面定義了jni 的規(guī)范凶杖,jni.h 在ndk 的目錄中找到胁艰,然后放到當(dāng)前工程中的jni目錄下即可
#include<jni.h>
//定義在MainActivity.java 類中的helloC 對應(yīng)的C 語言函數(shù)
jstring Java_com_itheima_jnihello_MainActivity_helloC(JNIEnv* env, jobject obj) {
char* str = "hello from C";
//調(diào)用jni.h 中定義的創(chuàng)建字符串函數(shù)
jstring string = (*(*env)).NewStringUTF(env, str);
return string;
Tips:上面的代碼雖然簡單但是關(guān)于jni.h 頭文件和方法名必須單獨說明。
- jni.h 頭文件位于NDK 安裝目錄下/platforms/android-*/(某平臺)/usr/include 目錄中智蝠,如下圖
上面的某平臺指CPU 的三種架構(gòu)如下圖腾么。我們選擇任意一架構(gòu)皆可,但是對于手機(jī)來說CPU 用arm架構(gòu)的最多杈湾,x86 次之解虱,mips 架構(gòu)最少。
- JNI 中C 源文件方法名的命名規(guī)則
這里的命名規(guī)則指用于跟java 文件中native 方法對應(yīng)的C 語言方法漆撞,而C 語言中的其他方法命名只要符合C 語言規(guī)則就行殴泰。
jstring Java_com_itheima_jnihello_MainActivity_helloC(JNIEnv* env, jobject obj)
jstring 是方法返回值類型,我們可以把jstring 看成是java 中String 跟C 語言中char*類型的一個中間轉(zhuǎn)換類型浮驳,java 跟C 語言的數(shù)據(jù)類型是不一樣的悍汛,他們之間要想互相調(diào)用就必須通過一種中介來實現(xiàn),這個中介就是在jni.h 頭文件中定義的至会。關(guān)于更多的轉(zhuǎn)換類型员凝,在本文檔的第2 章會有更詳細(xì)的說明。
方法名第一個字母必須是Java奋献,首單詞大寫健霹,然后下劃線,然后是將該方法所在的包瓶蚂、類糖埋、方法用“”連接起來,比如com.itheima.jnihello.MainActivity 類中的helloC 方法,轉(zhuǎn)變成C 語言中的方法名為Java_com_itheima_jnihello_MainActivity_helloC窃这。
方法的形參有兩個是必須的也就是不管java 中的方法是否有形參瞳别,但是C 語言中對應(yīng)的方法必須有JNIEnv* env,和jobject obj,如果java 方法中還用其他形參杭攻,那么在C 語言中嚴(yán)格按照順序排在jobject obj參數(shù)的后面即可祟敛。
上面的env 代表指向JVM 的指針,obj 是調(diào)用該方法的java 對象兆解。
5)使用NDK 工具將hello.c 編譯成hello.so 文件
為了方便直接在控制臺中使用NDK 工具的ndk-build.cmd 命令馆铁,我們首先將ndk-build.cmd 所在的目錄設(shè)置成系統(tǒng)環(huán)境變量。環(huán)境變量配置好以后锅睛,在命令行中輸入ndk-build.cmd 會有如下提示:
將當(dāng)前目錄切換到hello.c 所在的工程目錄埠巨,這時候如果直接輸入ndk-build.cmd 那么會出現(xiàn)如下異常:
出現(xiàn)這種錯誤時因為我們并沒有告訴ndk 我們要將那個C 語言源代碼編譯成目標(biāo)文件。為了告訴ndk要將那個C 源文件編譯成目標(biāo)文件现拒,我們需要在工程中的jni 目錄中添加Android.mk 配置文件辣垒。
6)在當(dāng)前工程的jni 目錄下添加Android.mk 配置文件,該配置文件可以從ndk 安裝目錄的實例代碼中拷貝印蔬,然后修改勋桶。
Android.mk 文件清單如下,我們只需要修改LOCAL_MODULE 和LOCAL_SRC_FILES 兩個參數(shù)即可侥猬。LOCAL_MODULE 參數(shù)是指定編譯后的目標(biāo)文件的名稱例驹,其實編譯好的目標(biāo)文件名為libhello.so,LOCAL_SRC_FILES 指定了要編譯的源文件陵究。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c
include $(BUILD_SHARED_LIBRARY)
7)在cmd 中眠饮,將當(dāng)前目錄切換到hello.c 所在目錄,然后重新執(zhí)行ndk-build.cmd 命令铜邮,這次成功編譯仪召,cmd 顯示效果如下圖所示。
查看項目目錄結(jié)構(gòu)松蒜,發(fā)現(xiàn)在libs 目錄中多了兩個文件夾armeabi 和x86扔茅,這兩個文件夾下分別包含了一個libhello.so 動態(tài)鏈接庫。這也代表著當(dāng)前工程中的動態(tài)庫支持arm 架構(gòu)和x86 架構(gòu)的cpu秸苗。
Tips:可能你的并沒有同時生成這兩個文件召娜,是因為我的工程中引入了Application.mk 文件,因此你需要引入該文件惊楼。
Application.mk 文件清單:
# Build both ARMv5TE and ARMv7-A machine code.
APP_ABI := armeabi x86
8)該清單其實只有一行內(nèi)容玖瘸,第一行是注釋秸讹。APP_ABI 參數(shù)指定要生成的目標(biāo)文件支持的平臺都有哪些,默認(rèn)是armeabi 如果想支持多個平臺只需要空一格然后寫出其他平臺名字即可雅倒。在MainActivity.java 中調(diào)用C 語言
public class MainActivity extends Activity {
//加載libhello.so 動態(tài)庫璃诀,但是我們加載的時候必須去掉lib 和后綴
static{
System.loadLibrary("hello");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//定義一個native 方法,意思是該方法的具體實現(xiàn)交給C 語言實現(xiàn)
public native String helloC();
//點擊按鈕調(diào)用C 語言方法
public void click(View view){
Toast.makeText(this, helloC(), 1).show();
}
}
運行上面工程蔑匣,效果如下:
Tips:如果我們編譯的arm 平臺的so 文件劣欢,但是卻部署到了x86 平臺的模擬器上,那么運行的時候會報找不到libhello.so 的異常裁良。
5. JNI 規(guī)范
5.1 JNI 數(shù)據(jù)類型和數(shù)據(jù)結(jié)構(gòu)
1)基本數(shù)據(jù)類型
JNI 基本類型和本地等效類型的對應(yīng)表格如下:
2)引用類型凿将,JNI 還包含了很多對應(yīng)于不同Java 對象的引用類型,JNI 引用類型的組織層次如下圖所示:
5.2 JNI 接口函數(shù)命名方式
設(shè)置向?qū)ФetUpActivity2.java 的代碼邏輯如下所示价脾,設(shè)置向?qū)Ф膱D形化界面如2-20 所示牧抵。
5.2.1 類型簽名
Java 虛擬機(jī)的類型簽名如下:
類型簽名 | Java 類型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
Lfully-qulitied-class; | 全限定類 |
[type type[] | 數(shù)組 |
(argtypes)rettype | 方法類型 |
例如咐旧,Java 方法int feet(int n, String s,int [] arr)的類型簽名如下:
(ILJava/lang/String;[I)I
圓括號里面為參數(shù)情龄,I 表示第一個參數(shù)int 型,LJava/lang/String;表示第二個參數(shù)為全限定Java.lang.String類型肥败,[I 表示第三個參數(shù)為int 型的數(shù)組座硕,圓括號后面為返回值類型弛作,I 表示返回值為int 型
5.2.2 一般函數(shù)的JNI 接口函數(shù)命名方式
一般JNI 接口函數(shù)命名如下:Java_包名類名方法名。
例如:某工程下com/itheima 包下MainActivity 類的int getIntFromC()方法的C 語言實現(xiàn)函數(shù)命名如下:
jint Java_com_itheima_MainActivity_getIntFromC(JNIEnv* env,jobject obj)
其中华匾,包名所包含的“/”應(yīng)全部以下劃線替代映琳,其本地實現(xiàn)的參數(shù)和返回值也應(yīng)轉(zhuǎn)換為JNI 類型。
5.2.3 重載函數(shù)的JNI 接口函數(shù)命名方式
重載函數(shù)的JNI 實現(xiàn)在一般函數(shù)的JNI 實現(xiàn)之外蜘拉,還應(yīng)添加上類型簽名以作為同名函數(shù)之間的區(qū)別萨西,其接口函數(shù)命名如下:Java_包名類名方法名_參數(shù)簽名。
例如:某工程下com/itheima 包下MainActivity 類的int getIntFromC(int n, String s,int [] arr)方法的C 語言實現(xiàn)函數(shù)命名如下:
jint Java_com_itheima_MainActivity_getIntFromC_ILJava_lang_String23I
(JNIEnv* env, jobject obj, jint n, jstring s, jintarray arr)
JNI 在函數(shù)命名時采用名字?jǐn)_亂方案旭旭,以保證所有的Unicode 字符都能轉(zhuǎn)換為有效的C 函數(shù)名谎脯,所有的“/”,無論是包名中的還是全限定類名中的,均使用“_”代替持寄,用_0,?,_9 來代替轉(zhuǎn)義字符源梭,如下:
轉(zhuǎn)義字符序列 | 表示 |
---|---|
_0XXXX | Unicode 字符XXXX |
_1 | 字符“_” |
_2 | 簽名中的字符“;” |
_3 | 簽名中的字符“[” |
5.3 JNI 函數(shù)與API
設(shè)置向?qū)齋etUpActivity3.java 的代碼邏輯如下所示稍味,設(shè)置向?qū)膱D形化界面如2-21 所示废麻。
在本文檔中我們所主要需要關(guān)心的是C/C++數(shù)據(jù)類型與JNI 本地類型之間的轉(zhuǎn)化過程,這個過程某些數(shù)據(jù)的轉(zhuǎn)換需要使用JNIEnv 對象的一系列方法來完成模庐。
5.3.1 jstring 轉(zhuǎn)換為C 風(fēng)格字符串
char* test = (char)(env)->GetStringUTFChars(env,jstring,NULL);
使用完畢后烛愧,應(yīng)調(diào)用:
(*env)->ReleaseStringUTFChars(env, jstring, test);
釋放資源。
5.3.2 C 風(fēng)格字符串轉(zhuǎn)換為jstring
char charStr[50];
jstring jstr;
jstr = env -> NewStringUTF(charStr);
5.3.3 C 語言中獲取的一段char的buffer 傳遞給Java*
在jni 中new 一個byte 數(shù)組,然后使用
(*env)->SetByteArrayRegion(env, bytearray, 0, len, buffer) 操作將buffer 拷貝到數(shù)組中怜姿。這種方式主要是針對buffer 中存在“\0”的情況慎冤,如果以C 風(fēng)格字符串的方式讀入,就會損失“\0”之后的字符沧卢。
5.3.4 數(shù)組操作
JNI 函數(shù) | 功能 |
---|---|
GetArrayLength | 返回數(shù)組中的元素數(shù) |
NewObjectArray | 創(chuàng)建一個指定長度的原始數(shù)據(jù)類型數(shù)組 |
GetObjectArrayElement | 返回Object 數(shù)組的元素 |
SetObjectArrayElement | 設(shè)置Object 數(shù)組的元素 |
GetObjectArrayRegion | 將原始數(shù)據(jù)類型數(shù)組中的內(nèi)容拷貝到預(yù)先分配好的內(nèi)存緩存中 |
SetObjectArrayRegion | 設(shè)置緩存中數(shù)組的值 |
ReleaseObjectArrayRegion | 釋放GetObjectArrayRegion 分配的內(nèi)存 |
Tips:對int粪薛,char 等基本數(shù)據(jù)類型的數(shù)組操作,將相關(guān)Object 名稱替換為對應(yīng)基本數(shù)據(jù)類型名稱即為相關(guān)函數(shù)搏恤。
數(shù)組操作的方法選擇基于使用者的需求而定,如果使用者需要在內(nèi)存中拷貝數(shù)組并對其進(jìn)行操作那么一般使用GetObjectArrayRegion 和SetObjectArrayRegion 函數(shù)湃交,否則一般使用SetObjectArrayElement 和GetObjectArrayElement 函數(shù)熟空。
6. 案例-銀行登錄系統(tǒng)
需求:假設(shè)銀行的登陸模塊是用C 語言來編寫的,但是我們的Android 應(yīng)用想登陸銀行系統(tǒng)搞莺,那么就需要通過JNI 來實現(xiàn)了息罗。
創(chuàng)建一個新Android 工程《建行客戶端》,工程目錄結(jié)構(gòu)如下圖才沧。
在工程中創(chuàng)建jni 文件夾迈喉,然后將jni.h、Android.mk温圆、Application.mk 從JNI 入門工程拷貝進(jìn)去挨摸。在jni 目錄下創(chuàng)建login.c 文件,在該文件中實現(xiàn)登錄業(yè)務(wù)邏輯岁歉。代碼清單如下得运。
#include<stdio.h>
//系統(tǒng)在查找投文件的時候""中的文件會去本地搜索,<>中的文件會去系統(tǒng)目錄中搜索锅移,因為jni.h 在當(dāng)前目錄中
所以用""將jni.h 引起來熔掺,可以加快搜索速度
#include"jni.h"
int login(int card,int pwd){
//真實的業(yè)務(wù)邏輯要復(fù)雜的多,這里只簡單的返回銀行卡號和密碼號
return card+pwd;
}
jint Java_com_itheima_ccb_MainActivity_login(JNIEnv* env,jobject obj,jint card,jint
pwd){
return login(card,pwd);
}
是用ndk 工具非剃,將login.c 編譯成動態(tài)庫文件置逻。編譯前修改Android.mk 文件的LOCAL_SRC_FILES := login.c
編寫在MainActivity.java 類
public class MainActivity extends Activity {
static{
System.loadLibrary("login-jni");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public native int login(int card,int pwd);
public void login(View view){
EditText et_card = (EditText) findViewById(R.id.et_card);
EditText et_pwd = (EditText) findViewById(R.id.et_pwd);
int card = Integer.valueOf(et_card.getText().toString());
int pwd = Integer.valueOf(et_pwd.getText().toString());
int result = login(card, pwd);
Toast.makeText(this, ""+result, 1).show();
}
}
布局文件比較簡單,這里就不再給出备绽。運行上面的代碼券坞,運行結(jié)果如下:
7. CDT 插件的安裝
7.1 CDT 簡介
CDT 項目致力于為Eclipse 平臺提供功能完全的C/C++ 集成開發(fā)環(huán)境(Integrated DevelopmentEnvironment,IDE)疯坤。CDT 是完全用Java 實現(xiàn)的開放源碼項目(根據(jù)Common Public License 特許的)报慕,它作為Eclipse SDK 平臺的一組插件。這些插件將C/C++ 透視圖添加到Eclipse 工作臺(Workbench)中压怠,現(xiàn)在后者可以用許多視圖和向?qū)б约案呒壘庉嫼驼{(diào)試支持來支持C/C++ 開發(fā)眠冈。
7.2 CDT 的下載
CDT 插件可以通過eclipse 的在線安裝,但是受限于跨國家網(wǎng)絡(luò)訪問,一般不是很好用蜗顽。因此這里我主要給大家說的是如何離線安裝布卡。
下載CDT 離線安裝包。針對不同版本eclipse 的cdt 安裝包如下雇盖,大家可以從我的百度網(wǎng)盤上直接下載忿等。考慮到我們大部分都用的最新的ADT 因此建議選擇8.5.0 版本的CDT崔挖。
選擇eclipse 的Help->Install New Software...贸街,彈出如下對話框
點擊Add 按鈕,在彈出的對話框中輸入Name狸相。在Location 欄如果輸入一個http 地址是讓eclipse自動從網(wǎng)絡(luò)上下載安裝薛匪,這里我們點擊Archive 按鈕找到我們事先下載好的離線安裝包。然后點擊OK脓鹃。
將CDT 所有的插件勾選上逸尖,同時將最下面的聯(lián)網(wǎng)檢查更新去掉勾選,然后點擊Next瘸右,直到Finish娇跟。
安裝好以后在File->New->Other 中會有C/C++選項,如下圖太颤。
在Open Perspective 中也多了C/C++視圖可選項苞俘,如下圖。
安裝好以后栋齿,我們就可以在eclipse 中開發(fā)我們的C/C++工程了苗胀。不過對我們Android 開發(fā)人員來說用到的機(jī)會不是很多。就算是開發(fā)C/C++工程瓦堵,大多數(shù)程序員也不會選擇在eclipse 平臺上進(jìn)行開發(fā)基协。Eclipse更多的是專注于Java 語言項目的開發(fā),比如JavaEE菇用、Android澜驮。