问题由来
最近项目里添加了64位动态库,即app/libs
下的armeabi
和arm64-v8a
两个存储动态库的目录。项目还有拷贝动态库的需要,即:从apk中将动态库拷贝到指定目录下,然后通过System.load()
来加载。问题是:
应该拷贝apk中哪个版本的动态库呢?armeabi
还是arm64-v8a
?
简单的回答:主要得看应用进程是被哪个zygote创建出来的。
应用进程被哪个zygote创建
Android系统从5.0开始支持64bit CPU,于是系统就有了zygote和zygote64两个进程来分别创建32和64位的应用进程。怎么确定我的应用进程由哪个zygote启动的?
简单验证一下:使用$adb shell ps | grep zygote
能看到两个zygote和zygote64两个进程。再使用$adb shell ps | grep <package_name>
就能找到应用进程ID和父进程ID。如下图:
$ adb shell ps | grep zygote
root 511 1 8760 768 __skb_recv 0000000000 S zygote
root 746 1 2118616 27044 poll_sched 0000000000 S zygote64
root 747 1 1554780 14612 poll_sched 0000000000 S zygote
$ adb shell ps | grep <package_name>
u0_a108 4162 746 1780048 95476 SyS_epoll_ 0000000000 S <package_name>
从输出中看到应用的父进程PID是746,也就是说是被zygote64给fork出来的,就是说运行时环境是64位。
功能实现方案
要写一个功能函数来判断当前运行环境是64bit还是32bit,怎么实现呢?
最先想到的方法就是获取到父进程PID,然后查到父进程名字,拿名字跟zygote64
和zygote
字符串作对比。
问题是系统没提供相关的接口(在Java中启动ps进程,然后分析输出日志应该可以达到目的,但是实现起来比较麻烦)。
另外,从/proc/<pid>/cmdline
中也能看到进程名,但是应用进程没有权限读取zygote进程的cmdline。
有没有系统接口呢?
有。但是只在6.0及以后的SDK上才提供。5.0上是不能用的。
加载动态库的细节
回过头来想一个问题:应用进程被zygote或zygote64启动是由什么因素决定的?
主要有三个:
-
CPU ABI,就是CPU支持或兼容哪些架构
- CPU支持的架构主要包括armeabi、armeabi-v7a、arm64-v8a、x86、mips。
- Android OS,即系统是否支持64位运行环境
- 5.0以上开始支持64bit CPU
- 应用的动态库类型
- 应用程序本身提供的动态库类型。比如,我的工程里有armeabi和arm64-v8a两种。(如果应用不携带任何动态库,运行时环境由系统自己来判断)。
然后就是系统启动应用时的决策过程和逻辑:
决策过程可以简单概括为:
- PackageManagerService扫描应用程序时,先查看lib目录下是否有动态库,有哪种类型的动态库。例如,在64位设备上:
- 如果app的 lib 目录下存在armeabi,则以32位兼容方式运行
- 存在arm64-v8a 则以64位运行
- 如果没有任何 so,按照系统的默认配置决定,也就是64位运行。
以上决策过程的结果记录在ApplicationInfo.primaryCpuAbi和ApplicationInfo.secondaryCpuAbi两个Filed上。
ActivityManagerService
中startProcessLocked()
方法实现启动应用。
- 这个方法最终是向相应的zygote进程发出fork的请求
zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote)
。其中的参数abi就是第二步中提到的ApplicationInfo.primaryCpuAbi和secondaryCpuAbi。ABI参数不同,使用到的zygote进程也不同。
- 在确定zygote进程后,要加载的动态库类型就能确定了。zygote64会加载64bit的动态库,例如,
/system/lib64/libart.so
,还有应用程序的arm64-v8a下的动态库。
所以,可以通过查看libart.so的路径来判断zygote的类型;也可以判断手机的CPU ABI来判断。
最终方案:
- 6.0+:使用系统接口(since Android M)
android.os.Process.is64Bit()
- 5.x:有两个判断方法
- ① 根据应用进程依赖的libart.so的实际路径来判断:
- 路径是"/system/lib64/libart.so" 对应 zygote64
- 路径是"/system/lib/libart.so" 对应 zygote
- ② 根据CPU ABI来判断
- 根据
Build.SUPPORTED_ABIS
返回的数组第一个值来判断- arm64-v8a 对应 zygote64
- armeabi-v7a,armeabi等 对应 zygote
- 根据
- ① 根据应用进程依赖的libart.so的实际路径来判断:
- 4.x:只能是32位环境。
(5.0上)详细实现如下:
方法① 查找libart.so的路径
使用ClassLoader的findLibrary方法来查找libart.so的路径;该方法是隐藏函数,所以需要使用反射。
说明:zygote64会加载/system/lib64/libart.so
,zygote会加载"/system/lib/libart.so"
。
private static boolean isART64() {
final String tag = "is64ART";
final String fileName = "art";
try {
ClassLoader classLoader = getContext().getClassLoader();
Class<?> cls = ClassLoader.class;
Method method = cls.getDeclaredMethod("findLibrary", String.class);
Object object = method.invoke(classLoader, fileName);
if (object != null) {
return ((String)object).contains("lib64");
}
} catch (Exception e) {
//如果发生异常就用方法②
return is64bitCPU();
}
return false;
}
方法② 根据CPU ABI来判断
private static boolean is64bitCPU() {
String CPU_ABI = null;
if (Build.VERSION.SDK_INT >= 21) {
String[] CPU_ABIS = Build.SUPPORTED_ABIS;
if (CPU_ABIS.length > 0) {
CPU_ABI = CPU_ABIS[0];
}
} else {
CPU_ABI = Build.CPU_ABI;
}
if (CPU_ABI != null && CPU_ABI.contains("arm64")) {
return true;
}
return false;
}
这个方法可能有一个缺陷:如果将32位的ROM安装在支持arm64-v8a的机器上,查询SUPPORTED_ABIS
时依然得到的是arm64-v8a,而此时系统只能以32位环境来运行应用,这就出问题了。
但是,暂时没法验证这个种case,不好找这样的ROM,因为厂商一般不会这么做。所以,这种情况暂时可以不用考虑。
参考文章
Android 64位兼容方式运行32位分析
腾讯Bugly干货分享】动态链接库加载原理及HotFix方案介绍
Android系统启动-zygote篇
理解Android进程创建流程