写点什么

插件化框架解读之 so- 文件加载机制(四)

用户头像
Android架构
关注
发布于: 5 小时前

Q2,Q3,Q4,这几个问题都是基于设备支持 64 位的前提下,在旧系统版本中,只支持 32 位,也就没这么多疑问需要处理了。

源码

准备工作

由于这次的源码会涉及很多 framework 层的代码,包括 java 和 c++,直接在 AndroidStudio 跟进 SDK 的源码已不足够查看到相关的代码了。所以,此次是借助 Source Insight 软件,而源码来源如下:


https://android.googlesource.com/platform/


我并没有将所有目录下载下来,只下载了如下目录的源码:



我没有下载最新版本的代码,而是选择了 Tags 下的 More 按钮,然后选择 tag 为:?android-5.1.1 r24?的代码下载。所以,此次分析的源码是基于这个版本,其余不同版本的代码可能会有所不一样,但大体流程应该都是一致的。

分析

源码分析的过程很长很长,不想看过程的话,你也可以直接跳到末尾看结论,但就会错失很多细节的分析了。


那么下面就开始来过下源码吧,分析的入口就是跟着?System.loadlibrary()?走 :


//System#loadlibrary()public static void loadLibrary(String libName) {Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());}


//Runtime#loadLibrary()void loadLibrary(String libraryName, ClassLoader loader) {//1. 程序中通过 System.loadlibrary() 方式,这个 loader 就不会为空,流程走这边 if (loader != null) {//2. loader.findLibrary() 这是个重点,这个方法用于寻找 so 文件是否存在 String filename = loader.findLibrary(libraryName);if (filename == null) {throw new UnsatisfiedLinkError(loader + " couldn't find "" + System.mapLibraryName(libraryName) + """);}//3. 如果 so 文件找到,那么加载它 String error = doLoad(filename, loader);if (error != null) {//4. 如果加载失败,那么抛异常 throw new UnsatisfiedLinkError(error);}return;}


//1.1 以下代码的运行场景我不清楚,但有几个方法可以蛮看一下//mapLibraryName 用于拼接 so 文件名的前缀:lib,和后缀.soString filename = System.mapLibraryName(libraryName);//...省略//1.2 mLibPaths 存储着设备存放 so 文件的目录地址 for (String directory: mLibPaths) {String candidate = directory + filename;candidates.add(candidate);if (IoUtils.canOpenReadOnly(candidate))// 1.3 调用 native 层方法加载 so 库 String error = doLoad(candidate, loader);if (error == null) {return; // We successfully loaded the library. Job done.}lastError = error;}}//...省略}


所以,其实 System 的?loadlibrary()?是调用的 Runtime 的?loadLibrary(),不同系统版本,这些代码是有些许差别的,但不管怎样,重点都还是?loadLibrary()?中调用的一些方法,这些方法基本没变,改变的只是其他代码的优化写法。


那么,要理清 so 文件的加载流程,或者说,要找出系统是去哪些地址加载 so 文件的,就需要梳理清这些方法:


  • loader.findLibrary()

  • doLoad()


第一个方法用于寻找 so 文件,所涉及的整个流程应该都在这个方法里,如果可以找到,会返回 so 文件的绝对路径,然后交由?doLoad()?去加载。

java.library.path

但在深入去探索之前,我想先探索另一条分支,loader 为空的场景。loader 什么时候为空,什么时候不为空,我并不清楚,只是看别人的文章分析时说,程序中通过?System.loadlibrary()?方式加载 so,那么 loader 就不会为空。那,我就信你了,不然我也不知道去哪分析为不为空的场景。


既然程序不会走另一个分支,为什么我还要先来探索它呢?因为,第一个分支太不好探索了,先从另一个分支摸索点经验,而且还发现了一些感觉可以拿来讲讲的方法:


  • System.mapLibraryName()


用于拼接 so 文件名的前缀?lib,和后缀?.so


  • mLibPaths


在其他版本的源码中,可能就没有这个变量了,直接就是调用一个方法,但作用都一样,我们看看这个变量的赋值:


//Runtime.mLibPathsprivate final String[] mLibPaths = initLibPaths();


//Runtime#initLibPaths()private static String[] initLibPaths() {String javaLibraryPath = System.getProperty("java.library.path");//...省略}


