一般情况下,我们不需要关心so。但是当APP使用的第三方SDK中包含了so文件,或者自己需要使用NDK开发某些功能,就有必要去好好了解下so的一些知识。

极光的产品中用到了so文件,在社区也看到一些小伙伴遇到so文件出错的问题,正好看到这篇讲so的帖子,分享一下。

一般情况下,我们不需要担心so。但是当APP使用的第三方SDK中包含了so文件,或者自己需要使用NDK开发某些功能,就有必要去好好了解下so的一些知识。

出处: 艾伦专区
作者: 冯艾伦

什么是ABI和so

早期的Android设备只支持ARMv5的CPU架构,通过Android系统的快速发展,搭载Android的硬件平台也早已发生了,又加入了ARMv7,x86,MIPS,ARMv8,MIPS64和x86_64。
每一种CPU架构,都定义了一种ABI(应用二进制接口,应用二进制接口),ABI定义了其所对应的CPU架构能够执行的二进制文件(如.so文件)的格式规范,决定了二进制文件如何与系统进行交互。每一种ABI的详细介绍可以参见官方的介绍ABI管理
null

so( 共享 库,共享库)是机器可以直接运行的二进制代码,是Android上的动态链接库,是Windows上的dll。每一个Android应用所支持的ABI是由其APK提供的.so文件决定的,这些so文件被打包在apk文件的lib /目录下,其中 abi 可以是上面表格中的一个
或多个。例如,解压一个apk文件后,在lib目录下可以看到如下文件:

LIB
├──阿米拉比
│└──libmath.so
├──armeabi-v7a
│└──libmath.so
├──ps
│└──libmath.so
└──x86
└──libmath.so

说明该应用所支持的ABI为armeabi,armeabi-v7a,mips和和x86。

注:可以使用 aapt 命令快速查看apk支持的abi

〜aapt dump徽章baidutieba.apk grep abi
本机代码:“armeabi” “MIPS” “86

为什么使用so

  • so机制让开发者最大化利用现有的C和C ++代码,达到重用的效果,利用软件世界积累了几十年的优秀代码;
  • so是二进制,没有解释编译的开消,用so实现的功能比纯java实现的功能要快;
  • so内存分配无限制Dalivik / ART的单独应用限制,减少了OOM;
  • 相对于Java代码,二进制代码的反编译缺点以及一些核心代码可以考虑放在中中。

为指定的ABI生成so

默认情况下,NDK只会为 armeabi 生成.so文件,如果需要生成支持其他ABI的.so文件,可以在 Application.mk 文件中指定 APP_ABI 参数:

APP_ABI:= armeabi-v7a

APP_ABI 参数可以被指定多个值以支持多个ABI:

APP_ABI:= armeabi armeabi-v7a x86

当然,你也可以使用 all 来生成支持所有ABI的so:

APP_ABI:=全部

null

查看Android系统的ABI支持

Android可以在运行期间确定当前系统所支持的ABI,这是由系统编译时的具体参数指定的:

  • primary ABI (主ABI):对应当前系统中使用的机器码类型
  • secondary ABI (副ABI):表示当前系统支持的其他ABI类型

许多手机支持不止一个ABI,小型,一个基于ARMv7的设备切换armeabi-v7a定义为 primary ABI ,armeabi作为 secondary ABI ,意味着这台机器同时支持armeabi-v7a和armeabi。
许多基于x86的设备也可以运行armeabi-v7a和armeabi的so,对于这些机器, 主要ABI 是x86, 次要ABI 则是armeabi-v7a。

但是,为了能获得更好的性能表现,我们应该适当的直接提供primary ABI所对应的so文件。例如,我们可以为x86手机直接提供x86的so文件,而不是仅提供arm的so让系统通过 houdini 去动态转换手臂指令,避免转换过程中的性能损耗。

查看Android系统支持的ABI有以下两种方法:

使用adb命令

/system/build.prop 中指定了支持的ABI类型,在adb中,可使用如下命令查看:

shell @ NX529J:/ $ getprop grep abilist
[ro.product.cpu.abi]:[arm64-v8a]
[ro.product.cpu.abilist32]:[armeabi-v7a,armeabi]
[ro.product.cpu.abilist64]:[arm64-v8a]
[ro.product.cpu.abilist]:[arm64-v8a,armeabi-v7a,armeabi]

使用API​​获取

使用Build.SUPPORTED_ABIS可以获取当前设备支持的ABI列表:

导入 android.os.Build;

字符串支持Abis = Build.SUPPORTED_ABIS;

x86手机对arm的支持

值得注意的是原本的x86架构的CPU是不支持运行武装架构的原生代码的,但英特尔和谷歌合作在x86的机子的系统内核层之上加入了一个名为 胡迪尼 的二进制翻译(二进制转换中间层),这个中间层会在运行期间动态的读取臂指令转换为x86指令去执行。所以能看到很多没有提供x86对应so的应用(如新浪微博)也能够运行在x86手机上。
null

