APK安装流程系列文章整体内容如下:
本片文章的主要内容如下:
1、ABI简介
2、PackageManagerService#derivePackageAbi(PackageParser.Package, File,String, boolean)方法解析
3、PackageManagerService#setNativeLibraryPaths(PackageParser.Package)方法分析
一、ABI简介
ABI全程是:Application binary interface,即:应用程序二进制接口,它定义了一套规则,允许编译好的二进制目标代码在所兼容该ABI的操作系统和硬件平台中无需改动就能运行。
不同的Android手机使用不同的CPU,因此支持不同的指令集。CPU与指令集的每种组合都有其自己的应用二进制接口(或ABI)。"ABI"精确定义了"运行时,应用的机器码和系统的交互方式"。你必须为应用要使用每个CPU架构指定ABI。
典型的ABI包含以下信息:
1、机器代码应使用的CPU指令集
2、运行时内存存储和加载的字节顺序
3、可执行二进制文件(例如程序和共享库)的格式,以及它们支持的内容类型
4、用于解析内容与系统之间的数据的各种约定。这些约定包括对齐限制,以及系统如何使用堆栈和在调用函数时注册。
5、运行时可用于机器代码的函数符号列表 - 通常来自非常具体的库集。
由上述定义可以判断:
ABI定义了规则,而具体的实现是由编译器、CPU、操作系统共同来完成的。不同的CPU芯片(如:ARM、Intel x86、MIPS)支持不同的ABI架构,常见的ABI类型包含:armabi、armabi-v7a、x86、x86_64、mips、mips64、arm64-v8a等。
这也就是为什么我们编译出的运行于windows的二进制程序不能运行于Mac OS/Linux/Android平台了,因此CPU芯片和操作系统均不相同,支持的ABI类型也不一样,因此无法识别对方的二进制程序。
而我们说的"交叉编译"的核心原理也跟这些密切相关,交叉编译,就是使用交叉编译工具,在平台上编译生成另一个平台的二进制可执行程序,为什么可以做到?因为交叉编译工具实现了另一个平台所定义的ABI规则。我们在Window/Linux平台使用Android NDK交叉编译工具来编译出Android平台的库也是这个道理。
(一)、.so文件与ABI
如果你的项目中使用了NDK,它就生成了.so文件。如果你的项目只使用了Java语言进行编程,可能就不太关注so文件了。因为Java是跨平台的。但是其实项目中的依赖函数库或者引擎库已经嵌入了so文件。并依赖不同的ABI,比如项目中使用了百度地图,里面就会涉及相应的so文件。
Android应用支持的ABI取决于APK中位于lib/ABI目录中的so文件,其中
ABI可能是上面说过的其中ABI的一种
(二)、关于so文件的一些补充
1、so文件的重要法则
处理so文件时有一条简单但却很重的法则:
应该尽最大可能为每个ABI提供经过优化过的.so文件,且最好不要混合着使用。即你应该为每个ABI目录提供对应的so文件。
2、NDK兼容性
使用NDK时,一般人会倾向于使用最新的编译凭条,但实际上这样做是有问题的。因为NDK平台是不向后兼容的,而是向前兼容的。所以推荐使用APP的minSdkVersion对应的编译平台。这也意味着当你引入一个预编译好的.so文件时,你需要检查它被编译所用的平台版本。
3、混合使用不同的编译的so文件
so文件可以依赖于不同的C++运行时,静态编译或者动态加载,混合使用不同版本的C++运行时可能会导致很多奇怪的crash。最好避免这种情况。
PS:当只有一个so文件时,静态编译C++运行时是没有问题的。但是当存在多个so文件时,应该让所有so文件都动态链接相同的C++运行时。这意味着当引入一个新的预编译so文件,而且项目中还存在其他so文件时,我们需要首先确认新引入的so文件使用的C++运行时是否已经存在的so文件一致。
(三)、ABI和CPU的关系
1、Android CPU的基础知识
C++代码必须根据Android 设备的CPU类型(通常称为"ABIs")进行编译,常用的五种 ABI:
armeabiv-v7a:第七代及以上ARM处理器。2011年以后的生产的大部分Android设备都是用它。
arm64-v8a:第8代、64位ARM处理器,设备不多,比如三星Galaxy S6
armeabi:第5代、第6代ARM处理器,早期的手机用的比较多。
x86:平台、模拟器用得比较多。
x86_64:64位的平板。
2、 ABI支持CPU列表
ABI支持CPU列表,如下:
ABI支持CPU列表.png
举例说明:
在x86设备上,选择ABI的先后顺序
第一步:在libs/x86目录中如果存在.so文件的话,会被安装,如果没有走第二步。
第二步:会在armeabi-v7a中的.so文件,如果有,会被安装,如果没有会走第三步。
第三步:会在armeabi目录中的.so文件寻找
PS:x86设备能够很好的运行ARM类型函数库,但并不保证100% 发生crash,特别是对旧设备,因为是运行在x86设备上模拟ARM的虚拟层上。
3、 ABI支持CPU的知识点
1、大部分CPU都支持多余一种的ABI
2、 当一个应用安装在设备上,只有设备支持的CPU架构对应的.so文件会被安装。
3、64位设备(arm64-v8a、x86_64、mips64)能够运行32位的函数库,但是以32位版本的ART和Android组件,将丢失64位优化过的性能(ART、webview、media等等)。
4、最好针对特定平台提供相应平台的二进制包,这种情况下运行时就少了一个模拟层(例如x86设备上模拟arm模拟层),从而得到更好的性能(归功与最近的架构更新,例如硬件fpu,更多的寄存器,更好的向量化)。
5、会优先安装优先级较高的ABI目录,则其他优先级较低的ABI目录(包括其他module中的ABI目录),都无法安装。例如:在cpu是ARMv7架构的手机上,如果检测到armeabi-v7a,就会选择安装armeabi-v7a,则armeabi下的文件,就无法安装了。
6、相应的ABI二进制文件,要放进相应的ABI目录中
7、一般情况下不要随便修改架构目录名
(四)、常见问题:
1、so文件 放进了优先级低的ABI目录
问题:
如果你的项目中,有其他优先级更好的ABI目录,但是你把ABI文件方法放到了优先级低的目录,最后导致你的ABI文件无法被加载
举例:
某手机CPU架构是ARMv7,ABI文件是armeabi-v7a,但是放进了armeabi目录中:
导致结果
项目中有armeabi-v7a的目录,armeabi目录的文件,无法被加载,然后运行报错,出现类似于如下log信息。
Caused by: java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/.xx../base.apk"],nativeLibraryDirectories=[/data/app/.xx../lib/arm, /vendor/lib, /system/lib]]] couldn't find "lib..xx...so"
解决方案:建议armeabi-v7a的目录下的文件和armeabi目录的文件保持一致。
2、两个第三方SDK中的ABI文件优先级不一样
问题:
两个第三方的SDK中ABI文件优先级不一样,手机加载运行时,会导致优先级低的库,无法被加载。
例子:
某手机CPU架构是ARMv7,项目中使用了两个第三方SDK:假设是"支付宝"和"银联".
支付宝:ABI文件是armeabi-v7a,所以放到armeabi-v7a目录中。
银联:ABI文件是armeabi,所以放到armeabi目录中。
导致结果:
在运行时,会发现运行后crash,出现如下日志:
Caused by: java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/.xx../base.apk"],nativeLibraryDirectories=[/data/app/.xx../lib/arm, /vendor/lib, /system/lib]]] couldn't find "lib..xx...so"
解决方案:
解决方案1:
使用同一优先级的ABI文件,ABI文件放入到优先级相同的ABI目录
比如:
支付宝:ABI文件是armeabi-v7a,放到armeabi-v7a目录中。
银联:ABI文件是armeabi-v7a,放到armeabi-v7a目录中。
或
支付宝:ABI文件是armeabi,放到armeabi目录中。
银联:ABI文件是armeabi,放到armeabi目录中。
解决方案2:
如果两个第三方提供的是不同优先级的ABI文件,则将ABI文件放入到优先级相同的ABI。
比如:
支付宝:ABI文件是armeabi-v7a,放到armeabi目录中。
银联:ABI文件是armeabi,放到armeabi目录中。
二、 PackageManagerService#derivePackageAbi(PackageParser.Package, File,String, boolean)方法解析
这个方法在PackageManagerService的installPackageLI方法里面被调用。代码在代码在PackageManagerService.java 12442行
private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
...
derivePackageAbi(pkg, new File(pkg.codePath), args.abiOverride,true /* extract libs */);
...
}
在讲解这个方法的时候我们先来了解一个概念是"primaryCpuAbi",关于ABI的概念可以参考我的文章Android ABI简介 ,那"primaryCpuAbi"又是什么?
因为一个系统支持的ABI有很多,不止一个,比如一个64位的机器上它的supportAbiList,可能如下所示:
public static final String[] SUPPORTED_ABIS = getStringList("ro.product.cpu.abilist", ",");
root@:/ # getprop ro.product.cpu.abilist
arm64-v8a,armeabi-v7a,armeabi
所以它能支持的abi有如上的3个,这个primaryCpuAbi就是要知道当前程序的abi在他支持的abi中最靠前的的哪一个。同时依靠这个primaryCpuAbi的值可以决定我们的程序是运行在32位还是64位的。
那我们来看下derivePackageAbi这个方法的内部实现
代码在PackageManagerService.java 7553行
/**
* Derive the ABI of a non-system package located at {@code scanFile}. This information
* is derived purely on the basis of the contents of {@code scanFile} and
* {@code cpuAbiOverride}.
*
* If {@code extractLibs} is true, native libraries are extracted from the app if required.
*/
public void derivePackageAbi(PackageParser.Package pkg, File scanFile,
String cpuAbiOverride, boolean extractLibs)
throws PackageManagerException {
// TODO: We can probably be smarter about this stuff. For installed apps,
// we can calculate this information at install time once and for all. For
// system apps, we can probably assume that this information doesn't change
// after the first boot scan. As things stand, we do lots of unnecessary work.
// Give ourselves some initial paths; we'll come back for another
// pass once we've determined ABI below.
// *********** 第一步 ***********
// 设置so库的安装路径
setNativeLibraryPaths(pkg);
// We would never ne