最近我們發現很多用戶在接入虹軟ArcFace人臉識別SDK時,經常會遇到動態庫加載失敗的相關問題。本文詳細介紹從編譯動態庫(.so)到程序調用so的整個流程,模擬在加載虹軟人臉識別so文件時經常遇到的一些問題,幫助大家了解這些問題出現的原因以及解決方法。
一、 ArcFace庫加載常見錯誤
1.1 找不到動態庫
java.lang.UnsatisfiedLinkError: couldn't find "libarcsoft_face_engine.so"
原因:
在安裝應用時,APK中指定的ABI目錄下沒有發現指定的動態庫,尋找apk中動態庫的規則詳見
https://developer.android.google.cn/ndk/guides/abis?hl=en#aen
導致這個問題的間接原因很多,比如:
- Android工程中沒有指定的動態庫
- Android工程中動態庫存放位置錯誤
- 設備支持的最高ABI是armeabi-v7a,而apk只有arm64-v8a的動態庫
解決方案:
確保被安裝程序中包含的目標設備支持的ABI的動態庫,可以解壓APK檢查動態庫是否存在。
1.2 加載的動態庫ABI不對
java.lang.UnsatisfiedLinkError: "libarcsoft_face_engine.so" is 32-bit instead of 64-bit
原因:
在64位庫目錄下存放的動態庫文件是32位的。
例如將armeabi-v7a的動態庫存放在arm64-v8a目錄下,並安裝在支持arm64-v8a的設備上,就會導致這樣的錯誤。
解決方案:
確保動態庫ABI正確,一般在拷貝文件時拷貝ABI文件夾即可。
1.3 動態庫文件長度為0
java.lang.UnsatisfiedLinkError: dlopen failed: file offset for the library ".../libarcsoft_face_engine.so" >= file size: 0 >= 0
原因:
動態庫存在,但是文件是空的。
解決方案:
重新將動態庫引入工程。
1.4 執行函數時找不到XXXX函數
java.lang.UnsatisfiedLinkError: No implementation found for int b.a.a.b.b(android.content.Context, java.lang.String, java.lang.String) (tried Java_b_a_a_b_b and Java_b_a_a_b_b__Landroid_content_Context_2Ljava_lang_String_2Ljava_lang_String_2)
at b.a.a.b.b(Native Method)
at b.a.a.b.a(:182)
原因:
在Java函數確定后,按照固定的規則去尋找native函數找不到。一般情況下都是Java代碼混淆導致的。
解決方案:
修改混淆配置文件,確保相關的Java代碼不被混淆。
1.5 在加載動態庫時出現crash
JNI DETECTED ERROR IN APPLICATION: JNI RegisterNatives called with pending exception java.lang.ClassNotFoundException: Didn't find class "com.arcsoft.face.FaceEngine"
原因:
在動態庫中,以指定的Java簽名無法找到對應的Java類、函數、變量。
解決方案:
修改混淆配置文件,確保相關的Java代碼不被混淆。
以上是常見的crash與基本原因和解決方案的介紹,接下來,我們來自己編譯動態庫並使用,了解下這些問題是怎么出現的。
二、自己編譯並使用動態庫
2.1. 編譯動態庫
2.1.1 CMakeLists.txt
CMakeLists.txt
里的內容比較簡單,將hello.cpp
編譯成一個名為libhello-sdk.so
的動態庫
add_library(
hello-sdk
SHARED
hello.cpp
)
2.1.2 hello.cpp
在這個文件中,使用JNI靜態注冊和動態注冊的方式定義了兩個函數,並在JNI_Onload
中對需要動態注冊的函數進行注冊:
-
Java_com_arcsoft_functionregisterdemo_MainActivity_hello
需要被靜態注冊的函數,在Java中定義的native函數首次被調用時,會由JVM按照固定的規則去尋找native函數並注冊。這個規則一般是:Java_包名_類名_函數名
。具體的實現,大家感興趣的話,可參考這個地址中的JniShortName()
和JniLongName()
: http://androidxref.com/9.0.0_r3/xref/art/runtime/art_method.cc -
dynamicRegisterFunction
需要被動態注冊的函數,一般在JNI_OnLoad
中進行注冊。 -
JNI_OnLoad
動態庫被加載時,會被執行的函數,在這里對dynamicRegisterFunction
進行注冊。對於JNI_OnLoad
函數被調用的具體實現,大家感興趣的話,可參考 http://androidxref.com/9.0.0_r3/xref/art/runtime/java_vm_ext.cc 的第1009至1024行。
#include <jni.h>
#include <string>
// 靜態注冊的函數,對應MainActivity類中的hello函數
extern "C" JNIEXPORT jstring JNICALL
Java_com_arcsoft_functionregisterdemo_MainActivity_hello(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("hello world");
}
// 動態注冊的函數
jstring dynamicRegisterFunction(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("hello, I'm from dynamicRegisterFunction");
}
// 在動態庫加載時進行函數注冊
int JNI_OnLoad(JavaVM *javaVM, void *reserved) {
JNIEnv *jniEnv;
if (javaVM->GetEnv((void **) (&jniEnv), JNI_VERSION_1_4) != JNI_OK) {
return JNI_ERR;
}
jclass registerClass = jniEnv->FindClass("com/arcsoft/functionregisterdemo/MainActivity");
JNINativeMethod jniNativeMethods[] = {
// name signature nativeFunction
{"dynamicRegisterFunction", "()Ljava/lang/String;",
(void *) (dynamicRegisterFunction)}
};
if (jniEnv->RegisterNatives(registerClass, jniNativeMethods,
sizeof(jniNativeMethods) / sizeof((jniNativeMethods)[0])) < 0) {
return JNI_ERR;
} else {
return JNI_VERSION_1_4;
}
}
2.1.3 build.gradle
配置CMakeLists.txt
所在路徑,且配置當前編譯的abi僅為armeabi-v7a
和arm64-v8a
apply plugin: 'com.android.application'
android {
...
defaultConfig {
...
ndk.abiFilters 'armeabi-v7a', 'arm64-v8a'
}
...
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
}
dependencies {
...
}
2.1.4 編譯
我們可以選擇直接打包apk安裝運行,但這里為了模擬調用SDK,我們可以選擇手動打包動態庫再拿來使用。
執行externalNativebuild(Release|Debug)
(可terminal
執行gradlew externalNativebuildRelease
或點擊Android Studio右側Gradle中的選項)編譯release或debug版本的動態庫,這里選擇externalNativeBuildRelease
,編譯結果如下:
至此,libhello-sdk.so
編譯完成,接下來把工程的Native構建配置刪除,像接入SDK一樣使用這兩個動態庫。
2.2 正確地使用已編譯的動態庫
2.2.1 將所需的動態庫存放在src/main/jniLibs
目錄下
2.2.2 去除gradle中的Native構建配置
由於我們已經編譯好動態庫了,現在去除gradle中的Native構建配置,否則會報More than one file was found with OS independent path 'XXXX'
的錯誤
// externalNativeBuild {
// cmake {
// path "src/main/cpp/CMakeLists.txt"
// version "3.10.2"
// }
// }
2.2.3 在MainActivity
中使用
package com.arcsoft.functionregisterdemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// 加載動態庫
static {
System.loadLibrary("hello-sdk");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = findViewById(R.id.sample_text);
tv.setText(hello());
tv.append("\n\n");
tv.append(dynamicRegisterFunction());
}
// 靜態注冊的函數
public native String hello();
// 動態注冊的函數
public native String dynamicRegisterFunction();
}
2.2.4 運行效果正常
2.3 錯誤地使用已編譯的動態庫,復現上述問題
2.3.1 找不到動態庫
操作方式:jniLibs目錄下不保留任何動態庫
日志如下,在加載動態庫時,由於在幾個庫目錄尋找所需的動態庫沒找到,於是報了UnsatisfiedLinkError
錯誤:couldn't find "libhello-sdk.so"
2020-03-26 15:55:09.448 26336-26336/com.arcsoft.functionregisterdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.arcsoft.functionregisterdemo, PID: 26336
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.arcsoft.functionregisterdemo-6C3PyVyDJypXOtLP_dDykA==/base.apk"],nativeLibraryDirectories=[/data/app/com.arcsoft.functionregisterdemo-6C3PyVyDJypXOtLP_dDykA==/lib/arm64, /system/lib64, /system/vendor/lib64]]] couldn't find "libhello-sdk.so"
at java.lang.Runtime.loadLibrary0(Runtime.java:1012)
....
2.3.2 加載的動態庫ABI不對
操作方式:將armeabi-v7a
的動態庫放到arm64-v8a
目錄下
日志如下,在加載動態庫時,雖然庫是存在的,但是ABI不對,於是報了UnsatisfiedLinkError
錯誤: "XXXX" is 32-bit instead of 64-bit
2020-03-26 15:56:25.747 26517-26517/com.arcsoft.functionregisterdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.arcsoft.functionregisterdemo, PID: 26517
java.lang.UnsatisfiedLinkError: dlopen failed: "/data/app/com.arcsoft.functionregisterdemo-EWDPPRqzg8u7sv1Dq30ZJA==/lib/arm64/libhello-sdk.so" is 32-bit instead of 64-bit
at java.lang.Runtime.loadLibrary0(Runtime.java:1016)
....
2.3.3 動態庫文件長度為0
操作方式:刪除動態庫文件再撤銷刪除
這可能是Android Studio的一個小問題,有時刪除文件后撤銷刪除,文件雖然能夠重新出現,但是大小為0。
日志如下,在加載動態庫時,雖然庫是存在的,但是文件大小為0,於是報了UnsatisfiedLinkError
錯誤: dlopen failed: file offset for the library "XXXX" >= file size: 0 >= 0
2020-03-26 15:56:58.114 26669-26669/com.arcsoft.functionregisterdemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.arcsoft.functionregisterdemo, PID: 26669
java.lang.UnsatisfiedLinkError: dlopen failed: file offset for the library "/data/app/com.arcsoft.functionregisterdemo-PITl9rCd6FztSupEwwvjQA==/lib/arm64/libhello-sdk.so" >= file size: 0 >= 0
at java.lang.Runtime.loadLibrary0(Runtime.java:1016)
at java.lang.System.loadLibrary(System.java:1669)
....
2.3.4 執行函數時找不到XXXX函數
操作方式:混淆Java代碼
這也是導致crash的最常見的一種場景,一般情況下,我們在編譯debug版apk時,是沒有進行代碼混淆的,而編譯release版apk時會做混淆,這就會導致debug時程序運行正常,但一運行release版就crash。剛才在代碼中,我們用靜態注冊和動態注冊兩種方式實現函數的注冊,
對於JNI靜態注冊,JVM會根據Java函數的名稱和簽名尋找對應的native函數,若找不到,則報java.lang.UnsatiesFiedLinkError
錯誤。
由於我們的動態庫中包含靜態注冊和動態注冊的函數,直接混淆所有函數可能會導致加載動態庫時直接crash,因此這里手動修改靜態注冊的函數模擬下靜態注冊的函數被混淆的效果,將hello()
函數修改為a()
,運行,錯誤日志如下:
java.lang.UnsatisfiedLinkError: No implementation found for java.lang.String com.arcsoft.functionregisterdemo.MainActivity.a() (tried Java_com_arcsoft_functionregisterdemo_MainActivity_a and Java_com_arcsoft_functionregisterdemo_MainActivity_a__)
at com.arcsoft.functionregisterdemo.MainActivity.a(Native Method)
修改為原來的函數名,運行正常。
2.3.5 在加載動態庫時出現crash
操作方式:混淆Java代碼
混淆Java代碼也可能會導致加載動態庫時直接crash。
對於JNI動態注冊,我們一般會在JNI_OnLoad中進行函數注冊,此時native函數由函數指針確定,JVM根據指定的Java函數名和函數簽名尋找對應的Java函數,若找不到,則會直接報錯,錯誤內容一般如下:
JNI DETECTED ERROR IN APPLICATION: JNI NewGlobalRef called with pending exception java.lang.NoSuchMethodError: no static or non-static method "classSignature + . + functionName + FunctionSignature"
修改build.gradle
文件,配置代碼混淆:
buildTypes {
debug {
minifyEnabled true
proguardFiles 'proguard-rules.pro'
}
}
當前proguard-rules.pro
中沒有任何配置,因此運行直接crash,部分日志如下,從日志中可以看到,按照指定的規則尋找Java函數找不到了。
......
2020-03-26 15:58:39.046 26947-26947/com.arcsoft.functionregisterdemo A/ionregisterdem: java_vm_ext.cc:542] JNI DETECTED ERROR IN APPLICATION: JNI NewGlobalRef called with pending exception java.lang.NoSuchMethodError: no static or non-static method "Lcom/arcsoft/functionregisterdemo/MainActivity;.dynamicRegisterFunction()Ljava/lang/String;"
......
修改proguard-rules.pro
,添加混淆規則,保留MainActivity
中的native函數:
-keepclasseswithmembers class com.arcsoft.functionregisterdemo.MainActivity{
native <methods>;
}
此時運行效果正常,需要注意的是,如果自己編寫native函數,需要在native反射修改java中的field,還需要確保需要被反射的field不被混淆。
三、 小結
若以下其中一項不滿足,就無法成功調用動態庫:
- 動態庫及其依賴庫存在,且加載成功
- Java函數和native函數關聯成功(靜態注冊 or 動態注冊)
當遇到錯誤時,日志中一般都有一些關鍵信息,能看到錯誤的具體原因,我們可以分析日志,了解排錯方法。