常和 SO 库开发打交道的同学来说已经是老生长谈,但是既然要讨论一整个动态加载系列,我想还是有必要说说使用 SO 库时的一些问题。

在项目里使用 SO 库非常简单,在 加载 SD 卡中的 SO 库 中也有谈到,只需要把需要用到的 SO 库拷贝进 jniLibs(或者 Eclipse 项目里面的 libs) 中,然后在 JAVA 代码中调用 System.loadLibrary(“xxx”) 加载对应的 SO 库,就可以使用 JNI 语句调用 SO 库里面的 Native 方法了。

但是有同学注意到了,SO 库文件可以随便改文件名,却不能任意修改文件夹路径,而是 “armeabi”、“armeabi-v7a”、“x86” 等文件夹名有着严格的要求,这些文件夹名有什么意义么?

SO 库类型和 CPU 架构类型

原因很简单,不同 CPU 架构的设备需要用不同类型 SO 库(从文件名也可以猜出来个大概嘛 ╮( ̄▽ ̄”)╭)。

记得还在学校的时候,提及 ARM 处理器时,老师说以后移动设备的 CPU 基本就是 ARM 类型的了。老师不曾欺我,早期的 Android 系统几乎只支持 ARM 的 CPU 架构,不过现在至少支持以下七种不同的 CPU 架构:ARMv5,ARMv7,x86,MIPS,ARMv8,MIPS64 和 x86_64。每一种 CPU 类型都对应一种 ABI(Application Binary Interface),“armeabi-v7a”文件夹前面的 “armeabi” 指的就是 ARM 这种类型的 ABI,后面的 “v7a” 指的是 ARMv7。这 7 种 CPU 类型对应的 SO 库的文件夹名是:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64。

不同类型的移动设备在运行 APP 时,需要加载自己支持的类型的 SO 库,不然就 GG 了。通过 Build.SUPPORTED_ABIS 我们可以判断当前设备支持的 ABI,不过一般情况下,不需要开发者自己去判断 ABI,Android 系统在安装 APK 的时候,不会安装 APK 里面全部的 SO 库文件,而是会根据当前 CPU 类型支持的 ABI,从 APK 里面拷贝最合适的 SO 库,并保存在 APP 的内部存储路径的 libs 下面。(这里说一般情况,是因为有例外的情况存在,比如我们动态加载外部的 SO 库的时候,就需要自己判断 ABI 类型了。)

一种 CPU 架构 = 一种对应的 ABI 参数 =  一种对应类型的 SO 库

到这里,我们发现使用 SO 库的逻辑还是比较简单的,但是 Android 系统加载 SO 库的逻辑还是给我们留下了一些坑。

使用 SO 库时要注意的一些问题

1. 别把 SO 库放错地方

SO 库其实都是 APP 运行时加载的,也就是说 APP 只有在运行的时候才知道 SO 库文件的存在,这就无法通过静态代码检查或者在编译 APP 时检查 SO 库文件是否正常。所以,Android 开发对 SO 库的存放路径有严格的要求。

使用 SO 库的时候,除了 “armeabi-v7a” 等文件夹名需要严格按照规定的来自外,SO 库要放在项目的哪个文件夹下也要按照套路来,以下是一些总结:

  • Android Studio 工程放在 jniLibs/xxxabi 目录中(当然也可以通过在 build.gradle 文件中的设置 jniLibs.srcDir 属性自己指定);
  • Eclipse 工程放在 libs/xxxabi 目录中(这也是使用 ndk-build 命令生成 SO 库的默认目录);
  • aar 依赖包中位于 jni/ABI 目录中(SO 库会自动包含到引用 AAR 压缩包到 APK 中);
  • 最终构建出来的 APK 文件中,SO 库存在 lib/xxxabi 目录中(也就是说无论你用什么方式构建,只要保证 APK 包里 SO 库的这个路径没错就没问题);
  • 通过 PackageManager 安装后,在小于 Android 5.0 的系统中,SO 库位于 APP 的 nativeLibraryPath 目录中;在大于等于 Android 5.0 的系统中,SO 库位于 APP 的 nativeLibraryRootDir/CPU_ARCH 目录中;

既然扯到了这里,顺便说一下,我在使用 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 库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(引用自别的文章,没实际使用过):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 库。

具体实现代码看上去像是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* 将一个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;
}

总结

  1. 一种 CPU 架构 = 一种 ABI = 一种对应的 SO 库;
  2. 加载 SO 库时,需要加载对应类型的 SO 库;
  3. 尽量提供全平台 CPU 类型的 SO 库支持;

题外话,SO 库的使用本身就是一种最纯粹的动态加载技术,SO 库本身不参与 APK 的编译过程,使用 JNI 调用 SO 库里的 Native 方法的方式看上去也像是一种 “硬编程”,Native 方法看上去与一般的 Java 静态方法没什么区别,但是它的具体实现却是可以随时动态更换的(更换 SO 库就好),这也可以用来实现热修复的方案,与 Java 方法一旦加载进内存就无法再次更换不同,Native 方法不需要重启 APP 就可以随意更换。

出于安全和生态控制的原因,Google Play 市场不允许 APP 有加载外部可执行文件的行为,一旦你的 APK 里被检查出有额外的可执行文件时就不好玩了,所以现在许多 APP 都偷偷把用于动态加载的可执行文件的后缀名换成 “.so”,这样被发现的几率就降低了,因为加载 SO 库看上去就是官方合法版本的动态加载啊(不然 SO 库怎么工作),虽然这么做看起来有点掩耳盗铃。

