JNI(Java Native Interface)
提供一種Java字節(jié)碼調(diào)用C/C++的解決方案秀睛,JNI描述的是一種技術(shù)。
NDK(Native Development Kit)
Android NDK 是一組允許您將 C 或 C++(“原生代碼”)嵌入到 Android 應(yīng)用中的工具叛甫,NDK描述的是工具集。 能夠在 Android 應(yīng)用中使用原生代碼對于想執(zhí)行以下一項(xiàng)或多項(xiàng)操作的開發(fā)者特別有用:
- 在平臺之間移植其應(yīng)用应媚。
- 重復(fù)使用現(xiàn)有庫弥雹,或者提供其自己的庫供重復(fù)使用。
- 在某些情況下提高性能园担,特別是像游戲這種計(jì)算密集型應(yīng)用届谈。
JNI方法注冊
靜態(tài)注冊
當(dāng)Java層調(diào)用navtie函數(shù)時(shí),會在JNI庫中根據(jù)函數(shù)名查找對應(yīng)的JNI函數(shù)弯汰。如果沒找到艰山,會報(bào)錯。如果找到了咏闪,則會在native函數(shù)與JNI函數(shù)之間建立關(guān)聯(lián)關(guān)系曙搬,其實(shí)就是保存JNI函數(shù)的函數(shù)指針。下次再調(diào)用native函數(shù)鸽嫂,就可以直接使用這個(gè)函數(shù)指針纵装。
- JNI函數(shù)名格式(需將”.”改為”_”):
Java_ + 包名(com.example.auto.jnitest)+ 類名(MainActivity) + 函數(shù)名(stringFromJNI)
- 靜態(tài)方法的缺點(diǎn):
- 要求JNI函數(shù)的名字必須遵循JNI規(guī)范的命名格式;
- 名字冗長据某,容易出錯橡娄;
- 初次調(diào)用會根據(jù)函數(shù)名去搜索JNI中對應(yīng)的函數(shù),會影響執(zhí)行效率癣籽;
- 需要編譯所有聲明了native函數(shù)的Java類挽唉,每個(gè)所生成的class文件都要用javah工具生成一個(gè)頭文件;
動態(tài)注冊
通過提供一個(gè)函數(shù)映射表筷狼,注冊給JVM虛擬機(jī)瓶籽,這樣JVM就可以用函數(shù)映射表來調(diào)用相應(yīng)的函數(shù),就不必通過函數(shù)名來查找需要調(diào)用的函數(shù)埂材。
- Java與JNI通過JNINativeMethod的結(jié)構(gòu)來建立函數(shù)映射表塑顺,它在jni.h頭文件中定義,其結(jié)構(gòu)內(nèi)容如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
- 創(chuàng)建映射表后俏险,調(diào)用RegisterNatives函數(shù)將映射表注冊給JVM;
- 當(dāng)Java層通過System.loadLibrary加載JNI庫時(shí)严拒,會在庫中查JNI_OnLoad函數(shù)∈溃可將JNI_OnLoad視為JNI庫的入口函數(shù)糙俗,需要在這里完成所有函數(shù)映射和動態(tài)注冊工作,及其他一些初始化工作预鬓。
數(shù)據(jù)類型轉(zhuǎn)換
基礎(chǔ)數(shù)據(jù)類型轉(zhuǎn)換
引用數(shù)據(jù)類型轉(zhuǎn)換
除了Class巧骚、String、Throwable和基本數(shù)據(jù)類型的數(shù)組外格二,其余所有Java對象的數(shù)據(jù)類型在JNI中都用jobject表示劈彪。Java中的String也是引用類型,但是由于使用頻率較高顶猜,所以在JNI中單獨(dú)創(chuàng)建了一個(gè)jstring類型沧奴。
- 引用類型不能直接在 Native 層使用,需要根據(jù) JNI 函數(shù)進(jìn)行類型的轉(zhuǎn)化后长窄,才能使用;
- 多維數(shù)組(含二維數(shù)組)都是引用類型滔吠,需要使用 jobjectArray 類型存取其值纲菌;
例如,二維整型數(shù)組就是指向一位數(shù)組的數(shù)組疮绷,其聲明使用方式如下:
//獲得一維數(shù)組的類引用翰舌,即jintArray類型
jclass intArrayClass = env->FindClass("[I");
//構(gòu)造一個(gè)指向jintArray類一維數(shù)組的對象數(shù)組,該對象數(shù)組初始大小為length冬骚,類型為 jsize
jobjectArray obejctIntArray = env->NewObjectArray(length ,intArrayClass , NULL);
JNI函數(shù)簽名信息
由于Java支持函數(shù)重載椅贱,因此僅僅根據(jù)函數(shù)名是沒法找到對應(yīng)的JNI函數(shù)。為了解決這個(gè)問題只冻,JNI將參數(shù)類型和返回值類型作為函數(shù)的簽名信息庇麦。
JNI規(guī)范定義的函數(shù)簽名信息格式:
(參數(shù)1類型字符…)返回值類型字符-
函數(shù)簽名例子:
-
JNI常用的數(shù)據(jù)類型及對應(yīng)字符:
JNIEnv介紹
JNIEnv概念 :
JNIEnv是一個(gè)線程相關(guān)的結(jié)構(gòu)體, 該結(jié)構(gòu)體代表了 Java 在本線程的運(yùn)行環(huán)境。通過JNIEnv可以調(diào)用到一系列JNI系統(tǒng)函數(shù)喜德。JNIEnv線程相關(guān)性:
每個(gè)線程中都有一個(gè) JNIEnv 指針山橄。JNIEnv只在其所在線程有效, 它不能在線程之間進(jìn)行傳遞。
注意:在C++創(chuàng)建的子線程中獲取JNIEnv舍悯,要通過調(diào)用JavaVM的AttachCurrentThread函數(shù)獲得驾胆。在子線程退出時(shí),要調(diào)用JavaVM的DetachCurrentThread函數(shù)來釋放對應(yīng)的資源贱呐,否則會出錯丧诺。
- JNIEnv 作用:
- 訪問Java成員變量和成員方法;
- 調(diào)用Java構(gòu)造方法創(chuàng)建Java對象等奄薇。
JNI編譯
ndkBuild
Cmake編譯
CMake 則是一個(gè)跨平臺的編譯工具驳阎,它并不會直接編譯出對象,而是根據(jù)自定義的語言規(guī)則(CMakeLists.txt)生成 對應(yīng) makefile 或 project 文件馁蒂,然后再調(diào)用底層的編譯呵晚, 在Android Studio 2.2 之后支持Cmake編譯。
- add_library 指令
語法:add_library(libname [SHARED | STATIC | MODULE] [EXCLUDE_FROM_ALL] [source])
將一組源文件 source 編譯出一個(gè)庫文件沫屡,并保存為 libname.so (lib 前綴是生成文件時(shí) CMake自動添加上去的)饵隙。其中有三種庫文件類型,不寫的話沮脖,默認(rèn)為 STATIC;- SHARED: 表示動態(tài)庫金矛,可以在(Java)代碼中使用 System.loadLibrary(name) 動態(tài)調(diào)用;
- STATIC: 表示靜態(tài)庫勺届,集成到代碼中會在編譯時(shí)調(diào)用驶俊;
- MODULE: 只有在使用 dyId 的系統(tǒng)有效,如果不支持 dyId免姿,則被當(dāng)作 SHARED 對待饼酿;
- EXCLUDE_FROM_ALL: 表示這個(gè)庫不被默認(rèn)構(gòu)建,除非其他組件依賴或手工構(gòu)建;
#將compress.c 編譯成 libcompress.so 的共享庫
add_library(compress SHARED compress.c)
- target_link_libraries 指令
語法:target_link_libraries(target library <debug | optimized> library2…)
這個(gè)指令可以用來為 target 添加需要的鏈接的共享庫,同樣也可以用于為自己編寫的共享庫添加共享庫鏈接故俐。如:
#指定 compress 工程需要用到 libjpeg 庫和 log 庫
target_link_libraries(compress libjpeg ${log-lib})
- find_library 指令
語法:find_library(<VAR> name1 path1 path2 ...)
VAR 變量表示找到的庫全路徑想鹰,包含庫文件名 。例如:
find_library(libX X11 /usr/lib)
find_library(log-lib log) #路徑為空药版,應(yīng)該是查找系統(tǒng)環(huán)境變量路徑
Abi架構(gòu)
ABI(Application binary interface)應(yīng)用程序二進(jìn)制接口辑舷。不同的CPU 與指令集的每種組合都有定義的 ABI (應(yīng)用程序二進(jìn)制接口),一段程序只有遵循這個(gè)接口規(guī)范才能在該 CPU 上運(yùn)行刚陡,所以同樣的程序代碼為了兼容多個(gè)不同的CPU,需要為不同的 ABI 構(gòu)建不同的庫文件株汉。當(dāng)然對于CPU來說筐乳,不同的架構(gòu)并不意味著一定互不兼容。
- armeabi設(shè)備只兼容armeabi乔妈;
- armeabi-v7a設(shè)備兼容armeabi-v7a蝙云、armeabi;
- arm64-v8a設(shè)備兼容arm64-v8a路召、armeabi-v7a勃刨、armeabi;
- X86設(shè)備兼容X86股淡、armeabi身隐;
- X86_64設(shè)備兼容X86_64、X86唯灵、armeabi贾铝;
- mips64設(shè)備兼容mips64、mips埠帕;
- mips只兼容mips垢揩;
根據(jù)以上的兼容總結(jié),我們還可以得到一些規(guī)律:
- armeabi的SO文件基本上可以說是萬金油敛瓷,它能運(yùn)行在除了mips和mips64的設(shè)備上叁巨,但在非armeabi設(shè)備上運(yùn)行性能還是有所損耗;
- 64位的CPU架構(gòu)總能向下兼容其對應(yīng)的32位指令集呐籽,如:x86_64兼容X86锋勺,arm64-v8a兼容armeabi-v7a,mips64兼容mips狡蝶;
問題排查 addr2line
03-21 23:59:32.032 6770-6770/? A/DEBUG: *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
03-21 23:59:32.032 6770-6770/? A/DEBUG: Build fingerprint: 'google/sdk_gphone_x86/generic_x86:8.1.0/OPM1.171004.001/4376136:user/release-keys'
03-21 23:59:32.032 6770-6770/? A/DEBUG: Revision: '0'
03-21 23:59:32.032 6770-6770/? A/DEBUG: ABI: 'x86'
03-21 23:59:32.032 6770-6770/? A/DEBUG: pid: 6745, tid: 6745, name: ucai.nativedemo >>> com.choufucai.nativedemo <<<
03-21 23:59:32.032 6770-6770/? A/DEBUG: signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x70
03-21 23:59:32.032 6770-6770/? A/DEBUG: Cause: null pointer dereference
03-21 23:59:32.032 6770-6770/? A/DEBUG: eax 00000070 ebx a8a6479c ecx 00000035 edx 00000075
03-21 23:59:32.032 6770-6770/? A/DEBUG: esi ffffffff edi ffffffff
03-21 23:59:32.032 6770-6770/? A/DEBUG: xcs 00000073 xds 0000007b xes 0000007b xfs 0000003b xss 0000007b
03-21 23:59:32.032 6770-6770/? A/DEBUG: eip a89a2553 ebp bffa2408 esp bffa1e78 flags 00010202
03-21 23:59:32.228 6770-6770/? A/DEBUG: backtrace:
03-21 23:59:32.228 6770-6770/? A/DEBUG: #00 pc 0001d553 /system/lib/libc.so (strlen+51)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #01 pc 0005fd5d /system/lib/libc.so (__vfprintf+5581)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #02 pc 0008439e /system/lib/libc.so (vsnprintf+222)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #03 pc 00022f30 /system/lib/libc.so (__vsnprintf_chk+48)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #04 pc 000068de /system/lib/liblog.so (__android_log_print+78)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #05 pc 00000ee2 /data/app/com.choufucai.nativedemo-c_F0BwkNYJA0ITdueTXEdg==/lib/x86/libnative-lib.so
03-21 23:59:32.228 6770-6770/? A/DEBUG: #06 pc 00647e67 /system/lib/libart.so (art_quick_generic_jni_trampoline+71)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #07 pc 00641e62 /system/lib/libart.so (art_quick_invoke_stub+338)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #08 pc 00115fdf /system/lib/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+223)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #09 pc 0032143f /system/lib/libart.so (art::interpreter::ArtInterpreterToCompiledCodeBridge(art::Thread*, art::ArtMethod*, art::ShadowFrame*, unsigned short, art::JValue*)+335)
03-21 23:59:32.228 6770-6770/? A/DEBUG: #10 pc 0031a6a4 /system/lib/libart.so
以上錯誤日志中backtrace就是堆棧信息宙刘,#00 #01 就是堆棧列表。 #00 就是堆棧頂層即是錯誤所在地址牢酵,pc后面的就是地址悬包,可以通過以下命令查找出地址可以獲得對應(yīng)的源碼文件和行號:
// -f 輸出函數(shù)名
// -e 輸出錯誤代碼行數(shù)和文件路徑
// xxx.so 對應(yīng)出錯的so文件, 在android工程obj目錄下
// addr 是具體的地址
arm-linux-androideabi-addr2line -f -e xxx.so addr