Skip to content

Qigsaw 源码分析之打包插件流程分析

kissonchen edited this page Aug 7, 2020 · 2 revisions

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 任务中,主要完成以下事情。

  1. 生成插件信息文件,名称格式为 qigsaw_${appVersionName}_${splitInfoVersion}.json。
  2. 插件信息文件拷贝至 mergeAssetsTask 的产物目录下。
  3. 插件 apk 文件拷贝至 mergeAssetsTask 或 mergeJniLibsTask 的产物目录下。

Transform 任务

Qigsaw 打包插件中,有三个 Transform 任务,分别是 ComponentInfoTransform、SplitLibraryLoaderTransform、SplitResourcesLoaderTransform。

ComponentInfoTransform

该任务有两个作用。

  1. 生成 com.iqiyi.android.qigsaw.core.extension.ComponentInfo.class 文件,记录插件 AndroidManifest.xml 中 Activity、Service、Receiver、ContentProvider、Application 信息。Qigsaw 不支持四大组件更新,将插件四大组件用 class 文件记录可以尽最大可能避免插件四大组件信息解析失败,提高访问效率。
  2. 生成插件 ContentProvider 代理类(均继承自SplitContentProvider),用于加载插件 ContentProvider。QigsawProcessManifestTask 任务将编译后 AndroidManifest.xml 中插件 ContentProvider 的名字修改为代理类名,两个任务共同协作完成插件 ContentProvider 加载。

SplitResourcesLoaderTransform

该任务作用是插件 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();
    }
}

SplitLibraryLoaderTransform

Qigsaw 提供两种加载方式加载插件 apk,详见 SplitLoadMULTIPLE_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 打包插件提供插件升级编译处理,插件升级操作说明见插件升级指南

当配置了 QigsawSplitExtensionoldApk,Qigsaw 打包插件会试图解压内置插件以及插件信息文件。该过程由 QigsawProcessOldApkTask 任务完成。

在 QigsawAssembleTask 生成新的插件信息文件时,首先判断老的插件信息文件是否存在,如果存在则应用老的插件信息文件部分信息,如 qigsawId 及版本号未改变的插件信息。具体操作详见SplitDetailsProcessorImpl

插件间依赖分析

Android App Bundles 支持 feature-in-feature 功能,即插件可以依赖另外一个插件(不能出现循环依赖)。Qigsaw 打包插件通过 AnalyzeSplitDependenciesTask 任务可分析得出插件直接依赖了哪些插件并用文件记录。

在 QigsawAssembleTask 中,通过 AnalyzeSplitDependenciesTask 生成的依赖信息文件来分析某一插件依赖的所有插件(包括直接依赖和间接依赖)。详见 QigsawAssembleTask#findAllSplitDependencies(String splitName)

另外为保证被依赖的插件优先加载,QigsawAssembleTask 中采取拓扑排序重排插件信息顺序。