最后都是通过调用 System 的?getProperty()?方法,读取?java.library.path?的属性值。


也就是说,通过读取?java.library.path?的系统属性值,是可以获取到设备存放 so 库的目录地址的,那么就来看看在哪里有设置这个属性值进去。


System 内部有一个类型为 Properties 的静态变量,不同版本,这个变量名可能不一样,但作用也都一样,用来存储这些系统属性值,这样程序需要的时候,调用?getProperty()?读取属性值时其实是来这个静态变量中读取。而变量的初始化地方在类中的 static 代码块中:


//Systemstatic {//...省略//1.初始化一些不变的系统属性值 unchangeableSystemProperties = initUnchangeableSystemProperties();//2.将上述的属性值以及一些默认的系统属性值设置到静态变量中 systemProperties = createSystemProperties();//...}


//System#initUnchangeableSystemProperties()private static Properties initUnchangeableSystemProperties() {//...省略一些属性值设置 p.put("java.vm.vendor", projectName);p.put("java.vm.version", runtime.vmVersion());p.put("file.separator", "/");p.put("line.separator", "\n");p.put("path.separator", ":");//...


//1.这里是重点 parsePropertyAssignments(p, specialProperties());


//...return p;}


//System#createSystemProperties()private static Properties createSystemProperties() {//1.拷贝不可变的一些系统属性值 Properties p = new PropertiesWithNonOverrideableDefaults(unchangeableSystemProperties);//2.设置一些默认的属性值 setDefaultChangeableProperties(p);return p;}


//System#setDefaultChangeableProperties()private static void setDefaultChangeableProperties(Properties p) {p.put("java.io.tmpdir", "/tmp");p.put("user.home", "");}


static 静态代码块中的代码其实就是在初始化系统属性值,分两个步骤,一个是先设置一些不可变的属性值,二是设置一些默认的属性值,然后将这些存储在静态变量中。


但其实,不管在哪个方法中,都没找到有设置?java.library.path?属性值的代码,那这个属性值到底是在哪里设置的呢?


关键点在于设置不可变的属性时,有调用了一个 native 层的方法:


//System/**


  • Returns an array of "key=value" strings containing information not otherwise

  • easily available, such as #defined library versions.*/private static native String[] specialProperties();


这方法会返回 key=value 形式的字符串数组,然后?parsePropertyAssignments()?方法会去遍历这些数组,将这些属性值填充到存储系统属性值的静态变量中。


也就是说,在 native 层还会设置一些属性值,而?java.library.path?有可能就是在 native 中设置的,那么就跟下去看看吧。


System 连同包名的全名是:java.lang.System;那么,通常,所对应的 native 层的 cpp 文件名为:java_lang_System.cpp,到这里去看看:


//platform/libcore/luni/src/main/native/java_lang_System.cpp#System_specialProperties()static jobjectArray System_specialProperties(JNIEnv* env, jclass) {std::vectorstd::string properties;


//...


//1. 获取 LD_LIBRARY_PATH 环境变量值 const char* library_path = getenv("LD_LIBRARY_PATH");#if defined(HAVE_ANDROID_OS)if (library_path == NULL) {//2.如果 1 步骤没获取到路径,那么通过该方法获取 so 库的目录路径 android_get_LD_LIBRARY_PATH(path, sizeof(path));library_path = path;}#endifif (library_path == NULL) {library_path = "";}//3.设置 java.library.path 属性值 properties.push_back(std::string("java.library.path=") + library_path);


return toStringArray(env, properties);}


没错吧,对应的 native 层的方法是上述这个,它干的事,其实也是设置一些属性值,我们想要的?java.library.path?就是在这里设置的。那么,这个属性值来源的逻辑是这样的:


  1. 先读取 LD_LIBRARY_PATH 环境变量值,如果不为空,就以这个值为准。但我测试过,貌似,程序运行时读取的这个值一直是 null,在 Runtime 的?doLoad()?方法注释中,Google 有解释是说由于 Android 的进程都是通过 Zygote 进程 fork 过来,所以不能使用 LD_LIBRARY_PATH 。应该,大概,可能是这个意思吧,我英文不大好,你们可以自行去确认一下。

  2. 也就是说,第一步读取的 LD_LIBRARY_PATH 值是为空,所以会进入第二步,调用 android_get_LD_LIBRARY_PATH 方法来读取属性值。(进入这个步骤有个条件是定义了 HAVE_ANDROID_OS 宏变量,我就不去找到底哪里在什么场景下会定义了,看命名我直接猜测 Android 系统就都有定义的了)


