1. 什么是CPU架构及ABI
Android系统目前支持以下七种不同的CPU架构:ARMv***RMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI。
应用程序二进制接口(Application Binary Interface)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集、内存对齐到可用的系统函数库。在Android系统上,每一个CPU架构对应一个ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。
2. 为什么需要重点关注.so文件
如果项目中使用到了NDK,它将会生成.so文件,因此显然你已经在关注它了。如果只是使用Java语言进行编码,你可能在想不需要关注.so文件了吧,因为Java是跨平台的。但事实上,即使你在项目中只是使用Java语言,很多情况下,你可能并没有意识到项目中依赖的函数库或者引擎库里面已经嵌入了.so文件,并依赖于不同的ABI。例如,项目中使用RenderScript支持库,OpenCV,Unity,android-gif-drawable,SQLCipher等,你都已经在生成的APK文件中包含.so文件了,而你需要关注.so文件。
Android应用支持的ABI取决于APK中位于lib/ABI目录中的.so文件,其中ABI可能是上面说过的七种ABI中的一种。
Native Libs Monitor这个应用可以帮助我们理解手机上安装的APK用到了哪些.so文件,以及.so文件来源于哪些函数库或者框架。当然,我们也可以自己对APP反编译来获取这些信息,不过相对麻烦一些。
很多设备都支持多于一种的ABI,例如ARM64和x86设备也可以同时运行armeabi-v7a和armeabi的二进制包。但***是针对特定平台提供相应平台的二进制包,这种情况下运行时就少了一个模拟层(例如x86设备上模拟arm的虚拟层),从而得到更好的性能(归功于最近的架构更新,例如硬件fpu,更多的寄存器,更好的向量化等)。我们可以通过Build.SUPPORTED_ABIS得到根据偏好排序的设备支持的ABI列表。但你不应该从你的应用程序中读取它,因为Android包管理器安装APK时,会自动选择APK包中为对应系统ABI预编译好的.so文件,如果在对应的lib/ABI目录中存在.so文件的话。
3. .so文件应该放在什么地方
我们往往很容易对.so文件应该放在或者生成到哪里感到困惑,下面是一个总结:
- Android Studio工程放在main/jniLibs/ABI目录中(当然也可以通过在build.gradle文件中的设置jniLibs.srcDir属性自己指定)
- Eclipse工程放在libs/ABI目录中(这也是ndk-build命令默认生成.so文件的目录)
- AAR压缩包中位于jni/ABI目录中(.so文件会自动包含到引用AAR压缩包的APK中)
- 最终APK文件中的lib/ABI目录中
- 通过PackageManager安装后,在小于Android 5.0的系统中,.so文件位于app的nativeLibraryPath目录中;在大于等于Android 5.0的系统中,.so文件位于app的nativeLibraryRootDir/CPU_ARCH目录中。
4. 安装Apk时PackageManagerService选择解压so文件的策略
在Android系统中,当我们安装Apk文件的时候,lib目录下的so文件会被解压App的原生库目录,一般来说是放到/data/data/package-name/lib目录下,而根据系统和CPU架构的不同,其拷贝策略也是不一样的,不正确地配置so文件,比如某些App使用第三方的so时,只配置了其中某一种CPU架构的so,可能会造成App在某些机型上的适配问题。
Android版本
so拷贝策略
策略问题
5. 配置so的建议
针对Android 系统的这些拷贝策略的问题,我们给出了一些配置so的建议:
5.1 针对armeabi和armeabi-v7a两种ABI
- 方法1:由于armeabi-v7a指令集兼容armeabi指令集,所以如果损失一些应用的性能是可以接受的,同时不希望保留库的两份拷贝,可以移除armeabi-v7a目录和其下的库文件,只保留armeabi目录;比如Apk使用第三方的so只有armeabi这一种ABI时,可以考虑去掉Apk中lib目录下armeabi-v7a目录。
- 方法2:在armeabi和armeabi-v7a目录下各放入一份so。
5.2 针对x86
目前市面上的x86机型,为了兼容arm指令,基本都内置libhoudini模块,即二进制转码支持,该模块负责把ARM指令转换为x86指令,所以如果是出于Apk包大小的考虑,并且可以接受一些性能损失,可以选择删掉x86库目录,x86下配置的armeabi目录的so库一样可以正常加载使用。
5.3 针对64位ABI
如果App开发者打算支持64位,那么64位的so要放全,否则可以选择不单独编译64位的so,全部使用32位的so,64位机型默认支持32位so的加载。比如Apk使用第三方的so只有32位ABI的so,可以考虑去掉Apk中lib目录下的64位ABI子目录,保证Apk安装后正常使用。
5. Android Studio配置abiFilters
- android {
- defaultConfig {
- ndk {
- abiFilters 'armeabi-v7a' //, 'armeabi', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
- }
- }
- }
这句话的意思就是指定NDK需要兼容的架构,把除了armeabi-v7a以外的兼容包都过滤掉,只剩下一个armeabi-v7a的文件夹。
即使我们没有指定其他的兼容框架,也需要一个过滤。当我们接入多个第三方库时,很可能第三方库做了多个平台的兼容。譬如fresco就做了各个平台的兼容,所以它创建了各个兼容平台的目录。因为只要出现了这个目录,系统就只会在这个目录里找.so文件而不会遍历其他的目录,所以就出现了找不到.so文件的情况。
6. java.lang.UnsatisfiedLinkError
该错误类型较多,以下进行分类:
- java.lang.UnsatisfiedLinkError : dlopen failed: library //dlopen打开失败
- java.lang.UnsatisfiedLinkError :findLibrary returned null //找不到library
- java.lang.UnsatisfiedLinkError : Native method not found //找不到对应函数
- java.lang.UnsatisfiedLinkError :Cannot load library: load_library //无法load library
出现原因:
显然出现上述崩溃的根本原因是:
- (1)so无法加载,可能是so不存在等原因
- (2)so正常加载,但是没有找到相应的函数
针对第二个原因,显然相对来说很容易排查,而且在开发中,这样的函数调用必然会在编译时和debug模式下进行测试,所以这种原因产生的概率很小。
那么下面主要总结几类“so无法加载”而导致上述崩溃的几种原因:
6.1 生成的so本身缺陷
一个简单的例子:
crash堆栈:
- java.lang.UnsatisfiedLinkError: Cannot load library: find_library(linker.cpp:889): "/data/data/com.netease.nis.apptestunit/app_lib/libdemo.so" failed to load previously
- at java.lang.Runtime.load(Runtime.java:340)
- at java.lang.System.load(System.java:521)
- at com.netease.nis.bugrpt.ReLinker.loadLibrary(ReLinker.java:76)
- at com.example.crash.MainActivity.onCreate(MainActivity.java:272)
- at android.app.Activity.performCreate(Activity.java:5220)
- at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1086)
- at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2193)
- at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2279)
- at android.app.ActivityThread.access$600(ActivityThread.java:142)
- at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1272)
- at android.os.Handler.dispatchMessage(Handler.java:99)
- at android.os.Looper.loop(Looper.java:137)
- at android.app.ActivityThread.main(ActivityThread.java:5105)
- at java.lang.reflect.Method.invokeNative(Native Method)
- at java.lang.reflect.Method.invoke(Method.java:511)
- at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
- at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
- at dalvik.system.NativeStart.main(Native Method)
解决方法:
查看原项目Application.mk,发现APP_STL := gnustl_shared。原方案使用的是共享库,这不一定都支持所有的机型,改用静态库gnustl_static问题解决。
对应的在Android Studio中需要将共享库改用静态库gnustl_static。这一类关于so编译共享库问题,需要进行检查。
- APP_STL 可用值
- system 系统默认
- stlport_static - 使用STLport作为静态库
- stlport_shared - 使用STLport 作为共享库
- gnustl_static - 使用GNU libstdc++ 作为静态库
- gnustl_shared - 使用GNU libstdc++ 作为共享库
上述例子只是一个简单的例子,可能在so编译生成时,由于没有考虑共享库的机型匹配等原因导致UnsatisfiedLinkError崩溃,其次是64位32位系统架构问题,也可能导致UnsatisfiedLinkError崩溃。
6.2 手机设备没有空间
在so正确生成情况下,会根据设置的支持so库框架生成对应的库。在Android系统中,当我们安装Apk文件的时候,lib目录下的so文件会被解压到App的原生库目录,一般来说是放到/data/data/package-name/lib目录下,当准备加载native层的so时,虽然在Apk中有对应的so文件,但是由于手机设备没有足够的空间加载该so,导致加载失败,产生上述崩溃。
6.3 so配置错误
倘若so正确生成,且手机空间充足,那么如上所述,在Android系统中,当我们安装Apk文件的时候,lib目录下的so文件会被解压到App的原生库目录,一般来说是放到/data/data/package-name/lib目录下。但是根据系统和CPU架构的不同,其拷贝策略也是不一样的。倘若不正确地配置了so文件,比如某些App使用第三方的so时,只配置了其中某一种CPU架构的so,可能会造成App在某些机型上的适配问题,产生上述崩溃。
6.4 Android的PackageManager安装问题
用户安装了与手机CPU架构不符的Apk安装包,或者App升级过程中因各种原因未正确释放so文件。这种问题可以使用ReLinker解决。
使用ReLinker十分简单,使用
- ReLinker.loadLibrary(context, “mylibrary”)
代替标准的即可。
- System.loadLibrary(“mylibrary”);