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

还好 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.java
public String findLibrary(String name) {
return pathList.findLibrary(name);
}

private final DexPathList pathList;

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

//platform/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public String findLibrary(String libraryName) {
//1. 拼接前缀:lib,和后缀:.so
String 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.java
public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
//…
//1. 唯一赋值的地方,构造函数
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}

private static File[] splitLibraryPath(String path) {
// 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.java
public 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.java
ActivityInfo 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:

//ActivityManagerService
private 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 值传入:

//Process
public 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() 方法中使用,跟进看看:

//Process
private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
try {
//ZYGOTE_SOCKET值为 zygote,
//通过 ZygoteState 的 connect 方法,连接进程名为 zygote 的 Zygote 进程
primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);
} catch(IOException ioe) {
throw new ZygoteStartFailedEx(“Error connecting to primary zygote”, ioe);
}
}
//在进程名为 zygote 的 Zygote 进程支持的 abi 列表中,查看是否支持要启动的 app 的需要的 abi
if (primaryZygoteState.matches(abi)) {
return primaryZygoteState;
}

// The primary zygote didn’t match. Try the secondary.
if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
try {
//SECONDARY_ZYGOTE_SOCKET 的值为 zygote_secondary,
//通过 ZygoteState 的 connect 方法,连接进程名为 zygote_secondary 的 Zygote 进程
secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET);
} catch(IOException ioe) {
throw new ZygoteStartFailedEx(“Error connecting to secondary zygote”, ioe);
}
}
//在进程名为 zygote_secondary 的 Zygote 进程支持的 abi 列表中,查看是否支持要启动的 app 的需要的 abi
if (secondaryZygoteState.matches(abi)) {
return secondaryZygoteState;
}

throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
}

static ZygoteState primaryZygoteState;
static ZygoteState secondaryZygoteState;
public static final String ZYGOTE_SOCKET = “zygote”;
public static final String SECONDARY_ZYGOTE_SOCKET = “zygote_secondary”;

到了这里,是先获取进程名 zygote 的 Zygote 进程,查看它支持的 abi 列表中是否满足要启动的 app 所需的 abi,如果满足,则使用这个 Zygote 来 fork 新进程,否则,获取另一个进程名为 zygote_secondary 的 Zygote 进程,同样查看它支持的 abi 列表中是否满足 app 所需的 abi,如果都不满足,抛异常。

那么,名为 zygote 和 zygote_secondary 分别对应的是哪个 Zygote 进程呢?哪个对应 32 位,哪个对应 64 位?

还记得上述说过的,系统启动后,会去根据设备的 ro.zygote 属性决定启动哪个 Zygote 进程吗?对应关系就是这个属性值决定的,举个例子,可以看看 zygote64_32 对应的 Zygote 启动配置文件:

//platform/system/core/rootdir/init.zygote64_32.rc
service zygote /system/bin/app_process64 -Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd

service zygote_secondary /system/bin/app_process32 -Xzygote /system/bin --zygote --socket-name=zygote_secondary
class main
socket zygote_secondary stream 660 root system
onrestart restart zygote

这份代码前半段的意思就表示,让 Linux 启动一个 service,进程名为 zygote,可执行文件位于 /system/bin/app_process64,后面是参数以及其他命令。

所以,名为 zygote 和 zygote_secondary 分别对应的是哪个 Zygote 进程,就取决于设备的 ro.zygote 属性。

而,获取 Zygote 支持的 abi 列表是通过 ZygoteState 的 connect() 方法,我们继续跟进看看:

//Process$ZygoteState
public static ZygoteState connect(String socketAddress) throws IOException {
//…

String abiListString = getAbiList(zygoteWriter, zygoteInputStream);
Log.i(“Zygote”, "Process: zygote socket opened, supported ABIS: " + abiListString);

return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter, Arrays.asList(abiListString.split(“,”)));
}

发现没有,源码内部将 Zygote 支持的 abi 列表输出日志了,你们可以自己尝试下,过滤下 TAG 为 Zygote,然后重启下设备,因为如果本来就连着 Zygote,那么是不会走到这里的了,最后看一下相关日志,如:

