1. 什么是 CPU 架构及 ABI
Android 系统目前支持以下七种不同的 CPU 架构: ARMv5,ARMv7 (从 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");
来源: http://mobile.51cto.com/ahot-581225.htm