一般情况下,我们不需要关心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管理。
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:=全部
查看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手机上。
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/
0条评论