-
Notifications
You must be signed in to change notification settings - Fork 268
Qigsaw 源码分析之打包插件流程分析
在 Android App Bundle 初窥门径 一文中有介绍,当 Run 'app' 方式为 Default APK 时,实际调用了以下命令。
Executing tasks: [
:features:java:assembleDebug,
:features:initialInstall:assembleDebug,
:instant:url:assembleDebug,
:instant:split:assembleDebug,
:features:kotlin:assembleDebug,
:app:assembleDebug,
:features:assets:assembleDebug,
:features:native:assembleDebug
] in project ......
所有 dynamic feature 模块都会在 */build/output/apk* 目录生成apk文件。借助该思路,当运行 Qigsaw 命令时,只需要将所有 dynamic feature 以及 base 模块的 assemble 命令一起调用即可。
上图描述 qigsaw 打包命令最终是调用了所有 dynamic feature 和 base 模块的 assemble 命令,通过 qigsaw 命令就可以生成 base apk 和 dynamic feature apks。
当然 Qigsaw 打包插件要做的事情远不仅于,后文将详细介绍打包过程中的重点步骤。
首先来看下 Qigsaw 打包插件核心任务依赖关系。
实线箭头表示 dependsOn,虚线箭头表示 finalizedBy
qigsawAssembleTask 依赖 base 模块 mergeJniLibsTask。如果 dynamic feature apk (其 AndroidManifest.xml 的 onDemand 值为 true) 需要内置于 base apk,Qigsaw 会依据您当前项目 abiFilter 配置及项目实际 so 文件类型来决定内置插件是存储在 base apk 的 assets 目录还是 lib 目录。
内置插件存储于 base apk 的 lib 目录,可以减少一次插件 apk 拷贝,减少用户设备磁盘空间占用。
mergeJniLibsTask 依赖于 mergeAssetsTask。qigsawAssembleTask 会生成插件信息文件(Json格式),最终会将该文件拷贝至 base apk 的 assets 目录,base apk 就是通过该文件来安装插件。
核心一环是将 mergeJniLibsTask 依赖于所有 dynamic feature 的 assembleTask。只有先生成插件apk,才能知道各个插件的基本信息。
最后在 qigsawAssembleTask 执行完毕后,执行 base 模块的 assembleTask。
在 qigsawAssembleTask 任务中,主要完成以下事情。
- 生成插件信息文件,名称格式为 qigsaw_${appVersionName}_${splitInfoVersion}.json。
- 插件信息文件拷贝至 mergeAssetsTask 的产物目录下。
- 插件 apk 文件拷贝至 mergeAssetsTask 或 mergeJniLibsTask 的产物目录下。
Qigsaw 打包插件中,有三个 Transform 任务,分别是 ComponentInfoTransform、SplitLibraryLoaderTransform、SplitResourcesLoaderTransform。
该任务有两个作用。
- 生成 com.iqiyi.android.qigsaw.core.extension.ComponentInfo.class 文件,记录插件 AndroidManifest.xml 中 Activity、Service、Receiver、ContentProvider、Application 信息。Qigsaw 不支持四大组件更新,将插件四大组件用 class 文件记录可以尽最大可能避免插件四大组件信息解析失败,提高访问效率。
- 生成插件 ContentProvider 代理类(均继承自SplitContentProvider),用于加载插件 ContentProvider。QigsawProcessManifestTask 任务将编译后 AndroidManifest.xml 中插件 ContentProvider 的名字修改为代理类名,两个任务共同协作完成插件 ContentProvider 加载。
该任务作用是插件 AndroidManifest.xml 文件记录的 Activity、Service、Receiver 等所有 class 文件插入对应的 SplitInstallHelper#loadResources(...) 方法,到达访问插件资源目的。
以插件 Activity 为例。
public class JavaSampleActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_java_sample);
}
@Override
public Resources getResources() {
//自动注入如下方法
SplitInstallHelper.loadResources(this, super.getResources());
return super.getResources();
}
}
Qigsaw 提供两种加载方式加载插件 apk,详见 SplitLoad。MULTIPLE_CLASSLOADER 加载方式指每个插件由独立 ClassLoader 加载,SINGLE_CLASSLOADER 指所有插件由同一 ClassLoader 加载(涉及私有 Api 访问)。PlayCoreLibrary 提供 SplitInstallHelper#loadLibrary(...) 用于访问插件 so 文件。当 Qigsaw 插件加载方式为 MULTIPLE_CLASSLOADER 时,调用 SplitInstallHelper#loadLibrary(...) 会导致插件 so 加载失败。
PlayCoreLibrary 中 SplitInstallHelper#loadLibrary(...) 源码如下。
public static void loadLibrary(Context context, String libraryName) {
try {
System.loadLibrary(libraryName);
} catch (UnsatisfiedLinkError error) {
boolean loadLibOK = false;
try {
String nativeLibraryDir = context.getApplicationInfo().nativeLibraryDir;
String libName = System.mapLibraryName(libraryName);
String targetLibFile = (new StringBuilder(1 + String.valueOf(nativeLibraryDir).length() + String.valueOf(libName).length())).append(nativeLibraryDir).append("/").append(libName).toString();
if ((new File(targetLibFile)).exists()) {
System.load(targetLibFile);
loadLibOK = true;
}
} catch (UnsatisfiedLinkError e) {
throw e;
}
if (!loadLibOK) {
throw error;
}
}
}
PlayCoreLibrary 是 Google 提供 Android App Bundles 安装 dynamic feature 使用,非开源。
原因是 callerClass 是 SplitInstallHelper,它是由 PathClassLoader 加载。首先来看 System.loadLibrary(String libName) 源码。
@CallerSensitive
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
最终是调用至 Runtime#loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) 方法。
private synchronized void loadLibrary0(ClassLoader loader, Class<?> callerClass, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
// Android-note: BootClassLoader doesn't implement findLibrary(). http://b/111850480
// Android's class.getClassLoader() can return BootClassLoader where the RI would
// have returned null; therefore we treat BootClassLoader the same as null here.
if (loader != null && !(loader instanceof BootClassLoader)) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
// We know some apps use mLibPaths directly, potentially assuming it's not null.
// Initialize it here to make sure apps see a non-null value.
getLibPaths();
String filename = System.mapLibraryName(libraryName);
String error = nativeLoad(filename, loader, callerClass);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
通过上述源码分析可知,通过 callerClass 拿到对应 ClassLoader,然后再调用该 ClassLoader 的 findLibrary 方法即可获取 so 路径。
在 MULTIPLE_CLASSLOADER 方式下,使用 SplitInstallHelper#loadLibrary(...) 加载插件 so,callerClass 是 SplitInstallHelper,其对应 ClassLoader 是 PathClassLoader(正常情况下)。然而每个插件由独立 SplitDexClassLoader 加载,因此 PathClassLoader 中是无法找到插件对应 so 文件,进入抛出 UnsatisfiedLinkError 错误。
为保证 callerClass 是对应插件中的类,SplitResourcesLoaderTransform 为每个插件创建一个类用于调用 System.loadLibrary(String libName) 方法。
Qigsaw 打包插件提供插件升级编译处理,插件升级操作说明见插件升级指南。
当配置了 QigsawSplitExtension 中 oldApk,Qigsaw 打包插件会试图解压内置插件以及插件信息文件。该过程由 QigsawProcessOldApkTask 任务完成。
在 QigsawAssembleTask 生成新的插件信息文件时,首先判断老的插件信息文件是否存在,如果存在则应用老的插件信息文件部分信息,如 qigsawId 及版本号未改变的插件信息。具体操作详见SplitDetailsProcessorImpl
Android App Bundles 支持 feature-in-feature 功能,即插件可以依赖另外一个插件(不能出现循环依赖)。Qigsaw 打包插件通过 AnalyzeSplitDependenciesTask 任务可分析得出插件直接依赖了哪些插件并用文件记录。
在 QigsawAssembleTask 中,通过 AnalyzeSplitDependenciesTask 生成的依赖信息文件来分析某一插件依赖的所有插件(包括直接依赖和间接依赖)。详见 QigsawAssembleTask#findAllSplitDependencies(String splitName)。
另外为保证被依赖的插件优先加载,QigsawAssembleTask 中采取拓扑排序重排插件信息顺序。