01-01 08:00:13.509 2818-2818/? D/AndroidRuntime: >>>>>> START com.android.internal.os.ZygoteInit uid 0 <<<<<<
01-01 08:00:15.068 2818-2818/? D/Zygote: begin preload
01-01 08:00:15.081 2818-3096/? I/Zygote: Preloading classes…
01-01 08:00:15.409 2818-3097/? I/Zygote: Preloading resources…
01-01 08:00:16.637 2818-3097/? I/Zygote: …preloaded 343 resources in 1228ms.
01-01 08:00:16.669 2818-3097/? I/Zygote: …preloaded 41 resources in 33ms.
01-01 08:00:17.242 2818-3096/? I/Zygote: …preloaded 3005 classes in 2161ms.
01-01 08:00:17.373 2818-2818/? I/Zygote: Preloading shared libraries…
01-01 08:00:17.389 2818-2818/? D/Zygote: end preload
01-01 08:00:17.492 2818-2818/? I/Zygote: System server process 3102 has been created
01-01 08:00:17.495 2818-2818/? I/Zygote: Accepting command socket connections
01-01 08:00:32.789 3102-3121/? I/Zygote: Process: zygote socket opened, supported ABIS: armeabi-v7a,armeabi

系统启动后,Zygote 工作的相关内容基本都打日志出来了。

最后,再来稍微理一理:

app 安装过程,会确定 app 的一个属性值:primaryCpuAbi,它代表着这个应用的 so 文件使用的是哪个 abi 架构,而且它的确定过程很复杂,apk 包中的 so 文件、系统应用、相同 UID 的应用、设备的 abilist 等都对这个属性值的确定过程有所影响。安装成功后,可以在 data/system/packages.xml 中查看这个属性值。

每启动一个新的应用,都是运行在新的进程中,而新的进程是从 Zygote 进程 fork 过来的,系统在启动时,会根据设备的 ro.zygote 属性值决定启动哪几个 Zygote 进程,然后打开 socket,等待 AMS 发送消息来 fork 新进程。

当系统要启动一个新的应用时,AMS 在负责这个工作进行到 Process 类的工作时,会先尝试在进程名为 zygote 的 Zygote 进程中,查看它所支持的 abi 列表中是否满足要启动的 app 所需的 abi,如果满足,则以这个 Zygote 为主,fork 新进程,运行在 32 位还是 64 位就跟这个 Zygote 进程一致,而 Zygote 运行在几位上取决于 ro.zygote 对应的文件,如值为 zygote64_32 时,对应着 init.zygote64_32.rc 这份文件,那么此时名为 zygote 的 Zygote 就是运行在 64 位上的。

而当上述所找的 Zygote 支持的 abi 列表不满足 app 所需的 abi 时,那么再去名为 zygote_secondary 的 Zygote 进程中看看,它所支持的 abi 列表是否满足。

另外,Zygote 的相关工作流程,包括支持的 abi 列表,系统都有打印相关日志,可过滤 Zygote 查看,如没发现,可重启设备查看。

abi 兼容

so 文件加载的流程,及应用运行在 32 位或 64 位的依据我们都梳理完了,以上内容足够掌握什么场景下,该去哪些目录下加载 so 文件的判断能力了。

那么,还有个问题,如果应用运行在 64 位上,那么此时,它是否能够使用 armeabi-v7a 的 so 文件?

首先,先来罗列一下常见的 abi :

  • arm64-v8a,armeabi-v7a,armeabi,mips,mips64,x86,x86_64

其中,运行在 64 位的 Zygote 进程上的是:

  • arm64-v8a,mips64,x86_64

同样,运行在 32 位的 Zygote 进程上的是:

  • armeabi-v7a,armeabi,mips,x86

你们如果去网上搜索如下关键字:so 文件,abi 兼容等,你们会发现,蛮多文章里都会说:arm64-v8a 的设备能够向下兼容,支持运行 32 位的 so 文件,如 armeabi-v7a。

这句话没错,64 位的设备能够兼容运行 32 位的 so 文件,但别只看到这句话啊,良心一些的文章里还有另一句话:不同 cpu 架构的 so 文件不能够混合使用,例如,程序运行期间,要么全部使用 arm64-v8a 的 so 文件,要么全部使用 armeabi-v7a 的 so 文件,你不能跑在 64 位进程上,却使用着 32 位的 so 文件。

我所理解的兼容,并不是说,64 位的设备,支持你运行在 64 位的 Zygote 进程上时仍旧可以使用 32 位的 so 文件。有些文章里也说了,如果在 64 位的设备上,你选择使用 32 位的 so 文件,那么此时,你就丢失了专门为 64 位优化过的性能(ART,webview,media等等 )。这个意思就是说,程序启动时是从 32 位的 Zygote 进程 fork 过来的,等于你在 64 位的设备上,但却只运行在 32 位的进程上。