Android使用 SO 库时要注意的一些问题的更多相关文章

  1. Android使用SO库时要注意的一些问题

    转自:https://segmentfault.com/a/1190000005646078 正好动态加载系列文章谈到了加载SO库的地方,我觉得这里可以顺便谈谈使用SO库时需要注意的一些问题.或许这些 ...

  2. 【转】用JitPack发布开源库时附加文档和源码

    来自:http://www.gcssloop.com/course/jitpack-sources-javadoc 用JitPack发布开源库时附加文档和源码 很早之前写过一篇用JitPack发布An ...

  3. Android M Permission 运行时权限 学习笔记

    Android M Permission 运行时权限 学习笔记 从Android 6.0开始, 用户需要在运行时请求权限, 本文对运行时权限的申请和处理进行介绍, 并讨论了使用运行时权限时新老版本的一 ...

  4. dlmalloc(Android bionic C库的malloc实现)简介

    欢迎转载opendevkit文章, 文章原始地址: http://www.opendevkit.com/?e=56 Dlmalloc是目前一个十分流行的内存分配器,其由Doug Lea从1987年开始 ...

  5. Android 第三方开源库收集整理(转)

    原文地址:http://blog.csdn.net/caoyouxing/article/details/42418591 Android开源库 自己一直很喜欢Android开发,就如博客签名一样,  ...

  6. Android进阶笔记02:Android 网络请求库的比较及实战(二)

    一.Volley        既然在android2.2之后不建议使用HttpClient,那么有没有一个库是android2.2及以下版本使用HttpClient,而android2.3及以上版本 ...

  7. Android media媒体库分析之:MediaProvider

    在做Android媒体应用程序时(Audio.Image.Video)需要对Android的媒体提供者(MediaProvider)做详细的分析,下面记录一下我的收获: 一.获取MediaProvid ...

  8. Android引入动态库so的方法

    Android引入动态库so的方法 标签(空格分隔): Android so 第三方库 为了执行效率,会将一些CPU密集性任务如音视频解码.图像处理等放入到so中,还有也会将程序关键核心部分放入到so ...

  9. findlibrary returned null产生的联想,Android ndk开发打包时我们应该怎样注意平台的兼容(x86,arm,arm-v7a)

    非常多朋友在开发Android JNI的的时候,会遇到findlibrary returned null的错误,由于某种原因,so没有打包到apk中.以下浅析下引起该错误的原因以及平台兼容性问题. A ...

随机推荐

  1. regex & tab spaces

    regex & tab spaces txt file format let x = `2018-10-20a2018-10-20a2018-10-20a`; x.replace(/a/ig, ...

  2. 【bzoj4562】[Haoi2016]食物链 拓扑排序+dp

    原文地址:http://www.cnblogs.com/GXZlegend/p/6832118.html 题目描述 如图所示为某生态系统的食物网示意图,据图回答第1小题 现在给你n个物种和m条能量流动 ...

  3. [洛谷P3807]【模板】卢卡斯定理

    题目大意:给你$n,m,p(p \in \rm prime)$,求出$C_{n + m}^m\bmod p(可能p\leqslant n,m)$ 题解:卢卡斯$Lucas$定理,$C_B^A\bmod ...

  4. BZOJ2957 楼房重建 【线段树】

    题目 小A的楼房外有一大片施工工地,工地上有N栋待建的楼房.每天,这片工地上的房子拆了又建.建了又拆.他经常无聊地看着窗外发呆,数自己能够看到多少栋房子. 为了简化问题,我们考虑这些事件发生在一个二维 ...

  5. Codeforces Round #352 (Div. 2) C

    C. Recycling Bottles time limit per test 2 seconds memory limit per test 256 megabytes input standar ...

  6. 理想中的SQL语句条件拼接方式 (二)

    问题以及想要的效果,不重复叙述,如果需要的请先看 理想中的SQL语句条件拼接方式 . 效果 现在有2个类映射数据库的2张表,结构如下: public class User { public int U ...

  7. 除了IE浏览器,其他浏览器都联不上网怎么办~转载百度经验

    百度师傅最快的到家服务,最优质的电脑清灰 百度经验:jingyan.baidu.com 有个网友遇到一个非常奇怪的上网问题,刚才始,发现QQ不能登录,后来接着发现火狐浏览器也打不开网页,刚开始,以为只 ...

  8. [9018_1963][IOI_1998]Picture

    题目描述 N(N<5000) 张矩形的海报,照片和其他同样形状的图片贴在墙上.它们的边都是垂直的或水平的.每个矩形可以部分或者全部覆盖其他矩形.所有的矩形组成的集合的轮廓称为周长.写一个程序计算 ...

  9. Linux有名信号量的创建(sem_open中name参数构造)【转】

    转自:http://blog.csdn.net/gfeng168/article/details/40740865 版权声明:本文为博主原创文章,未经博主允许不得转载. 一.sem_open函数nam ...

  10. (二十三)深入了解epoll (转)

    一. 介绍Epoll 是一种高效的管理socket的模型,相对于select和poll来说具有更高的效率和易用性.传统的select以及poll的效率会因为 socket数量的线形递增而导致呈二次乃至 ...