經驗總結
在JNI開發過程中,我們使用C++去寫一個動態庫,由於C++編譯器對於函數的符號的生成需要進行名字修飾處理,然後生成的函數符號不再跟源代碼中定義的函數名一致
這樣導致調用方通過函數名去調用我們的函數(用函數名充當函數符號去查找函數地址),將會找不到具體的實現,然後崩潰。
JVM調用我們寫的native接口/接口,就是這種情況。
所以當我們使用C++去寫navtive的接口時,需要用extern “C” 包住native接口,這樣就顯示告訴C++編譯器,對於我們的接口/函數使用C語言的方式(函數符號即函數名稱)去編譯代碼和生成函數符號。
如下是CPP源碼中native接口的實現沒有使用extern “C” 聲明,運行時出現的崩潰棧
2020-06-30 18:45:22.522 10202-10202/? E/art: No implementation found for java.lang.String com.example.hellolibs.MainActivity.stringFromJNI() (tried Java_com_example_hellolibs_MainActivity_stringFromJNI and Java_com_example_hellolibs_MainActivity_stringFromJNI__)
2020-06-30 18:45:22.523 10202-10202/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.hellolibs, PID: 10202
java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.example.hellolibs.MainActivity.stringFromJNI() (tried Java_com_example_hellolibs_MainActivity_stringFromJNI and Java_com_example_hellolibs_MainActivity_stringFromJNI__)
at com.example.hellolibs.MainActivity.stringFromJNI(Native Method)
at com.example.hellolibs.MainActivity.onCreate(MainActivity.java:31)
at android.app.Activity.performCreate(Activity.java:6813)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2805)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2927)
at android.app.ActivityThread.-wrap12(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1650)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:159)
at android.app.ActivityThread.main(ActivityThread.java:6364)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1096)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:883)
如上案例,對應的so中的函數符號是
_Z53Java_com_example_hellolibs_MainActivity_stringFromJNIP7_JNIEnvP8_jobject
示例說明
- 本文使用的示例是google官方的JNI demo中的hello-libs跟hello-jni
- 另外使用llvm-readelf來查看so中的符號信息
示例分析
hello-jni示例
hello-jni的項目結構結下圖所示,關鍵的hello-jni.c的代碼如下
我們進去看下so中的符號,就是看到定義的native接口的名稱跟函數符號是一致的
dw_luogongwu@dw-luogongwudeMacBook-Pro hello-jni$ ff *.so
./app/build/intermediates/cmake/arm8Debug/obj/armeabi-v7a/libhello-jni.so
./app/build/intermediates/cmake/arm8Debug/obj/arm64-v8a/libhello-jni.so
./app/build/intermediates/merged_native_libs/arm8Debug/out/lib/armeabi-v7a/libhello-jni.so
./app/build/intermediates/merged_native_libs/arm8Debug/out/lib/arm64-v8a/libhello-jni.so
./app/build/intermediates/stripped_native_libs/arm8Debug/out/lib/armeabi-v7a/libhello-jni.so
./app/build/intermediates/stripped_native_libs/arm8Debug/out/lib/arm64-v8a/libhello-jni.so
dw_luogongwu@dw-luogongwudeMacBook-Pro out$ llvm-readelf --symbols lib/arm64-v8a/libhello-jni.so | grep FUNC
3: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@LIBC
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __register_atfork@LIBC
5: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@LIBC
11: 0000000000000608 64 FUNC GLOBAL DEFAULT 10 Java_com_example_hellojni_HelloJni_stringFromJNI
40: 00000000000005c0 12 FUNC LOCAL DEFAULT 10 __on_dlclose
41: 00000000000005d0 4 FUNC LOCAL DEFAULT 10 __on_dlclose_late
53: 00000000000005fc 12 FUNC LOCAL DEFAULT 10 pthread_atfork
55: 00000000000005d4 12 FUNC LOCAL DEFAULT 10 __atexit_handler_wrapper
59: 00000000000005e0 28 FUNC LOCAL DEFAULT 10 atexit
60: 00000000000005cc 4 FUNC LOCAL DEFAULT 10 __emutls_unregister_key
63: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_finalize@@LIBC
64: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __register_atfork@@LIBC
71: 0000000000000608 64 FUNC GLOBAL DEFAULT 10 Java_com_example_hellojni_HelloJni_stringFromJNI
72: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __cxa_atexit@@LIBC
總結:使用C來寫動態庫,不需要使用extern
hello-libs示例
hello-libs的項目結構,跟關鍵的代碼如下圖所示
如下圖是hello-lib使用與去掉extern後,native接口對應的函數符號的對比
總結: 使用C++來寫動態庫,需要extern “C” 來聲明我們實現的natvie的接口/函數
其它說明
通過函數符號調用函數
這個屬於dlopen/dlsum的使用範疇,即運行時加載一個動態庫,並通過符號找到函數地址,通過函數針指方式調用函數
具體可以看參考文檔中的 C語言調用so動態庫的兩種方式
有關C++的名字修飾
在C/C++中,一個程序要運行起來,需要經歷以下幾個階段:預處理、編譯、彙編、鏈接。
名字修飾(Name Mangling)是一種在編譯過程中,將函數、變量的名稱重新改編的機制,
簡單來說就是編譯器爲了區分各個函數,將函數通過一定算法,重新修飾爲一個全局唯一的名稱。
具體可以看篇文章 >> C+±-名字修飾
有關llvm-readelf的配置與使用
我使用的是ndk自帶的llvm,我把llvm的bin目錄放到了系統環境變量中,方便使用所有用llvm-xxx命令
在.bash_profile配置如下
# for android-ndk env
ANDROID_NDK_LLVM_BIN=${HOME}/Library/Android/sdk/ndk/21.2.6472646/toolchains/llvm/prebuilt/darwin-x86_64/bin
PATH=$PATH:$ANDROID_NDK_LLVM_BIN
export PATH