那么,继续看看 android_get_LD_LIBRARY_PATH 这个方法做了些什么:


//platform/libcore/luni/src/main/native/java_lang_System.cpp#if defined(HAVE_ANDROID_OS)extern "C" void android_get_LD_LIBRARY_PATH(char*, size_t);#endif


emmm,看不懂,头疼。那,直接全局搜索下这个方法名试试看吧,结果在另一个 cpp 中找到它的实现:


//platform/bionic/linker/dlfcn.cppvoid android_get_LD_LIBRARY_PATH(char* buffer, size_t buffer_size) {ScopedPthreadMutexLocker locker(&g_dl_mutex);do_android_get_LD_LIBRARY_PATH(buffer, buffer_size);}


第一行估计是加锁之类的意思吧,不管,第二行是调用另一个方法,继续跟下去看看:


//platform/bionic/linker/linker.cppvoid do_android_get_LD_LIBRARY_PATH(char* buffer, size_t buffer_size) {//...char* end = stpcpy(buffer, kDefaultLdPaths[0]);*end = ':';strcpy(end + 1, kDefaultLdPaths[1]);}


static const char* const kDefaultLdPaths[] = {#if defined(LP64)"/vendor/lib64","/system/lib64",#else"/vendor/lib","/system/lib",#endifnullptr};


还好 Source Insight 点击方法时有时可以支持直接跳转过去,调用的这个方法又是在另一个 cpp 文件中了。开头省略了一些大小空间校验的代码,然后直接复制了静态常量的值,而这个静态常量在这份文件顶部定义。


终于跟到底了吧,也就是说,如果有定义了?LP64?这个宏变量,那么就将?java.library.path?属性值赋值为 "/vendor/lib64:/system/lib64",否则,就赋值为 "/vendor/lib:/system/lib"。


也就是说,so 文件的目录地址其实是在 native 层通过硬编码方式写死的,网上那些理所当然的说 so 文件的存放目录也就是这四个,是这么来的。那么,说白了,系统默认存放 so 文件的目录就两个,只是有两种场景。


而至于到底什么场景下会有这个 LP64 宏变量的定义,什么时候没有,我实在没能力继续跟踪下去了,网上搜索了一些资料后,仍旧不是很懂,如果有清楚的大佬,能够告知、指点下就最棒了。


我自己看了些资料,以及,自己也做个测试:同一个 app,修改它的 primaryCpuAbi 值,调用 System 的?getProperty()?来读取?java.library.path,它返回的值是会不同的。


所以,以我目前的能力以及所掌握的知识,我是这么猜测的,纯属个人猜测:


LP64 这个宏变量并不是由安卓系统代码来定义的,而是 Linux 系统层面所定义的。在 Linux 系统中,可执行文件,也可以说所运行的程序,如果是 32 位的,那么是没有定义这个宏变量的,如果是 64 位的,那么是有定义这个宏变量的。


总之,通俗的联想解释,LP64 这个宏变量表示着当前程序是 32 位还是 64 位的意思。(个人理解)


有时间再继续研究吧,反正这里清楚了,系统默认存放 so 文件的目录只有两个,但有两种场景。vendor 较少用,就不每次都打出来了。也就是说,如果应用在 system/lib 目录中没有找到 so 文件,那么它是不会再自动去 system/lib64 中寻找的,两者它只会选其一。至于选择哪个,因为 Zygote 是有分 32 位还是 64 位进程的,那么刚好可以根据这个为依据。

findLibrary

该走回主线了,在支线中的探索已经摸索了些经验了。


大伙应该还记得吧,System 调用了?loadlibrary()?之后,内部其实是调用了 Runtime 的?loadLibrary()?方法,这个方法内部会去调用 ClassLoader 的?findLibrary()?方法,主要是去寻找这个 so 文件是否存在,如果存在,会返回 so 文件的绝对路径,接着交由 Runtime 的?doLoad()?方法去加载 so 文件。


所以,我们想要梳理清楚 so 文件的加载流程,findLibrary()?是关键。那么,接下去,就来跟着?findLibrary()?走下去看看吧:


//ClassLoader#findLibrary()protected String findLibrary(String libName) {return null;}


ClassLoader 只是一个基类,具体实现在其子类,那这里具体运行的是哪个子类呢?


//System#loadlibrary()public static void loadLibrary(String libName) {Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());}


所以这里是调用了 VMStack 的一个方法来获取 ClassLoader 对象,那么继续跟进看看:


native public static ClassLoader getCallingClassLoader();


又是一个 native 的方法,我尝试过跟进去,没有看懂。那么,换个方向来找出这个基类的具体实现子类是哪个吧,很简单的一个方法,打 log 输出这个对象本身:


ClassLoader classLoader = getClassLoader();Log.v(TAG, "classLoader = " + classLoader.toString());//输出// classLoader = dalvik.system.PathClassLoader[dexPath=/data/app/com.qrcode.qrcode-1.apk,libraryPath=/data/app-lib/com.qrcode.qrcode-1]


以上打 Log 代码是从?Java中System.loadLibrary() 的执行过程?这篇文章中截取出来的,使用这个方法的前提是你得清楚 VMStack 的?getCallingClassLoader()?含义其实是获取调用这个方法的类它的类加载器对象。


或者,你对 Android 的类加载机制有所了解,知道当启动某个 app 时,经过层层工作后,会接着让 LoadedApk 去加载这个 app 的 apk,然后通过 ApplicationLoader 来加载相关代码文件,而这个类内部是实例化了一个 PathClassLoader 对象去进行 dex 的加载。


不管哪种方式,总之清楚了这里实际上是调用了 PathClassLoader 的?findLibrary()?方法,但 PathClassLoader 内部并没有这个方法,它继承自 BaseDexClassLoader,所以实际上还是调用了父类的方法,跟进去看看:


//platform/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.javapublic String findLibrary(String name) {return pathList.findLibrary(name);}


private final DexPathList pathList;


内部又调用了 DexPathList 的?findLibrary()?方法,继续跟进看看:


//platform/libcore/dalvik/src/main/java/dalvik/system/DexPathList.javapublic String findLibrary(String libraryName) {//1. 拼接前缀:lib,和后缀:.soString fileName = System.mapLibraryName(libraryName);//2. 遍历所有存放 so 文件的目录,确认指定文件是否存在以及是只读文件 for (File directory: nativeLibraryDirectories) {String path = new File(directory, fileName).getPath();if (IoUtils.canOpenReadOnly(path)) {return path;}}return null;}


/** List of native library directories. */private final File[] nativeLibraryDirectories;


到了这里,会先进行文件名补全操作,拼接上前缀:lib 和后缀:.so,然后遍历所有存放 so 文件的目录,当找到指定文件,且是只读属性,则返回该 so 文件的绝对路径。


所以,重点就是 nativeLibraryDirectories 这个变量了,这里存放着 so 文件存储的目录路径,那么得看看它在哪里被赋值了:


//platform/libcore/dalvik/src/main/java/dalvik/system/DexPathList.javapublic DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {//...//1. 唯一赋值的地方,构造函数 this.nativeLibraryDirectories = splitLibraryPath(libraryPath);}


private static File[] splitLibraryPath(String path) {//


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


Native libraries may exist in both the system and// application library paths, and we use this search order://// 1. this class loader's library path for application libraries// 2. the VM's library path from the system property for system libraries// (翻译下,大体是说,so 文件的来源有两处:1 是应用自身存放 so 文件的目录,2 是系统指定的目录)// This order was reversed prior to Gingerbread; see http://b/2933456.ArrayList < File > result = splitPaths(path, System.getProperty("java.library.path"), true);return result.toArray(new File[result.size()]);}


//将传入的两个参数的目录地址解析完都存放到集合中 private static ArrayList < File > splitPaths(String path1, String path2, boolean wantDirectories) {ArrayList < File > result = new ArrayList < File > ();


splitAndAdd(path1, wantDirectories, result);splitAndAdd(path2, wantDirectories, result);return result;}


private static void splitAndAdd(String searchPath, boolean directoriesOnly, ArrayList < File > resultList) {if (searchPath == null) {return;}//因为获取系统的 java.library.path 属性值返回的路径是通过 : 拼接的,所以先拆分,然后判断这些目录是否可用 for (String path: searchPath.split(":")) {try {StructStat sb = Libcore.os.stat(path);if (!directoriesOnly || S_ISDIR(sb.st_mode)) {resultList.add(new File(path));}} catch(ErrnoException ignored) {}}}


所以,nativeLibraryDirectories 这个变量是在构造函数中被赋值。代码不多,总结一下,构造函数会传入一个 libraryPath 参数,表示应用自身存放 so 文件的路径,然后内部会再去调用 System 的?getProperty("java.library.path")?方法获取系统指定的 so 文件目录地址。最后,将这些路径都添加到集合中。


而且,看添加的顺序,是先添加应用自身的 so 文件目录,然后再添加系统指定的 so 文件目录,也就是说,当加载 so 文件时,是先去应用自身的 so 文件目录地址寻找,没有找到,才会去系统指定的目录。


而系统指定的目录地址在 native 层的 linker.cpp 文件定义,分两种场景,取决于应用当前的进程是 32 位还是 64 位,32 位的话,则按顺序分别去 vendor/lib 和 system/lib 目录中寻找,64 位则是相对应的 lib64 目录中。


虽然,so 文件加载流程大体清楚了,但还有两个疑问点:


  • 构造方法参数传入的表示应用自身存放 so 文件目录的 libraryPath 值是哪里来的;

  • 应用什么时候运行在 32 位或 64 位的进程上;

nativeLibraryDir

先看第一个疑问点,应用自身存放 so 文件目录的这个值,要追究的话,这是一个很漫长的故事。


这个过程,我不打算全部都贴代码了,因为很多步骤,我自己也没有去看源码,也是看的别人的文章,我们以倒着追踪的方式来进行追溯吧。


首先,这个 libraryPath 值是通过 DexPathList 的构造方法传入的,而 BaseDexClassLoader 内部的 DexPathList 对象实例化的地方也是在它自己的构造方法中,同样,它也接收一个 libraryPath 参数值,所以 BaseDexClassLoader 只是做转发,来源并不在它这里。


那么,再往回走,就是 LoadedApk 实例化 PathClassLoader 对象的地方了,在它的?getClassLoader()?方法中:


//platform/frameworks/base/core/java/android/app/LoadedApk.javapublic ClassLoader getClassLoader() {synchronized(this) {//...final ArrayList < String > libPaths = new ArrayList < >();//...libPaths.add(mLibDir);//...final String lib = TextUtils.join(File.pathSeparator, libPaths);//...mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib, mBaseClassLoader);//...}}


public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) {//...mLibDir = aInfo.nativeLibraryDir;//...}


无关代码都省略掉了,也就是说,传给 DexPathList 的 libraryPath 值,其实是将要启动的这个 app 的 ApplicationInfo 中的 nativeLibraryDir 变量值。


可以看看 ApplicationInfo 中这个变量的注释:


//ApplicationInfo/**


  • Full path to the directory where native JNI libraries are stored.

  • 存放 so 文件的绝对路径*/public String nativeLibraryDir;


通俗点解释也就是,存放应用自身 so 文件的目录的绝对路径。那么问题又来了,传给 LoadedApk 的这个 ApplicationInfo 对象哪里来的呢?


这个就又涉及到应用的启动流程了,大概讲一下:


我们知道,当要启动其他应用时,其实是通过发送一个 Intent 去启动这个 app 的 LAUNCHER 标志的 Activity。而当这个 Intent 发送出去后,是通过 Binder 通信方式通知了 ActivityManagerServer 去启动这个 Activity。


AMS 在这个过程中会做很多事,但在所有事之前,它得先解析 Intent,知道要启动的是哪个 app 才能继续接下去的工作,这个工作在 ActivityStackSupervisor 的?resolveActivity()


//ActivityStackSupervisor.javaActivityInfo resolveActivity(Intent intent, String resolvedType, int startFlags, ProfilerInfo profilerInfo, int userId) {// Collect information about the target of the Intent.ActivityInfo aInfo;try {ResolveInfo rInfo = AppGlobals.getPackageManager().resolveIntent(intent, resolvedType, PackageManager.MATCH_DEFAULT_ONLY | ActivityManagerService.STOCK_PM_FLAGS, userId);aInfo = rInfo != null ? rInfo.activityInfo: null;} catch(RemoteException e) {aInfo = null;}//...}


不同版本,可能不是由这个类负责这个工作了,但可以跟着 ActivityManagerService 的?startActivity()?走下去看看,不用跟很深就能找到,因为这个工作是比较早进行的。


所以,解析 Intent 获取 app 的相关信息就又交给 PackageManagerService 的?resolveIntent()?进行了,PKMS 的工作不贴了,我直接说了吧:


PKMS 会根据 Intent 中目标组件的 packageName,通过一个只有包权限的类 Settings 来获取对应的 ApplicationInfo 信息,这个 Settings 类全名:com.android.server.pm.Settings,它的职责之一是存储所有 app 的基本信息,也就是在 data/system/packages.xml 中各 app 的信息都交由它维护缓存。


所以,一个 app 的 ApplicationInfo 信息,包括 nativeLibraryDir 我们都可以在 data/system/packages.xml 这份文件中查看到。这份文件的角色我把它理解成类似 PC 端上的注册表,所有 app 的信息都注册在这里。


那这份 packages.xml 文件的数据又是从哪里来的呢,这又得涉及到 apk 的安装机制过程了。


简单说一下,一个 app 的安装过程,在解析 apk 包过程中,还会结合各种设备因素等等来决定这个 app 的各种属性,比如说 nativeLibraryDir 这个属性值的确认,就需要考虑这个 app 是三方应用还是系统应用,这个应用的 primaryCpuAbi 属性值是什么,apk 文件的地址等等因素后,最后才确定了应用存放 so 文件的目录地址是哪里。


举个例子,对于系统应用来说,这个 nativeLibraryDir 值有可能最后是 /system/lib/xxx,也有可能是 system/app/xxx/lib 等等;而对于三方应用来说,这值有可能就是 data/app/xxx/lib;


也就是说,当 app 安装完成时,这些属性值也就都解析到了,就都会保存到 Settings 中,同时会将这些信息写入到 data/system/packages.xml 中。


到这里,先来小结一下,梳理下前面的内容:


当一个 app 安装的时候,系统会经过各种因素考量,最后确认 app 的一个 nativeLibraryDir 属性值,这个属性值代表应用自身的 so 文件存放地址,这个值也可以在 data/system/packages.xml 中查看。


当应用调用了 System 的?loadlibrary()?时,这个 so 文件的加载流程如下:


  1. 先到 nativeLibraryDir 指向的目录地址中寻找这个 so 文件是否存在、可用;

  2. 如果没找到,那么根据应用进程是 32 位或者 64 位来决定下去应该去哪个目录寻找 so 文件;

  3. 如果是 32 位,则先去 vendor/lib 找,最后再去 system/lib 中寻找;

  4. 如果是 64 位,则先去 vendor/lib64 找,最后再去 system/lib64 中寻找;

  5. 系统默认的目录是在 native 层中的 linker.cpp 文件中指定,更严谨的说法,不是进程是不是 32 位或 64 位,而是是否有定义了 LP64 这个宏变量。

primaryCpuAbi

我们已经清楚了,加载 so 文件的流程,其实就分两步,先去应用自身存放 so 文件的目录(nativeLibraryDir)寻找,找不到,再去系统指定的目录中寻找。


而系统指定是目录分两种场景,应用进程是 32 位或者 64 位,那么,怎么知道应用是运行在 32 位还是 64 位的呢?又或者说,以什么为依据来决定一个应用是应该跑在 32 位上还是跑在 64 位上?


这个就取决于一个重要的属性了 primaryCpuAbi,它代表着这个应用的 so 文件使用的是哪个 abi 架构。


abi 常见的如:arm64-v8a,armeabi-v7a,armeabi,mips,x86_64 等等。


我们在打包 apk 时,如果不指定,其实默认是会将所有 abi 对应的 so 文件都打包一份,而通常,为了减少 apk 包体积,我们在 build.gradle 脚本中会指定只打其中一两份。但不管 apk 包有多少种不同的 abi 的 so 文件,在 app 安装过程中,最终拷贝到 nativeLibraryDir 中的通常都只有一份,除非你手动指定了要多份。


那么,app 在安装过程中,怎么知道,应该拷贝 apk 中的 lib 下的哪一份 so 文件呢?这就是由应用的 primaryCpuAbi 属性决定。


而同样,这个属性一样是在 app 安装过程中确定的,这个过程更加复杂,末尾有给了篇链接,感兴趣可以去看看,大概来说,就是 apk 包中的 so 文件、系统应用、相同 UID 的应用、设备的 abilist 等都对这个属性值的确定过程有所影响。同样,这个属性值也可以在 data/system/packages.xml 中查看。


那么,这个 primaryCpuAbi 属性值是如何影响应用进程是 32 位还是 64 位的呢?


这就涉及到 Zygote 方面的知识了。


在系统启动之后,系统会根据设备的 ro.zygote 属性值决定启动哪个 Zygote,可以通过执行?getprop | grep ro.zygote?来查看这个属性值,属性值与对应的 Zygote 进程关系如下:


  • zygote32:只启动一个 32 位的 Zygote 进程

  • zygote32_64:启动两个 Zygote 进程,分别为 32 位和 64 位,32 位的进程名为 zygote,表示以它为主,64 位进程名为 zygote_secondary ,表示它作为辅助

  • zygote64:只启动一个 64 位的 Zygote 进程

  • zygote64_32:启动两个 Zygote 进程,分别为 32 位和 64 位,64 位的进程名为 zygote,表示以它为主,32 位进程名为 zygote_secondary ,表示它作为辅助


而 Zygote 进程启动之后,会打开一个 socket 端口,等待 AMS 发消息过来启动新的应用时 fork 当前 Zygote 进程,所以,如果 AMS 是发给 64 位的 Zygote,那么新的应用自然就是跑在 64 位的进程上;同理,如果发给了 32 位的 Zygote 进程,那么 fork 出来的进程自然也就是 32 位的。


那么,可以跟随着 AMS 的?startProcessLocked()?方法,去看看是以什么为依据选择 32 位或 64 位的 Zygote:


//ActivityManagerServiceprivate final void startProcessLocked(ProcessRecord app, String hostingType, String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs) {//...省略//1. 获取要启动的 app 的 primaryCpuAbi 属性值,abiOverride 不知道是什么,可能是 Google 开发人员写测试用例用的吧,或者其他一些场景 String requiredAbi = (abiOverride != null) ? abiOverride: app.info.primaryCpuAbi;if (requiredAbi == null) {//2. 如果为空,以设备支持的首个 abi 属性值,可执行 getprot ro.product.cpu.abilist 查看 requiredAbi = Build.SUPPORTED_ABIS[0];}//...


//3. 调用 Precess 的 start 方法,将 requiredAbi 传入 Process.ProcessStartResult startResult = Process.start(entryPoint, app.processName, uid, uid, gids, debugFlags, mountExternal, app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet, app.info.dataDir, entryPointArgs);//...}


AMS 会先获取要启动的 app 的 primaryCpuAbi 属性值,至于这个 app 的相关信息怎么来的,跟上一小节一样,解析 Intent 时交由 PKMS 去它模块内部的 Settings 读取的。


如果 primaryCpuAbi 为空,则以设备支持的首个 abi 属性值为主,设备支持的 abi 列表可以通过执行?getprot ro.product.cpu.abilist?查看,最后调用 Precess 的?start()?方法,将读取的 abi 值传入:


//Processpublic static final ProcessStartResult start(final String processClass, final String niceName, int uid, int gid, int[] gids, int debugFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, String instructionSet, String appDataDir, String[] zygoteArgs) {//...return startViaZygote(processClass, niceName, uid, gid, gids, debugFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, zygoteArgs);//...}


private static ProcessStartResult startViaZygote(final String processClass, final String niceName, final int uid, final int gid, final int[] gids, int debugFlags, int mountExternal, int targetSdkVersion, String seInfo, String abi, String instructionSet, String appDataDir, String[] extraArgs) throws ZygoteStartFailedEx {//...//所以 abi 最终是调用 openZygoteSocketIfNeeded() 方法,传入给它使用 return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);}


abi 值又是一层传一层,最终交到了 Process 的?openZygoteSocketIfNeeded()?方法中使用,跟进看看:

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
插件化框架解读之so-文件加载机制(四)