使用 SO 库时要注意的一些问题 1. 别把 SO 库放错地方
SO 库其实都是 APP 运行时加载的,也就是说 APP 只有在运行的时候才知道 SO 库文件的存在,这就无法通过静态代码检查或者在编译 APP 时检查 SO 库文件是否正常。所以,Android 开发对 SO 库的存放路径有严格的要求。
使用 SO 库的时候,除了 "armeabi-v7a" 等文件夹名需要严格按照规定的来自外,SO 库要放在项目的哪个文件夹下也要按照套路来,以下是一些总结:
既然扯到了这里,顺便说一下,我在使用 Android Studio 1.5 构建 APK 的时候,发现 Gradle 插件只会默认打包 application 类型的 module 的 jniLibs 下面的 SO 库文件,而不会打包 aar 依赖包的 SO 库,所以会导致最终构建出来的 APK 里的 SO 库文件缺失。暂时的解决方案是把所有的 SO 库都放在 application 模块中(这显然不是很好的解决方案),不知道这是不是 Studio 的 BUG,同事的解决方案是通过修改 Gradle 插件来增加对 aar 依赖包的 SO 库的打包支持(GitHub 有开源的第三方 Gradle 插件项目,使用 Java 和 Groovy 语言开发)。
2. 尽可能提供 CPU 支持的最优 SO 库
当一个应用安装在设备上,只有该设备支持的 CPU 架构对应的 SO 库会被安装。但是,有时候,设备支持的 SO 库类型不止一种,比如大多的 X86 设备除了支持 X86 类型的 SO 库,还兼容 ARM 类型的 SO 库(目前应用市场上大部分的 APP 只适配了 ARM 类型的 SO 库,X86 类型的设备如果不能兼容 ARM 类型的 SO 库的话,大概要嗝屁了吧)。
所以如果你的 APK 只适配了 ARM 类型的 SO 库的话,还是能以兼容的模式在 X86 类型的设备上运行(比如华硕的平板),但是这不意味着你就不用适配 X86 类型的 SO 库了,因为 X86 的 CPU 使用兼容模式运行 ARM 类型的 SO 库会异常卡顿(试着回想几年前你开始学习 Android 开发的时候,在 PC 上使用 AVD 模拟器的那种感觉)。
3. 注意 SO 库的编译版本
除了要注意使用了正确 CPU 类型的 SO 库,也要注意 SO 库的编译版本的问题。虽然现在的 Android Studio 支持在项目中直接编译 SO 库,但是更多的时候我们还是选择使用事先编译好的 SO 库,这时就要注意了,编译 APK 的时候,我们总是希望使用最新版本的 build-tools 来编译,因为 Android SDK 最新版本会帮我们做出最优的向下兼容工作。
但是这对于编译 SO 库来说就不一样了,因为 NDK 平台不是向下兼容的,而是向上兼容的。应该使用 app 的 minSdkVersion 对应的版本的 NDK 标本来编译 SO 库文件,如果使用了太高版本的 NDK,可能会导致 APP 性能低下,或者引发一些 SO 库相关的运行时异常,比如 "UnsatisfiedLinkError","dlopen: failed" 以及其他类型的 Crash。
一般情况下,我们都是使用编译好的 SO 库文件,所以当你引入一个预编译好的 SO 库时,你需要检查它被编译所用的平台版本。
4. 尽可能为每种 CPU 类型都提供对应的 SO 库
比如有时候,因为业务的需求,我们的 APP 不需要支持 AMR64 的设备,但这不意味着我们就不用编译 ARM64 对应的 SO 库。举个例子,我们的 APP 只支持 armeabi-v7a 和 x86 架构,然后我们的 APP 使用了一个第三方的 Library,而这个 Library 提供了 AMR64 等更多类型 CPU 架构的支持,构建 APK 的时候,这些 ARM64 的 SO 库依然会被打包进 APK 里面,也就是说我们自己的 SO 库没有对应的 ARM64 的 SO 库,而第三方的 Library 却有。这时候,某些 ARM64 的设备安装该 APK 的时候,发现我们的 APK 里带有 ARM64 的 SO 库,会误以为我们的 APP 已经做好了 AMR64 的适配工作,所以只会选择安装 APK 里面 ARM64 类型的 SO 库,这样会导致我们自己项目的 SO 库没有被正确安装(虽然 armeabi-v7a 和 x86 类型的 SO 库确实存在 APK 包里面)。
这时正确的做法是,给我们自己的 SO 库也提供 AMR64 支持,或者不打包第三方 Library 项目的 ARM64 的 SO 库。使用第二种方案时,可以把 APK 里面不需要支持的 ABI 文件夹给删除,然后重新打包,而在 Android Studio 下,则可以通过以下的构建方式指定需要类型的 SO 库。
- productFlavors {
- flavor1 {
- ndk {
- abiFilters "armeabi-v7a"abiFilters "x86"abiFilters "armeabi"
- }
- }
- flavor2 {
- ndk {
- abiFilters "armeabi-v7a"abiFilters "x86"abiFilters "armeabi"abiFilters "arm64-v8a"abiFilters "x86_64"
- }
- }
- }
需要说明的是,如果我们的项目是 SDK 项目,我们最好提供全平台类型的 SO 库支持,因为 APP 能支持的设备 CPU 类型的数量,就是项目中所有 SO 库支持的最少 CPU 类型的数量(使用我们 SDK 的 APP 能支持的 CPU 类型只能少于等于我们 SDK 支持的类型)。
5. 不要通过 "减少其他 CPU 类型支持的 SO 库" 来减少 APK 的体积
确实,所有的 x86/x86_64/armeabi-v7a/arm64-v8a 设备都支持 armeabi 架构的 SO 库,因此似乎移除其他 ABIs 的 SO 库是一个减少 APK 大小的好办法。但事实上并不是,这不只影响到函数库的性能和兼容性。
X86 设备能够很好的运行 ARM 类型函数库,但并不保证 100% 不发生 crash,特别是对旧设备,兼容只是一种保底方案。64 位设备(arm64-v8a, x86_64, mips64)能够运行 32 位的函数库,但是以 32 位模式运行,在 64 位平台上运行 32 位版本的 ART 和 Android 组件,将丢失专为 64 位优化过的性能(ART,webview,media 等等)。
过减少其他 CPU 类型支持的 SO 库来减少 APK 的体积不是很明智的做法,如果真的需要通过减少 SO 库来做 APK 瘦身,我们也有其他办法。
减少 SO 库体积的正确姿势 1. 构建特定 ABI 支持的 APK
我们可以构建一个 APK,它支持所有的 CPU 类型。但是反过来,我们可以为每个 CPU 类型都单独构建一个 APK,然后不同 CPU 类型的设备安装对应的 APK 即可,当然前提是应用市场得提供用户设备 CPU 类型设别的支持,就目前来说,至少 PLAY 市场是支持的。
Gradle 可以通过以下配置生成不同 ABI 支持的 APK(引用自别的文章,没实际使用过):
- android {...splits {
- abi {
- enable true reset() include 'x86',
- 'x86_64',
- 'armeabi-v7a',
- 'arm64-v8a' //select ABIs to build APKs for universalApk true //generate an additional APK that contains all the ABIs } } // map for the version code project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9] android.applicationVariants.all { variant -> // assign different version code for each output variant.outputs.each { output -> output.versionCodeOverride = project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode } } }
2. 从网络下载当前设备支持的 SO 库
说到这里,总算回到动态加载的主题了。⊙﹏⊙
使用 Android 的动态加载技术,可以加载外部的 SO 库,所以我们可以从网络下载 SO 库文件并加载了。我们可以下载所有类型的 SO 库文件,然后加载对应类型的 SO 库,也可以下载对应类型的 SO 库然后加载,不过无论哪种方式,我们最好都在加载 SO 库前,对 SO 库文件的类型做一下判断。
我个人的方案是,存储在服务器的 SO 库依然按照 APK 包的压缩方式打包,也就是,SO 库存放在 APK 包的 libs/xxxabi 路径下面,下载完带有 SO 库的 APK 包后,我们可以遍历 libs 路径下的所有 SO 库,选择加载对应类型的 SO 库。
具体实现代码看上去像是:
- /** * 将一个SO库复制到指定路径,会先检查改SO库是否与当前CPU兼容 * * @param sourceDir SO库所在目录 * @param so SO库名字 * @param destDir 目标根目录 * @param nativeLibName 目标SO库目录名 * @return */
- public static boolean copySoLib(File sourceDir, String so, String destDir, String nativeLibName) throws IOException {
- boolean isSuccess = false;
- try {
- LogUtil.d(TAG, "[copySo] 开始处理so文件");
- if (Build.VERSION.SDK_INT >= 21) {
- String[] abis = Build.SUPPORTED_ABIS;
- if (abis != null) {
- for (String abi: abis) {
- LogUtil.d(TAG, "[copySo] try supported abi:" + abi);
- String name = "lib" + File.separator + abi + File.separator + so;
- File sourceFile = new File(sourceDir, name);
- if (sourceFile.exists()) {
- LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath());
- isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so); //api21 64位系统的目录可能有些不同 //copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + name); break; } } } else { LogUtil.e(TAG, "[copySo] get abis == null"); } } else { LogUtil.d(TAG, "[copySo] supported api:" + Build.CPU_ABI + " " + Build.CPU_ABI2); String name = "lib" + File.separator + Build.CPU_ABI + File.separator + so; File sourceFile = new File(sourceDir, name); if (!sourceFile.exists() && Build.CPU_ABI2 != null) { name = "lib" + File.separator + Build.CPU_ABI2 + File.separator + so; sourceFile = new File(sourceDir, name); if (!sourceFile.exists()) { name = "lib" + File.separator + "armeabi" + File.separator + so; sourceFile = new File(sourceDir, name); } } if (sourceFile.exists()) { LogUtil.i(TAG, "[copySo] copy so: " + sourceFile.getAbsolutePath()); isSuccess = FileUtil.copyFile(sourceFile.getAbsolutePath(), destDir + File.separator + nativeLibName + File.separator + so); } } if (!isSuccess) { LogUtil.e(TAG, "[copySo] 安装 " + so + " 失败 : NO_MATCHING_ABIS"); throw new IOException("install " + so + " fail : NO_MATCHING_ABIS"); } } catch (IOException e) { e.printStackTrace(); throw e; } return true;}
总结
题外话,SO 库的使用本身就是一种最纯粹的动态加载技术,SO 库本身不参与 APK 的编译过程,使用 JNI 调用 SO 库里的 Native 方法的方式看上去也像是一种 "硬编程",Native 方法看上去与一般的 Java 静态方法没什么区别,但是它的具体实现却是可以随时动态更换的(更换 SO 库就好),这也可以用来实现热修复的方案,与 Java 方法一旦加载进内存就无法再次更换不同,Native 方法不需要重启 APP 就可以随意更换。
出于安全和生态控制的原因,Google Play 市场不允许 APP 有加载外部可执行文件的行为,一旦你的 APK 里被检查出有额外的可执行文件时就不好玩了,所以现在许多 APP 都偷偷把用于动态加载的可执行文件的后缀名换成 ".so",这样被发现的几率就降低了,因为加载 SO 库看上去就是官方合法版本的动态加载啊(不然 SO 库怎么工作),虽然这么做看起来有点掩耳盗铃。
就爱阅读 www.92to.com 网友整理上传, 为您提供最全的知识大全, 期待您的分享,转载请注明出处。
来源: