JNI與底層調用1

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 的下載

NDK 的官方下載地址

3.2 將NDK 解壓到一個不包含空格和中文的目錄下

本人將NDK 解壓在D:\software\ndkr9\android-ndk-r9b 中。

3.3 NDK 目錄結構說明

自定義好組合控件之后,之前的activity_setting.xml 中的代碼就可以進行簡化,具體如下所示:

jni
  • 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 入門》,工程的最終目錄結構如下圖所示。

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 目錄中,如下圖
jni

上面的某平臺指CPU 的三種架構如下圖。我們選擇任意一架構皆可,但是對于手機來說CPU 用arm架構的最多,x86 次之,mips 架構最少。

jni
  • 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 會有如下提示:

jni

將當前目錄切換到hello.c 所在的工程目錄,這時候如果直接輸入ndk-build.cmd 那么會出現如下異常:

jni

出現這種錯誤時因為我們并沒有告訴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 顯示效果如下圖所示。

jni

查看項目目錄結構,發現在libs 目錄中多了兩個文件夾armeabi 和x86,這兩個文件夾下分別包含了一個libhello.so 動態鏈接庫。這也代表著當前工程中的動態庫支持arm 架構和x86 架構的cpu。

jni

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();
    }
}

運行上面工程,效果如下:

jni

Tips:如果我們編譯的arm 平臺的so 文件,但是卻部署到了x86 平臺的模擬器上,那么運行的時候會報找不到libhello.so 的異常。

5. JNI 規范

5.1 JNI 數據類型和數據結構

1)基本數據類型
JNI 基本類型和本地等效類型的對應表格如下:

jni

2)引用類型,JNI 還包含了很多對應于不同Java 對象的引用類型,JNI 引用類型的組織層次如下圖所示:

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 文件夾,然后將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

jni

編寫在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();
    }
}

布局文件比較簡單,這里就不再給出。運行上面的代碼,運行結果如下:

jni

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...,彈出如下對話框

jni

點擊Add 按鈕,在彈出的對話框中輸入Name。在Location 欄如果輸入一個http 地址是讓eclipse自動從網絡上下載安裝,這里我們點擊Archive 按鈕找到我們事先下載好的離線安裝包。然后點擊OK。

jni

將CDT 所有的插件勾選上,同時將最下面的聯網檢查更新去掉勾選,然后點擊Next,直到Finish。

jni

安裝好以后在File->New->Other 中會有C/C++選項,如下圖。

jni

在Open Perspective 中也多了C/C++視圖可選項,如下圖。

jni

安裝好以后,我們就可以在eclipse 中開發我們的C/C++工程了。不過對我們Android 開發人員來說用到的機會不是很多。就算是開發C/C++工程,大多數程序員也不會選擇在eclipse 平臺上進行開發。Eclipse更多的是專注于Java 語言項目的開發,比如JavaEE、Android。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容