apk安装过程中对so的选择

在Android上安装应用程序时,Package Manager会扫描整个apk文件,查找符合以下文件路径格式的动态连接库:

lib / <primary-abi> / lib <name> .so

在这里, primary-abi 是上面表中的abi的值,对应 name 的是我们在 Android.mk 中定义的 LOCAL_MODULE 的值,

如果在Apk内并没有找到适合当前机器的 primary-abi 的so,Package Manager会 尝试 寻找适合 secondary-abi 的so文件:

 lib / <secondary-abi> / lib <name> .so

即安装应用时,系统会根据当前CPU架构选择最佳的ABI调整,如果找到了合适的so文件,包管理器会进入ABI文件夹下所有so库全部拷贝至应用的数据目录下: data/data/<package_name>/lib/

注意:apk安装过程对so选择 是基于整个ABI文件夹的,而不是以单独的so文件为粒度 ,从而把lib / armeabi,lib / armeabi-v7a,lib / x86等文件夹的其中一个文件夹内所有.so复制到应用的数据目录下。

如果我们在代码中调用了某个so的功能,而最终拷贝的ABI文件夹下并没有提供这个文件,apk的安装过程中并不会报错,但是运行时会遇到 java.lang.UnsatisfiedLinkError

所以的加载

对于so的加载,Android在 System 类中提供了两种方法:

 / **
