JNI開發系列閱讀
1. JNI 簡介
1.1 什么是JNI
Java Native Interface(JNI),它允許Java 代碼和其他語言寫的代碼進行交互。JNI 一開始是為了本地已編譯語言,尤其是C 和C++而設計的,但是它并不妨礙你使用其他語言,只要調用約定受支持就可以了。
1.2 為什么用JNI
- JNI 擴展了Java 虛擬機的能力,因為Java 不能直接和硬件交互, 不能開發驅動
- Java 代碼效率一般要低于C 代碼,而Native code 效率高,因此在數學運算,實時渲染的游戲上以及音視頻處理上都需要用Java 調用C 語言
- 復用C/C++代碼,C 語言經過幾十年的發展,已經形成了強大的類庫(比如文件壓縮,人臉識別opencv,7zip,ffmpeg 等),這些類庫我們沒必要用java 語言重新實現一遍,通過JNI 直接調用這些類庫即可
- 特殊的業務場景,比如電視、車載系統、微波爐等跟硬件直接相關的開發
2. NDK 簡介
2.1 NDK 產生的背景
Android 平臺從誕生起,就已經支持C、C++開發。眾所周知,Android 的SDK 基于Java 實現,這意味著基于Android SDK 進行開發的第三方應用都必須使用Java 語言。但這并不等同于“第三方應用只能使用Java”。在Android SDK 首次發布時,Google 就宣稱其虛擬機Dalvik 支持JNI 編程方式,也就是第三方應用完全可以通過JNI 調用自己的C 動態庫,即在Android 平臺上,“Java+C”的編程方式是一直都可以實現的。
不過,Google 也表示,使用原生SDK 編程相比Dalvik 虛擬機也有一些劣勢,Android SDK 文檔里,找不到任何JNI 方面的幫助。即使第三方應用開發者使用JNI 完成了自己的C 動態鏈接庫(so)開發,但是so 如何和應用程序一起打包成apk 并發布?這里面也存在技術障礙。比如程序更加復雜,兼容性難以保障,無法訪問Framework API,Debug 難度更大等。開發者需要自行斟酌使用。
于是NDK 就應運而生了。NDK 全稱是Native Development Kit。
NDK 的發布,使“Java+C”的開發方式終于轉正,成為官方支持的開發方式。NDK 將是Android 平臺支持C 開發的開端。
2.2 為什么使用NDK
- 代碼的保護。由于apk 的java 層代碼很容易被反編譯,而C/C++庫反編譯難度較大
- 可以方便地使用現存的開源庫。大部分現存的開源庫都是用C/C++代碼編寫的
- 提高程序的執行效率。將要求高性能的應用邏輯使用C 開發,從而提高應用程序的執行效率
- 便于移植。用C/C++寫得庫可以方便在其他的嵌入式平臺上再次使用
2.3 NDK 簡介
2.3.1 NDK 是一系列工具的集合
NDK 提供了一系列的工具,幫助開發者快速開發C(或C++)的動態庫,并能自動將so 和java 應用一起打包成apk。NDK 集成了交叉編譯器,并提供了相應的mk 文件隔離CPU、平臺、ABI 等差異,開發人員只需要簡單修改mk 文件(指出“哪些文件需要編譯”、“編譯特性要求”等),就可以創建出so。
2.3.2 NDK 提供了一份穩定、功能有限的API 頭文件聲明
Google 明確聲明該API 是穩定的,在后續所有版本中都穩定支持當前發布的API。從該版本的NDK中看出,這些API 支持的功能非常有限,包含有:C 標準庫(libc)、標準數學庫(libm)、壓縮庫(libz)、Log 庫(liblog)。
3. NDK 的安裝
3.1 NDK 的下載
3.2 將NDK 解壓到一個不包含空格和中文的目錄下
本人將NDK 解壓在D:\software\ndkr9\android-ndk-r9b 中。
3.3 NDK 目錄結構說明
自定義好組合控件之后,之前的activity_setting.xml 中的代碼就可以進行簡化,具體如下所示:
- build:該目錄存放的使用NDK 的mk 腳本,mk 腳本指定了編譯參數
- docs:該目錄存放的是NDK 的使用幫助文檔
- platforms:這里面存放的是與各個Android 版本相關的平臺(x86,arm,mips)相關C 語言庫和頭文件
- prebuilt:預編譯工作目錄
- samples:存放的是演示程序
- sources:存放的是NDK 工具鏈的C 語言源碼
- tests:測試相關的文件
- toolchains:工具鏈,存放了三種架構的靜態庫等文件
- ndk-build.cmd:Window 平臺使用NDK 的命令
- ndk-build:Linux 平臺使用NDK 的命令
4. JNI 入門
下面通過一個簡單的JNI 案例來演示如何使用JNI 編程。
1)創建一個新的Android 工程《JNI 入門》,工程的最終目錄結構如下圖所示。
2)在MainActivity.java 類中定義一個native 方法
//定義一個native 方法,意思是該方法的具體實現交給C 語言實現
public native String helloC();
3)在工程跟目錄下創建一個文件夾jni,該目錄名稱是約定(約定優于配置)好的,不能是其他名字。
4)在jni 目錄下創建hello.c 源文件,文件名可以按照見名知意的規則來創建。hello.c 代碼清單如下。
#include<stdio.h>//引入頭文件
//引入jni.h jni.h 文件里面定義了jni 的規范,jni.h 在ndk 的目錄中找到,然后放到當前工程中的jni目錄下即可
#include<jni.h>
//定義在MainActivity.java 類中的helloC 對應的C 語言函數
jstring Java_com_itheima_jnihello_MainActivity_helloC(JNIEnv* env, jobject obj) {
char* str = "hello from C";
//調用jni.h 中定義的創建字符串函數
jstring string = (*(*env)).NewStringUTF(env, str);
return string;
Tips:上面的代碼雖然簡單但是關于jni.h 頭文件和方法名必須單獨說明。
- jni.h 頭文件位于NDK 安裝目錄下/platforms/android-*/(某平臺)/usr/include 目錄中,如下圖
上面的某平臺指CPU 的三種架構如下圖。我們選擇任意一架構皆可,但是對于手機來說CPU 用arm架構的最多,x86 次之,mips 架構最少。
- JNI 中C 源文件方法名的命名規則
這里的命名規則指用于跟java 文件中native 方法對應的C 語言方法,而C 語言中的其他方法命名只要符合C 語言規則就行。
jstring Java_com_itheima_jnihello_MainActivity_helloC(JNIEnv* env, jobject obj)
jstring 是方法返回值類型,我們可以把jstring 看成是java 中String 跟C 語言中char*類型的一個中間轉換類型,java 跟C 語言的數據類型是不一樣的,他們之間要想互相調用就必須通過一種中介來實現,這個中介就是在jni.h 頭文件中定義的。關于更多的轉換類型,在本文檔的第2 章會有更詳細的說明。
方法名第一個字母必須是Java,首單詞大寫,然后下劃線,然后是將該方法所在的包、類、方法用“”連接起來,比如com.itheima.jnihello.MainActivity 類中的helloC 方法,轉變成C 語言中的方法名為Java_com_itheima_jnihello_MainActivity_helloC。
方法的形參有兩個是必須的也就是不管java 中的方法是否有形參,但是C 語言中對應的方法必須有JNIEnv* env,和jobject obj,如果java 方法中還用其他形參,那么在C 語言中嚴格按照順序排在jobject obj參數的后面即可。
上面的env 代表指向JVM 的指針,obj 是調用該方法的java 對象。
5)使用NDK 工具將hello.c 編譯成hello.so 文件
為了方便直接在控制臺中使用NDK 工具的ndk-build.cmd 命令,我們首先將ndk-build.cmd 所在的目錄設置成系統環境變量。環境變量配置好以后,在命令行中輸入ndk-build.cmd 會有如下提示:
將當前目錄切換到hello.c 所在的工程目錄,這時候如果直接輸入ndk-build.cmd 那么會出現如下異常:
出現這種錯誤時因為我們并沒有告訴ndk 我們要將那個C 語言源代碼編譯成目標文件。為了告訴ndk要將那個C 源文件編譯成目標文件,我們需要在工程中的jni 目錄中添加Android.mk 配置文件。
6)在當前工程的jni 目錄下添加Android.mk 配置文件,該配置文件可以從ndk 安裝目錄的實例代碼中拷貝,然后修改。
Android.mk 文件清單如下,我們只需要修改LOCAL_MODULE 和LOCAL_SRC_FILES 兩個參數即可。LOCAL_MODULE 參數是指定編譯后的目標文件的名稱,其實編譯好的目標文件名為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 中,將當前目錄切換到hello.c 所在目錄,然后重新執行ndk-build.cmd 命令,這次成功編譯,cmd 顯示效果如下圖所示。
查看項目目錄結構,發現在libs 目錄中多了兩個文件夾armeabi 和x86,這兩個文件夾下分別包含了一個libhello.so 動態鏈接庫。這也代表著當前工程中的動態庫支持arm 架構和x86 架構的cpu。
Tips:可能你的并沒有同時生成這兩個文件,是因為我的工程中引入了Application.mk 文件,因此你需要引入該文件。
Application.mk 文件清單:
# Build both ARMv5TE and ARMv7-A machine code.
APP_ABI := armeabi x86
8)該清單其實只有一行內容,第一行是注釋。APP_ABI 參數指定要生成的目標文件支持的平臺都有哪些,默認是armeabi 如果想支持多個平臺只需要空一格然后寫出其他平臺名字即可。在MainActivity.java 中調用C 語言
public class MainActivity extends Activity {
//加載libhello.so 動態庫,但是我們加載的時候必須去掉lib 和后綴
static{
System.loadLibrary("hello");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
//定義一個native 方法,意思是該方法的具體實現交給C 語言實現
public native String helloC();
//點擊按鈕調用C 語言方法
public void click(View view){
Toast.makeText(this, helloC(), 1).show();
}
}
運行上面工程,效果如下:
Tips:如果我們編譯的arm 平臺的so 文件,但是卻部署到了x86 平臺的模擬器上,那么運行的時候會報找不到libhello.so 的異常。
5. JNI 規范
5.1 JNI 數據類型和數據結構
1)基本數據類型
JNI 基本類型和本地等效類型的對應表格如下:
2)引用類型,JNI 還包含了很多對應于不同Java 對象的引用類型,JNI 引用類型的組織層次如下圖所示:
5.2 JNI 接口函數命名方式
設置向導二SetUpActivity2.java 的代碼邏輯如下所示,設置向導二的圖形化界面如2-20 所示。
5.2.1 類型簽名
Java 虛擬機的類型簽名如下:
類型簽名 | Java 類型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
Lfully-qulitied-class; | 全限定類 |
[type type[] | 數組 |
(argtypes)rettype | 方法類型 |
例如,Java 方法int feet(int n, String s,int [] arr)的類型簽名如下:
(ILJava/lang/String;[I)I
圓括號里面為參數,I 表示第一個參數int 型,LJava/lang/String;表示第二個參數為全限定Java.lang.String類型,[I 表示第三個參數為int 型的數組,圓括號后面為返回值類型,I 表示返回值為int 型
5.2.2 一般函數的JNI 接口函數命名方式
一般JNI 接口函數命名如下:Java_包名類名方法名。
例如:某工程下com/itheima 包下MainActivity 類的int getIntFromC()方法的C 語言實現函數命名如下:
jint Java_com_itheima_MainActivity_getIntFromC(JNIEnv* env,jobject obj)
其中,包名所包含的“/”應全部以下劃線替代,其本地實現的參數和返回值也應轉換為JNI 類型。
5.2.3 重載函數的JNI 接口函數命名方式
重載函數的JNI 實現在一般函數的JNI 實現之外,還應添加上類型簽名以作為同名函數之間的區別,其接口函數命名如下:Java_包名類名方法名_參數簽名。
例如:某工程下com/itheima 包下MainActivity 類的int getIntFromC(int n, String s,int [] arr)方法的C 語言實現函數命名如下:
jint Java_com_itheima_MainActivity_getIntFromC_ILJava_lang_String23I
(JNIEnv* env, jobject obj, jint n, jstring s, jintarray arr)
JNI 在函數命名時采用名字擾亂方案,以保證所有的Unicode 字符都能轉換為有效的C 函數名,所有的“/”,無論是包名中的還是全限定類名中的,均使用“_”代替,用_0,?,_9 來代替轉義字符,如下:
轉義字符序列 | 表示 |
---|---|
_0XXXX | Unicode 字符XXXX |
_1 | 字符“_” |
_2 | 簽名中的字符“;” |
_3 | 簽名中的字符“[” |
5.3 JNI 函數與API
設置向導三SetUpActivity3.java 的代碼邏輯如下所示,設置向導三的圖形化界面如2-21 所示。
在本文檔中我們所主要需要關心的是C/C++數據類型與JNI 本地類型之間的轉化過程,這個過程某些數據的轉換需要使用JNIEnv 對象的一系列方法來完成。
5.3.1 jstring 轉換為C 風格字符串
char* test = (char)(env)->GetStringUTFChars(env,jstring,NULL);
使用完畢后,應調用:
(*env)->ReleaseStringUTFChars(env, jstring, test);
釋放資源。
5.3.2 C 風格字符串轉換為jstring
char charStr[50];
jstring jstr;
jstr = env -> NewStringUTF(charStr);
5.3.3 C 語言中獲取的一段char的buffer 傳遞給Java*
在jni 中new 一個byte 數組,然后使用
(*env)->SetByteArrayRegion(env, bytearray, 0, len, buffer) 操作將buffer 拷貝到數組中。這種方式主要是針對buffer 中存在“\0”的情況,如果以C 風格字符串的方式讀入,就會損失“\0”之后的字符。
5.3.4 數組操作
JNI 函數 | 功能 |
---|---|
GetArrayLength | 返回數組中的元素數 |
NewObjectArray | 創建一個指定長度的原始數據類型數組 |
GetObjectArrayElement | 返回Object 數組的元素 |
SetObjectArrayElement | 設置Object 數組的元素 |
GetObjectArrayRegion | 將原始數據類型數組中的內容拷貝到預先分配好的內存緩存中 |
SetObjectArrayRegion | 設置緩存中數組的值 |
ReleaseObjectArrayRegion | 釋放GetObjectArrayRegion 分配的內存 |
Tips:對int,char 等基本數據類型的數組操作,將相關Object 名稱替換為對應基本數據類型名稱即為相關函數。
數組操作的方法選擇基于使用者的需求而定,如果使用者需要在內存中拷貝數組并對其進行操作那么一般使用GetObjectArrayRegion 和SetObjectArrayRegion 函數,否則一般使用SetObjectArrayElement 和GetObjectArrayElement 函數。
6. 案例-銀行登錄系統
需求:假設銀行的登陸模塊是用C 語言來編寫的,但是我們的Android 應用想登陸銀行系統,那么就需要通過JNI 來實現了。
創建一個新Android 工程《建行客戶端》,工程目錄結構如下圖。
在工程中創建jni 文件夾,然后將jni.h、Android.mk、Application.mk 從JNI 入門工程拷貝進去。在jni 目錄下創建login.c 文件,在該文件中實現登錄業務邏輯。代碼清單如下。
#include<stdio.h>
//系統在查找投文件的時候""中的文件會去本地搜索,<>中的文件會去系統目錄中搜索,因為jni.h 在當前目錄中
所以用""將jni.h 引起來,可以加快搜索速度
#include"jni.h"
int login(int card,int pwd){
//真實的業務邏輯要復雜的多,這里只簡單的返回銀行卡號和密碼號
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 編譯成動態庫文件。編譯前修改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();
}
}
布局文件比較簡單,這里就不再給出。運行上面的代碼,運行結果如下:
7. CDT 插件的安裝
7.1 CDT 簡介
CDT 項目致力于為Eclipse 平臺提供功能完全的C/C++ 集成開發環境(Integrated DevelopmentEnvironment,IDE)。CDT 是完全用Java 實現的開放源碼項目(根據Common Public License 特許的),它作為Eclipse SDK 平臺的一組插件。這些插件將C/C++ 透視圖添加到Eclipse 工作臺(Workbench)中,現在后者可以用許多視圖和向導以及高級編輯和調試支持來支持C/C++ 開發。
7.2 CDT 的下載
CDT 插件可以通過eclipse 的在線安裝,但是受限于跨國家網絡訪問,一般不是很好用。因此這里我主要給大家說的是如何離線安裝。
下載CDT 離線安裝包。針對不同版本eclipse 的cdt 安裝包如下,大家可以從我的百度網盤上直接下載。考慮到我們大部分都用的最新的ADT 因此建議選擇8.5.0 版本的CDT。
選擇eclipse 的Help->Install New Software...,彈出如下對話框
點擊Add 按鈕,在彈出的對話框中輸入Name。在Location 欄如果輸入一個http 地址是讓eclipse自動從網絡上下載安裝,這里我們點擊Archive 按鈕找到我們事先下載好的離線安裝包。然后點擊OK。
將CDT 所有的插件勾選上,同時將最下面的聯網檢查更新去掉勾選,然后點擊Next,直到Finish。
安裝好以后在File->New->Other 中會有C/C++選項,如下圖。
在Open Perspective 中也多了C/C++視圖可選項,如下圖。
安裝好以后,我們就可以在eclipse 中開發我們的C/C++工程了。不過對我們Android 開發人員來說用到的機會不是很多。就算是開發C/C++工程,大多數程序員也不會選擇在eclipse 平臺上進行開發。Eclipse更多的是專注于Java 語言項目的開發,比如JavaEE、Android。