至于程序如何决定运行在 32 位还是 64 位,上面的章节中也分析过了,以 app 的 primaryCpuAbi 属性值为主,而这个属性值的确定因素之一就是含有的 so 文件所属的 abi。

如果,你还想自己验证,那么可以跟着 Runtime 的 doLoad() 方法跟到 native 层去看看,由于我下载的源码版本可能有些问题,我没找到 Runtime 对应的 cpp 文件,但我找到这么段代码:

//platform/bionic/linker/linker_phdr.cpp
bool ElfReader::VerifyElfHeader() {
//…
//1.读取 elf 文件的 header 的 class 信息
int elf_class = header_.e_ident[EI_CLASS];
#if defined(LP64)
//2. 如果当前进程是64位的,而 elf 文件属于 32 位的,则报错
if (elf_class != ELFCLASS64) {
if (elf_class == ELFCLASS32) {
DL_ERR(“”%s" is 32-bit instead of 64-bit", name_);
} else {
DL_ERR(“”%s" has unknown ELF class: %d", name_, elf_class);
}
return false;
}
#else
//3. 如果当前进程是32位的,而 elf 文件属于 64 位的,则报错
if (elf_class != ELFCLASS32) {
if (elf_class == ELFCLASS64) {
DL_ERR(“”%s" is 64-bit instead of 32-bit", name_);
} else {
DL_ERR(“”%s" has unknown ELF class: %d", name_, elf_class);
}
return false;
}
#endif

加载 so 文件,最终还是交由 native 层去加载,在 Linux 中,so 文件其实就是一个 elf 文件,elf 文件有个 header 头部信息,里面记录着这份文件的一些信息,如所属的是 32 位还是 64 位,abi 的信息等等。

而 native 层在加载 so 文件之前,会去解析这个 header 信息,当发现,如果当前进程运行在 64 位时,但要加载的 so 文件却是 32 位的,就会报 xxx is 32-bit instead of 64-bit 异常,同样,如果当前进程是运行在 32 位的,但 so 文件却是 64 位的,此时报 xxx is 64-bit instead of 32-bit 异常。

这个异常应该也有碰见过吧:

java.lang.UnsatisfiedLinkError: dlopen failed: “libimagepipeline.so” is 32-bit instead of 64-bit

所以说,64 位设备的兼容,并不是说,允许你运行在 64 位的进程上时,仍旧可以使用 32 位的 so 文件。它的兼容是说,允许你在 64 位的设备上运行 32 位的进程。

其实,想想也能明白,这就是为什么三方应用安装的时候,并不会将 apk 包中所有 abi 目录下的 so 文件都解压出来,只会解压一种,因为应用在安装过程中,系统已经确定你这个应用是应该运行在 64 位还是 32 位的进程上了,并将这个结果保存在 app 的 primaryCpuAbi 属性值中。

既然系统已经明确你的应用所运行的进程是 32 位还是 64 位,那么只需拷贝对应的一份 so 文件即可,毕竟 64 位的 so 文件和 32 位的又不能混合使用。

以上,是我的理解,如果有误,欢迎指点下。

总结

整篇梳理下来,虽然梳理 so 的加载流程不难,但要掌握知其所以然的程度,就需要多花费一点心思了。

毕竟都涉及到应用的安装机制,应用启动流程,系统启动机制,Zygote 相关的知识点了。如果你是开发系统应用的,建议还是花时间整篇看一下,毕竟系统应用的集成不像三方应用那样在 apk 安装期间自动将相关 so 文件解压到 nativeLibraryDirectories 路径下了。三方应用很少需要了解 so 的加载流程,但开发系统应用还是清楚点比较好。

不管怎么说,有时间,可以稍微跟着过一下整篇,相信多少是会有些收获的,如果发现哪里有误,也欢迎指点。没时间的话,那就看看总结吧。

  • 一个应用在安装过程中,系统会经过一系列复杂的逻辑确定两个跟 so 文件加载相关的 app 属性值:nativeLibraryDirectories ,primaryCpuAbi ;
  • nativeLibraryDirectories 表示应用自身存放 so 文件的目录地址,影响着 so 文件的加载流程;
  • primaryCpuAbi 表示应用应该运行在哪种 abi 上,如(armeabi-v7a),它影响着应用是运行在 32 位还是 64 位的进程上,进而影响到寻找系统指定的 so 文件目录的流程;
  • 以上两个属性,在应用安装结束后,可在 data/system/packages.xml 中查看;
  • 当调用 System 的 loadLibrary() 加载 so 文件时,流程如下:
  • 先到 nativeLibraryDirectories 指向的目录中寻找,是否存在且可用的 so 文件,有则直接加载这里的 so 文件;
  • 上一步没找到的话,则根据当前进程如果是 32 位的,那么依次去 vendor/lib 和 system/lib 目录中寻找;
  • 同样,如果当前进程是 64 位的,那么依次去 vendor/lib64 和 system/lib64 目录中寻找;
  • 当前应用是运行在 32 位还是 64 位的进程上,取决于系统的 ro.zygote 属性和应用的 primaryCpuAbi 属性值,系统的 ro.zygote 可通过执行 getprop 命令查看;
  • 如果 ro.zygote 属性为 zygote64_32,那么应用启动时,会先在 ro.product.cpu.abilist64 列表中寻找是否支持 primaryCpuAbi 属性,有,则该应用运行在 64 位的进程上;
  • 如果上一步不支持,那么会在 ro.product.cpu.abilist32 列表中寻找是否支持 primaryCpuAbi 属性,有,则该应用运行在 32 位的进程上;
  • 如果 ro.zygote 属性为 zygote32_64,则上述两个步骤互换;
  • 如果应用的 primaryCpuAbi 属性为空,那么以 ro.product.cpu.abilist 列表中第一个 abi 值作为应用的 primaryCpuAbi;
  • 运行在 64 位的 abi 有:arm64-v8a,mips64,x86_64
  • 运行在 32 位的 abi 有:armeabi-v7a,armeabi,mips,x86
  • 通常支持 arm64-v8a 的 64 位设备,都会向下兼容支持 32 位的 abi 运行;
  • 但应用运行期间,不能混合着使用不同 abi 的 so 文件;
  • 比如,当应用运行在 64 位进程中时,无法使用 32 位 abi 的 so 文件,同样,应用运行在 32 位进程中时,也无法使用 64 位 abi 的 so 文件;

参考资料

1.Android – 系统进程Zygote的启动分析

2.Android应用程序进程启动过程(前篇)

3.如何查找native方法

4.Android中app进程ABI确定过程

5.Android 64 bit SO加载机制
原文作者:dasu
原文链接:https://www.cnblogs.com/dasusu/p/9810673.html

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

简历首选内推方式,速度快,效率高啊!然后可以在拉钩,boss,脉脉,大街上看看。简历上写道熟悉什么技术就一定要去熟悉它,不然被问到不会很尴尬!做过什么项目,即使项目体量不大,但也一定要熟悉实现原理!不是你负责的部分,也可以看看同事是怎么实现的,换你来做你会怎么做?做过什么,会什么是广度问题,取决于项目内容。但做过什么,达到怎样一个境界,这是深度问题,和个人学习能力和解决问题的态度有关了。大公司看深度,小公司看广度。大公司面试你会的,小公司面试他们用到的你会不会,也就是岗位匹配度。

面试过程一定要有礼貌!即使你觉得面试官不尊重你,经常打断你的讲解,或者你觉得他不如你,问的问题缺乏专业水平,你也一定要尊重他,谁叫现在是他选择你,等你拿到offer后就是你选择他了。

另外,描述问题一定要慢!不要一下子讲一大堆,慢显得你沉稳、自信,而且你还有时间反应思路接下来怎么讲更好。现在开发过多依赖ide,所以会有个弊端,当我们在面试讲解很容易不知道某个方法怎么读,这是一个硬伤…所以一定要对常见的关键性的类名、方法名、关键字读准,有些面试官不耐烦会说“你到底说的是哪个?”这时我们会容易乱了阵脚。正确的发音+沉稳的描述+好听的嗓音决对是一个加分项!

最重要的是心态!心态!心态!重要事情说三遍!面试时间很短,在短时间内对方要摸清你的底子还是比较不现实的,所以,有时也是看眼缘,这还是个看脸的时代。

希望大家都能找到合适自己满意的工作!

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

de,所以会有个弊端,当我们在面试讲解很容易不知道某个方法怎么读,这是一个硬伤…所以一定要对常见的关键性的类名、方法名、关键字读准,有些面试官不耐烦会说“你到底说的是哪个?”这时我们会容易乱了阵脚。正确的发音+沉稳的描述+好听的嗓音决对是一个加分项!

最重要的是心态!心态!心态!重要事情说三遍!面试时间很短,在短时间内对方要摸清你的底子还是比较不现实的,所以,有时也是看眼缘,这还是个看脸的时代。

希望大家都能找到合适自己满意的工作!

进阶学习视频

[外链图片转存中…(img-AAojWKQX-1713625679008)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-celSz1q0-1713625679008)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 21
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值