一、JNI簡介
??Android系統(tǒng)按語言來劃分的話由兩個世界組成,分別是。為什么要這樣劃分呢?Android系統(tǒng)全部由Java開發(fā)不行嗎?除了性能的原因外,更多的原因是,Java誕生前,有多程序代碼都是由Native語言寫的,因此重復(fù)利用這些Native語言編寫的庫十分必要,況且Native擁有更好的性能。這樣就產(chǎn)生一個問題,如何將Java世界的代碼和Native代碼連接起來呢?這就需要一個橋梁,也就是JNI。

??
JNI是Java Native Interface的縮寫,譯為java本地接口,是java與其他語言通信的橋梁。當(dāng)java語言無法處理的時候,就可以使用JNI技術(shù)來完成,主要有以下幾種情況需要使用JNI技術(shù)。● 需要調(diào)用Java語言不支持的依賴于操作系統(tǒng)平臺特性的一些功能。例如:需要調(diào)用當(dāng)前的UNIX系統(tǒng)的某個功能,而Java不支持這個功能,就需要用到JNI技術(shù)來實現(xiàn)
● 為了整合以前的非Java語言開發(fā)的系統(tǒng)。例如早期使用C/C++實現(xiàn)的功能或系統(tǒng),通過JNI可以直接整合到項目,而無需再使用Java語言重復(fù)造輪子
● 為了提升運行效率,節(jié)約程序運行時間。例如:游戲,音視頻編解碼、圖形繪制等,使用C/C++開發(fā)會運行更快
二、手動實現(xiàn)JNI demo(靜態(tài)注冊)
??Native方法注冊分為靜態(tài)注冊和動態(tài)注冊,先對兩者進行簡要說明:
靜態(tài)注冊
原理:根據(jù)函數(shù)名來建立 java 方法與 JNI 函數(shù)的一一對應(yīng)關(guān)系
實現(xiàn)流程:
編寫 java 代碼;
利用 javah 指令生成對應(yīng)的 .h 文件;
對 .h 中的聲明進行實現(xiàn);
缺點
編寫不方便,JNI 方法名字必須遵循規(guī)則且名字很長;
編寫過程步驟多,不方便;
程序運行效率低,因為初次調(diào)用native函數(shù)時需要根據(jù)根據(jù)函數(shù)名在JNI層中搜索對應(yīng)的本地函數(shù),
然后建立對應(yīng)關(guān)系,這個過程比較耗時;
動態(tài)注冊
原理
利用 RegisterNatives 方法來注冊 java 方法與 JNI 函數(shù)的一一對應(yīng)關(guān)系;
實現(xiàn)流程
1、java中定義Native方法
2、編寫C/C++文件,實現(xiàn)jni接口方法,以及JNI_OnLoad方法
3、編譯so庫
4、java加載so庫,調(diào)用native方法
優(yōu)點:
靈活性高, 更改類名,包名或方法時, 只需對更改模塊進行少量修改, 效率高
缺點
對新手來說稍微有點難理解, 同時會由于搞錯簽名, 方法, 導(dǎo)致注冊失敗
下面分別對這兩種注冊方法進行講解。
??本篇我們先講一下如何靜態(tài)注冊Native,并實現(xiàn)我們的JNI demo
1、首先我們得創(chuàng)建一個Native方法吧,所以新建類JNITest.java,并創(chuàng)建Native方法getName
public class JNITest {
static {
System.loadLibrary("mgjnitest");
}
public static native String getName();
}
2、第二步,我們需要手動使用Javac 命令生產(chǎn).h頭文件:jni_com_cj_constom_jnitest_JNITest.h
C:\workspace\MyPracticeApp\jnitest\src\main\java\jni\com\cj\constom\jnitest>javac JNITest.java
C:\workspace\MyPracticeApp\jnitest\src\main\java\jni\com\cj\constom\jnitest>cd ../../../../../
C:\workspace\MyPracticeApp\jnitest\src\main\java>javah -jni jni.com.cj.constom.jnitest.JNITest
C:\workspace\MyPracticeApp\jnitest\src\main\java>
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class jni_com_cj_constom_jnitest_JNITest */
#ifndef _Included_jni_com_cj_constom_jnitest_JNITest
#define _Included_jni_com_cj_constom_jnitest_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jni_com_cj_constom_jnitest_JNITest
* Method: getName
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_jni_com_cj_constom_jnitest_JNITest_getName
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
3、根據(jù).h頭文件,創(chuàng)建對應(yīng)的.c文件jni_com_cj_constom_jnitest_JNITest.c,用于Native層的具體實現(xiàn)
#include <jni.h>
#include <string.h>
jstring Java_jni_com_cj_constom_jnitest_JNITest_getName(JNIEnv *env,jclass type){
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
備注:jstring Java_jni_com_cj_constom_jnitest_JNITest_getName(JNIEnv *env,jclass type)方法中,和java層的方法定義有兩點差異:
1、返回值不同于java中的String,而是jString,這個是jni所定義的數(shù)據(jù)類型別名,可以參考下方“java、C/C++,jni數(shù)據(jù)類型參照表”和“引用類型JNI參照表”
2、另外方法名稱由java層的getName變?yōu)榱薐ava_jni_com_cj_constom_jnitest_JNITest_getName,JAVA開頭指的是從JAVA層來調(diào)用JNI方法的,jni_com_cj_constom_jnitest_JNITest_getName則指的是 包名+類名+方法名