*请参见{ @link Runtime#loadLibrary}。
* /
公共 静态 void loadLibrary (字符串libName) {
Runtime.getRuntime()。loadLibrary(libName,VMStack.getCallingClassLoader());
}
/ **
*请参见{ @link Runtime#load}。
* /
公共 静态 无效 负载(字符串pathName) {
Runtime.getRuntime()。load(pathName,VMStack.getCallingClassLoader());
}

System.loadLibrary

这是我们最常用的一个方法, System.loadLibrary 只需要传入所以在 Android.mk中 中定义的 LOCAL_MODULE 的值即可,
系统-会调用 System.mapLibraryName 把这个LIBNAME转化成对应平台的这样的全称并去尝试寻找这个如此加载。
比如我们的so文件全称为 libmath.so ,加载该动态库只需要替换 math 即可:

System.loadLibrary(“ math”);

系统负载

对于 System.load 方法,官方是这样介绍的:

从本地文件系统中将具有指定文件名的代码文件作为动态库加载。
filename参数必须是完整的路径名。

所以它为动态加载非APK打包期间内置的,从而文件提供了可能,也就是说可以使用这个方法来指定我们要加载的,以便文件的路径来动态的加载,以便文件。
比如我们在打包期间并不打包,以便文件,甚至在应用运行时将当前设备适用的so文件从服务器上下载下来,放在 /data/data/<package-name>/mydir 下,然后在使用so时调用:

System.load(“ / data / data / <程序包名称> /mydir/libmath.so”);

即可成功加载这个so,开始调用本地方法了。

其实loadLibrary和load最终都会调用nativeLoad(名称,装载程序,ldLibraryPath)方法,只是因为loadLibrary的参数指向的这样的文件名,所以,loadLibrary需要首先找到该文件的路径,然后加载这个so文件。
而load的参数是一个文件路径,所以它不需要去寻找这个文件路径,或者直接通过这个路径来加载so文件。

但是当我们把需要加载的so文件放在SdCard中,会发生什么呢?把上面so的路径改成 /mnt/sdcard/libmath.so ,再尝试加载时,会得到如下错误:

java.lang.UnsatisfiedLinkError:dlopen失败:无法映射“ /mnt/sdcard/libmath.so”段1:权限被拒绝

这是因为SD卡等外部存储路径是一种可拆卸的(已安装)不可执行(noexec)的存储介质,不能直接用作作为替换文件的运行目录,使用前应该把文件复制到APP内部存储下再运行。所以使用 System.load 加载so时要注意把so拷贝至 /data/data/<package-name>/ 下。

通过精简so来重构包大小

通过上面的介绍,我们知道x86,x86_64,armeabi-v7a,arm64-v8a设备都支持armeabi架构的因此,因此,通过可移除预先的so来重叠包大小是一个不错的选择。

按照ABI分别单独打包APK

我们可以选择在Google Play中上传指定ABI版本的APK,生成不同ABI版本的APK可以在build.gradle中进行如下配置:

 android {
//其他配置...
拆分{
abi {
启用true
重启()
包括'x86''armeabi''armeabi-v7a''mips' //选择ABI为
UniversalApk false //生成包含所有ABI的附加APK
}
}
}

只提供 armabi 的so

上面的方法需要应用市场提供用户设备CPU类型更识别的支持,在国内并不是一个十分适用的方案常用的处理方式是利用gradle产出中的abiFilters配置。
首先配置修改主工程 build.gradle 下的 abiFilters

 android {
//其他配置...
defaultConfig {
ndk {
abiFilters 'armeabi'
}
}
}

abiFilters后面的ABI类型即为要打包进apk的ABI类型,除此以外都不打包进apk里。
然后在项目的根目录下的 gradle.properties (没有的话新建一个)中加入下面这行:

android.useDeprecatedNdk = true

通过上面方法减少的apk体积是十分可观的,也是目前比较主流的处理方案。

进阶版方案

如果进一步,会发现上面的方案并不完美。首要是性能问题:使用兼容模式去运行arm架构的,那么会丢失专门为当前ABI优化过的性能;其次还有兼容性问题,虽然x86设备能兼容arm类型的函数库,但是并不意味着100%兼容,某些情况下还是会发生崩溃,所以x86的arm兼容只是一个 折中方案以便 最好地利用x86自身的性能和避免兼容性问题,最好我们的做法仍的英文专为 x86 提供对应的左右。
针对这些问题,我们可以采用一个相对更好的方案:让所有所以都来自于网路,应用下载服务器上的这么库后,利用 System.load 方法动态加载当前设备对应的so。

需要注意的问题

不要把so放错地方

首先要注意的是不要把另一个ABI下的so文件放在另一个ABI文件夹下(每个ABI文件夹下的so文件名是相同的,有可能会搞错)。

完善为所有ABI提供so

理想状况下,应该尽量为所有ABI都提供对应的,所以,这一点的好处我们已经在上面讨论过了:在可以发挥更好的性能的同时,可以减少由兼容带来的某些崩溃问题。当然,这一点要结合实际情况(如SDK提供的so不全,芯片市场占有率,apk包大小等)去考量,如果使用的如此本身就很小,我们大可为适合多的ABI都提供so 。
若是累积包大小等因素,可以结合 通过精简so来分段包大小分段 中提供的第三个方案来调整so的使用策略。

所有ABI文件夹提供的so要保持一致

这是一个非常容易出现的错误。
如果我们的应用选择了支持多个ABI,要非常注意:对于每个ABI下的so,但只能完全 支持,只能都不支持 。不应该混合着使用,而应该为每个ABI目录提供对应的.so文件。

先举个例子,Bugtags的so支持所有的ABI:

 库
├──arm64-v8a
│└──libBugtags.so
├──阿米拉比
│└──libBugtags.so
├──armeabi-v7a
│└──libBugtags.so
├──ps
│└──libBugtags.so
├──mips64
│└──libBugtags.so
├──x86
│└──libBugtags.so
└──x86_64
└──libBugtags.so

但不是所有开发者提供的so都支持所有ABI:

LIB
├──阿米拉比
│└──libImages.so
└──armeabi-v7a
└──libImages.so

如果不做任何设置,最终打出来的apk的lib目录会是这样的:

LIB
├──arm64-v8a
│└──libBugtags.so
├──阿米拉比
│├──libBugtags.so
│└──libImages.so
├──armeabi-v7a
│├──libBugtags.so
│└──libImages.so
├──ps
│└──libBugtags.so
├──mips64
│└──libBugtags.so
├──x86
│└──libBugtags.so
└──x86_64
└──libBugtags.so

参考上面 apk安装过程中对so的选择 分区,假设当前设备是x86机器,包管理器会先去lib / x86下查找,发现该文件夹是存在的,所以最终只有lib / x86下的so–即只有libBugtags.so会被安装。当尝试在运行期间加载 libImages.so 时,就会遇上下面常见的 UnsatisfiedLinkError 错误:

E / xxx(10674):java.lang.UnsatisfiedLinkError:dalvik.system.PathClassLoader [DexPathList [[zip文件“ /data/app/xxx-2/base.apk"]],nativeLibraryDirectories=[/data/app/xxx- 2 / lib / x86,/ vendor / lib,/ system / lib]]] 找不到“ libImages.so”
E / xxx(10674):位于java.lang.Runtime.loadLibrary(Runtime.java:366

所以,需要我们遵循这样的 准则

  • 对于so开发者:支持所有的平台,否则将会搞砸你的用户。
  • 对于so使用者:或者支持所有平台,或者都不支持。

然而,因为种种原因(遗留so,芯片市场占有率,apk包大小等),并非所有人都遵循这样的原则。

一种可行的处理方案是:取你所有的so库所支持的ABI的交集,可移除其他(可以通过上面介绍的 abiFilters 来实现)。
如上述的示例,最终生成的apk可以是:

 LIB

├──阿米拉比
│├──libBugtags.so
│└──libImages.so
└──armeabi-v7a
├──libBugtags.so
└──libImages.so

原作者:Allen Feng
原文链接:http://allenfeng.com/2016/11/06/what-you-should-know-about-android-abi-and-so/