虹软人脸识别 - Android平台调用动态库时的常见错误解析

最近我们发现很多用户在接入虹软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代码不被混淆。

<br>以上是常见的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-v7aarm64-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,编译结果如下:

虹软人脸识别 - Android平台调用动态库时的常见错误解析

至此,libhello-sdk.so编译完成,接下来把工程的Native构建配置删除,像接入SDK一样使用这两个动态库。

2.2 正确地使用已编译的动态库

2.2.1 将所需的动态库存放在src/main/jniLibs目录下

虹软人脸识别 - Android平台调用动态库时的常见错误解析

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 运行效果正常

虹软人脸识别 - Android平台调用动态库时的常见错误解析

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" &gt;= file size: 0 &gt;= 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 动态注册)

当遇到错误时,日志中一般都有一些关键信息,能看到错误的具体原因,我们可以分析日志,了解排错方法。

虹软人脸识别 - Android平台调用动态库时的常见错误解析

上一篇:APP测试的注意事项


下一篇:Thinkphp 5.x 应用启动 App::run()