4、將.h文件和.c文件存放到同一個目錄,這里我們建立一個jni目錄

5、在上一步創(chuàng)建的jni目錄中,創(chuàng)建NDK編譯規(guī)則Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := mgjnitest
LOCAL_SRC_FILES := jni_com_cj_constom_jnitest_JNITest.c
include $(BUILD_SHARED_LIBRARY)
Android.mk文件參數(shù)規(guī)則說明如下:
# 此變量表示源文件在開發(fā)樹中的位置
# 在這里,構(gòu)建系統(tǒng)提供的宏函數(shù) my-dir 將返回當(dāng)前目錄(包含 Android.mk 文件本身的目錄)的路徑
LOCAL_PATH := $(call my-dir)
# 清除除了LOCAL_PATH之外的所有LOCAL_XXX變量
# 這個清理動作是必須的,因為所有的編譯控制文件由同一個GNU Make解析和執(zhí)行,其變量是全局的。所以清理后才能避免相互影響
include $(CLEAR_VARS)
# 表示Android.mk中的每一個模塊,名字必須唯一且不包含空格
# 構(gòu)建系統(tǒng)在生成最終共享庫文件時,會將正確的前綴和后綴自動添加到您分配給 LOCAL_MODULE 的名稱
LOCAL_MODULE := hello-jni
# 此可選變量可讓您覆蓋構(gòu)建系統(tǒng)默認用于其生成的文件的名稱
# 例如,如果 LOCAL_MODULE 的名稱為 foo,您可以強制系統(tǒng)將它生成的文件命名為 libnewfoo
LOCAL_MODULE_FILENAME := libhello-jni
# 枚舉源文件,以空格分隔多個文件
# LOCAL_SRC_FILES 變量必須包含要構(gòu)建到模塊中的 C 和/或 C++ 源文件列表
LOCAL_SRC_FILES =: src/hello-jni.cpp \
src/hello-jnicallback.cpp
# 此變量用于存儲當(dāng)前模塊依賴的靜態(tài)庫模塊列表
# 如果當(dāng)前模塊是共享庫或可執(zhí)行文件,此變量將強制這些庫鏈接到生成的二進制文件
# 如果當(dāng)前模塊是靜態(tài)庫,此變量只是指示,依賴當(dāng)前模塊的模塊也會依賴列出的庫
LOCAL_STATIC_LIBRARIES := world-jni
# 此變量包含在構(gòu)建共享庫或可執(zhí)行文件時要使用的其他鏈接器標(biāo)志列表
# 它可讓您使用 -l 前綴傳遞特定系統(tǒng)庫的名稱
# 例如,以下示例 -lz 指示鏈接器生成在加載時鏈接到 /system/lib/libz.so 的模塊
LOCAL_LDLIBS := -llog -lz -lm -landroid
# 設(shè)置頭文件的include目錄
LOCAL_C_INCLUDES := $(LOCAL_PATH)//include
# 指向 GNU Makefile 腳本,用于收集您自最近 include 后在 LOCAL_XXX 變量中定義的所有信息
# 此腳本確定要構(gòu)建的內(nèi)容及其操作方法
# BUILD_STATIC_LIBRARY: 編譯為靜態(tài)庫,靜態(tài)庫變量導(dǎo)致構(gòu)建系統(tǒng)生成擴展名為 .a 的庫
# BUILD_SHARED_LIBRARY: 編譯為動態(tài)庫,共享庫變量導(dǎo)致構(gòu)建系統(tǒng)生成具有 .so 擴展名的庫文件
# BUILD_EXECUTABLE: 編譯為Native C可執(zhí)行程序
# BUILD_PREBUILT: 該模塊已經(jīng)預(yù)先編譯,指向預(yù)建共享庫的單一路徑,例如 foo/libfoo.so
include $(BUILD_SHARED_LIBRARY)
6、在項目根目錄創(chuàng)建Application.mk
#APP_ABI := armeabi armeabi-v7a arm64-v8a x86
APP_ABI := all
APP_OPTIM := release
## 引用靜態(tài)庫
APP_STL := stlport_static
#NDK_TOOLCHAIN_VERSION=4.8
#APP_PLATFORM := android-14
Application.mk的參數(shù)規(guī)則如下:
# 此變量用于存儲應(yīng)用項目根目錄的絕對路徑
# 構(gòu)建系統(tǒng)使用此信息將生成的 JNI 共享庫的簡縮版放入 APK 生成工具已知的特定位置
# 如果將 Application.mk 文件放在 $NDK/apps/<myapp>/ 下,則必須定義此變量
# 如果將其放在 $PROJECT/jni/ 下,則此變量可選
# APP_PROJECT_PATH
# 將此可選變量定義為 release 或 debug,在構(gòu)建應(yīng)用的模塊時可使用它來更改優(yōu)化級別
# 發(fā)行模式是默認模式,可生成高度優(yōu)化的二進制文件。調(diào)試模式會生成未優(yōu)化的二進制文件,更容易調(diào)試
# 請注意,您可以調(diào)試發(fā)行或調(diào)試二進制文件。但發(fā)行二進制文件在調(diào)試時提供的信息較少
# 例如,構(gòu)建系統(tǒng)會選擇某些合適的變量,您無需檢查它們
# 此外,代碼重新排序可能增大單步調(diào)試代碼的難度;堆疊追蹤可能不可靠
# 在應(yīng)用清單的 <application> 標(biāo)記中聲明 android:debuggable 將導(dǎo)致此變量默認使用 debug而非 release
# 將 APP_OPTIM 設(shè)置為 release 可替換此默認值
# APP_OPTIM
# 指定機器指令集
# APP_ABI := all
APP_ABI := armeabi-v7a arm64-v8a
# 目標(biāo) Android 平臺的名稱
APP_PLATFORM := android-26
7、使用NDK工具對.c文件進行編譯,生成對應(yīng)的so庫
C:\workspace\MyPracticeApp\jnitest>cd jni
C:\workspace\MyPracticeApp\jnitest\jni>ndk-build
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16.
[arm64-v8a] Compile : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[arm64-v8a] SharedLibrary : libmgjnitest.so
[arm64-v8a] Install : libmgjnitest.so => libs/arm64-v8a/libmgjnitest.so
[armeabi-v7a] Compile thumb : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[armeabi-v7a] SharedLibrary : libmgjnitest.so
[armeabi-v7a] Install : libmgjnitest.so => libs/armeabi-v7a/libmgjnitest.so
[x86] Compile : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[x86] SharedLibrary : libmgjnitest.so
[x86] Install : libmgjnitest.so => libs/x86/libmgjnitest.so
[x86_64] Compile : mgjnitest <= jni_com_cj_constom_jnitest_JNITest.c
[x86_64] SharedLibrary : libmgjnitest.so
[x86_64] Install : libmgjnitest.so => libs/x86_64/libmgjnitest.so

8、配置jnitest module中的build.gradle文件,對
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
ndk {//打包apk時所集成的so庫類型
abiFilters "armeabi-v7a"
}
}
externalNativeBuild{//配置ndk編譯路徑,自動化ndk編譯,從而無需第七步的手動ndk-build命令編譯
ndkBuild{
path "jni/Android.mk"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {//指定jnilib目錄路徑
main {
jni.srcDirs = []
jniLibs.srcDirs = ['libs']
}
}
}
9、開始從Java層調(diào)用Native層的方法,回到我們最初創(chuàng)建的JNITest.java文件,我們在static初始化中加載Native so,然后我們就可以在Activity中調(diào)用getName方法了
public class JNITest {
static {
System.loadLibrary("mgjnitest");
}
public static native String getName();
}
MainActivity.java中的按鈕點擊事件,通過JNITest.getName()獲取到我們.c文件中返回的"Hello from JNI !"字符串
public void getJNIValue(View view) {
String test = JNITest.getName();
Toast.makeText(getApplicationContext(), "value=" + test, Toast.LENGTH_SHORT).show();
}
運行效果圖如下,第一個JNI demo成功!
demo github地址為:
https://github.com/MRCHENJUN/JNItest/tree/master/MyPracticeApp
