【Qigsaw系列01】Qigsaw编译插件做了哪些事

目录:

  • 1. Qigsaw 简介
  • 2. Qigsaw-app-plugin 插件
  • 3. Qigsaw-dyanmic-feature-plugin 插件

 

1. Qigsaw 简介

Qigsaw 是爱奇艺自主研发的动态化框架,其核心优势如下:

  • 利用 Android App Bundle 开发套件,极速开发体验。
  • 支持 Android App Bundle 所有功能特性,"山寨" Play Core Library 公开接口实现,开发者阅读官方文档即可愉快开发。
  • 任何进程均可动态加载插件,支持 Android 四大组件动态加载。
  • 如果您的应用有出海需求,可无缝切换至 Android App Bundle 方案。
  • 仅一处 Hook,少量私有 API 访问,保证框架稳定性。 Android 动态化方案,在国内已蓬勃发展数年之久,其核心目的是减少应用包体积,提升应用安装率。

Qigsaw 传送门:https://github.com/iqiyi/Qigsaw

Qigsaw 官方文档传送门:https://www.bookstack.cn/read/Qigsaw/c47ca9c7359b0d0d.md

Android APP Bundle 介绍:https://developer.android.google.cn/guide/app-bundle?hl=zh-cn

 

2. Qigsaw-app-plugin 插件

细节分析

(1) ComponentInfoTransform

Map<String, List> addFieldMap = new HashMap<>()
        // 遍历 dynamic feature 工程
        for (Project dfProject : dfProjects) {
            def dfAndroid = dfProject.extensions.android
            String splitName = dfProject.name
            File splitManifestFile = null
            dfAndroid.applicationVariants.all { ApplicationVariant variant ->
                String dfVariantName = variant.name.capitalize()
                if (dfVariantName.equals(variantName)) {
                    Task processManifestTask = AGPCompat.getProcessManifestTask(dfProject, dfVariantName)
                    splitManifestFile = AGPCompat.getMergedManifestFileCompat(processManifestTask)
                }
            }
            // 解析 dynamic feature 的 manifest
            ManifestReader manifestReader = new ManifestReaderImpl(splitManifestFile)
            List<String> activities = new ArrayList<>()
            List<String> services = new ArrayList<>()
            List<String> receivers = new ArrayList<>()
            List<String> providers = new ArrayList<>()
            List<String> applications = new ArrayList<>()
            
            // 获取 dynamic feature 的 Application、Activity、Service、Receiver、Provider
            String applicationName = manifestReader.readApplicationName().name
            if (applicationName != null && applicationName.length() > 0) {
                applications.add(applicationName)
            }

            manifestReader.readActivities().each {
                activities.add(it.name)
            }

            manifestReader.readServices().each {
                services.add(it.name)
            }
            manifestReader.readReceivers().each {
                receivers.add(it.name)
            }

            manifestReader.readProviders().each {
                providers.add(it.name)
            }

            addFieldMap.put(splitName + "_APPLICATION", applications)
            addFieldMap.put(splitName + "_ACTIVITIES", activities)
            addFieldMap.put(splitName + "_SERVICES", services)
            addFieldMap.put(splitName + "_RECEIVERS", receivers)
            addFieldMap.put(splitName + "_PROVIDERS", providers)
        }

然后通过 ASM 生成 ComponentInfo.java:

package com.iqiyi.android.qigsaw.core.extension;

public class ComponentInfo {
    public static final String native_ACTIVITIES = "com.iqiyi.qigsaw.sample.ccode.NativeSampleActivity";
    public static final String java_ACTIVITIES = "com.iqiyi.qigsaw.sample.java.JavaSampleActivity";
    public static final String java_APPLICATION = "com.iqiyi.qigsaw.sample.java.JavaSampleApplication";

    public ComponentInfo() {
    }
}

创建 ContentProvider 代理类:

app 启动时 provider 的执行时机是比较靠前的,在这个过程中插件 APK 并没有加载进来,一定会报 ClassNotFound。所以将插件 APK 的 provider 生成一个代理类,然后替换掉,如果插件没有加载进来,代理类什么也不执行即可:

Application->attachBaseContext ==>ContentProvider->onCreate ==>Application->onCreate ==>Activity->onCreate
if (name.endsWith("PROVIDERS")) {
    for (String providerName : value) {
        String splitName = name.split("_")[0]
        String providerClassName = providerName + "_Decorated_" + splitName
        createSimpleClass(dest, providerClassName, "com.iqiyi.android.qigsaw.core.splitload.SplitContentProvider", null)
    }
}

base apk 的 manifest 会提前合并 split 中的所有四大组件,看看 base apk 的 manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="271"
    android:versionName="1.0.0"
    android:compileSdkVersion="29"
    android:compileSdkVersionCodename="10"
    package="com.iqiyi.qigsaw.sample"
    platformBuildVersionCode="29"
    platformBuildVersionName="10">

    <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application>
        <activity
            android:name="com.iqiyi.qigsaw.sample.MainActivity">
            <intent-filter>

                <action
                    android:name="android.intent.action.MAIN" />

                <category
                    android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name="com.iqiyi.qigsaw.sample.QigsawInstaller" />

        // ....
        <provider
            android:name="com.iqiyi.qigsaw.sample.java.JavaContentProvider_Decorated_java"
            android:enabled="true"
            android:exported="false"
            android:authorities="java.feature" />

        <activity
            android:name="com.iqiyi.qigsaw.sample.ccode.NativeSampleActivity">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
            </intent-filter>
        </activity>
    </application>
</manifest>

可以看到 java split 中的 JavaContentProvider, 编译后,为其生成了一个 com.iqiyi.qigsaw.sample.java.JavaContentProvider_Decorated_java

package com.iqiyi.qigsaw.sample.java;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class JavaContentProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}

生成的代理 ContentProvider:

package com.iqiyi.qigsaw.sample.java;
 
import com.iqiyi.android.qigsaw.core.splitload.SplitContentProvider;
 
public class JavaContentProvider_Decorated_java extends SplitContentProvider {
    public JavaContentProvider_Decorated_java() {
    }
}

看看 SplitContentProvider:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class SplitContentProvider extends ContentProviderProxy {

    @Override
    protected boolean checkRealContentProviderInstallStatus(String splitName) {
        if (getRealContentProvider() != null) {
            return true;
        } else {
            if (SplitLoadManagerService.hasInstance()) {
                SplitLoadManager loadManager = SplitLoadManagerService.getInstance();
                loadManager.loadInstalledSplits();
                return getRealContentProvider() != null;
            }
        }
        return false;
    }
}

再看看 ContentProviderProxy:

@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public abstract class ContentProviderProxy extends ContentProvider {

    private static final String TAG = "Split:ContentProviderProxy";

    private ContentProvider realContentProvider;

    private static final String NAME_INFIX = "_Decorated_";

    private ProviderInfo providerInfo;

    private String realContentProviderClassName;

    private String splitName;

    protected ContentProvider getRealContentProvider() {
        return realContentProvider;
    }

    void activateRealContentProvider(ClassLoader classLoader) throws AABExtensionException {
        Throwable error = null;
        try {
            realContentProvider = createRealContentProvider(classLoader);
        } catch (ClassNotFoundException e) {
            error = e;
        } catch (IllegalAccessException e) {
            error = e;
        } catch (InstantiationException e) {
            error = e;
        }
        if (error != null) {
            throw new AABExtensionException(error);
        }
    }

    private ContentProvider createRealContentProvider(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        if (getContext() == null || realContentProviderClassName == null) {
            SplitLog.w(TAG, "Cause of null context, we can\'t create real provider " + realContentProviderClassName);
            return null;
        }
        ContentProvider realContentProvider = (ContentProvider) classLoader.loadClass(realContentProviderClassName).newInstance();
        realContentProvider.attachInfo(getContext(), providerInfo);
        SplitLog.d(TAG, "Success to create provider " + realContentProviderClassName);
        return realContentProvider;
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    protected abstract boolean checkRealContentProviderInstallStatus(String splitName);

    @Override
    public void attachInfo(Context context, ProviderInfo info) {
        String className = getClass().getName();
        String[] cuts = className.split(NAME_INFIX);
        this.realContentProviderClassName = cuts[0];
        this.splitName = cuts[1];
        super.attachInfo(context, info);
        this.providerInfo = new ProviderInfo(info);
        AABExtension.getInstance().put(splitName, this);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (checkRealContentProviderInstallStatus(splitName)) {
            realContentProvider.onConfigurationChanged(newConfig);
        }
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        if (checkRealContentProviderInstallStatus(splitName)) {
            return realContentProvider.query(uri, projection, selection, selectionArgs, sortOrder);
        }
        return null;
    }

    // ...

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        if (checkRealContentProviderInstallStatus(splitName)) {
            return realContentProvider.insert(uri, values);
        }
        return null;
    }

    // ...

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        if (checkRealContentProviderInstallStatus(splitName)) {
            return realContentProvider.delete(uri, selection, selectionArgs);
        }
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        if (checkRealContentProviderInstallStatus(splitName)) {
            return realContentProvider.update(uri, values, selection, selectionArgs);
        }
        return 0;
    }
}

(2) ResourcesLoaderTransform

处理组件资源加载问题:

byte[] injectClass(Path path, String className) {
        byte[] ret = null
        if (isActivity(className) && !isInNoNeedInjectResourceActivities(className)) {
            ret = new ActivityWeaver().weave(path.newInputStream())
        } else if (isService(className)) {
            ret = serviceWeaver.weave(path.newInputStream())
        } else if (isReceiver(className)) {
            ret = receiverWeaver.weave(path.newInputStream())
        }
        return ret
    }

装饰 Activity: getResources()、getAssets() 方法插入 SplitInstallHelper.loadResources()
装饰 Service: onCreate() 方法插入 SplitInstallHelper.loadResources()
装饰 Receiver: onReceive() 方法插入 SplitInstallHelper.loadResources()

如 dynamic feature 的 java 工程的 JavaSampleActivity,看看编译后的 class:

public class JavaSampleActivity extends Activity {
    public AssetManager getAssets() {
        SplitInstallHelper.loadResources(this, super.getResources());
        return super.getAssets();
    }

    public Resources getResources() {
        SplitInstallHelper.loadResources(this, super.getResources());
        return super.getResources();
    }

    /* access modifiers changed from: protected */
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_java_sample);
    }
}

(3) Task: qigsawProcessOldApk

这个 task 主要在动态更新时使用,动态更新时,我们需要将 old APK 和 mapping 文件放在 app module 的 qigsaw 目录中,如下:

看下 app.apk 中我们需要关注哪几个文件:

  • - java.zip: demo 中内置的 dynamic feature 包
  • - qigsaw_1.0.0_1.0.0.json: 旧的 qigsaw 配置文件

简单看下 qigsaw 配置文件内容:

{
	"qigsawId": "1.0.0_1464da0", // qigsaw id: 唯一标识
	"appVersionName": "1.0.0", // app 版本号
	"builtInUrlPrefix": "assets://", // 内置 dynamic feature 的前缀,可选项为 assets 和 libs,存放位置不一样
	"splits": [{
		"splitName": "java", // java dynamic feature
		"url": "assets://java.zip", // 内置,存放在 base apk 的 assets 目录中
		"builtIn": true, // 内置
		"onDemand": false,
		"size": 10294,
		"applicationName": "com.iqiyi.qigsaw.sample.java.JavaSampleApplication",
		"version": "1.1@1", // versionCode@versionName
		"md5": "46511ef30ff34575a41fe9053982f1bf", // dynamic feature apk md5
		"workProcesses": [":qigsaw", ""],
		"minSdkVersion": 14,
		"dexNumber": 1 // dex 数
	}, {
		"splitName": "assets",
		"url": "https://wos2.58cdn.com.cn/FgHcBazYFgLi/cutpackage/assets1620644561953.apk", // 非内置,远程下载
		"builtIn": false, // 非内置
		"onDemand": true,
		"size": 8294,
		"version": "1.0@1",
		"md5": "fc8c83923909c4b10af96974c3314256",
		"minSdkVersion": 14,
		"dexNumber": 1
	}, {
		"splitName": "native",
		"url": "https://wos2.58cdn.com.cn/FgHcBazYFgLi/cutpackage/native1620644562358.apk",
		"builtIn": false,
		"onDemand": true,
		"size": 21490,
		"version": "1.0@1",
		"md5": "3451553544d37413d81f3b0096a2095f",
		"minSdkVersion": 14,
		"dexNumber": 1,
 	        // ...
	}]
}

qigsawProcessOldApk task 的作用就是如果是动态更新,则从 old APK 中解压出内置的 dynamic feature apk (无需重新打包内置 apk,直接拷贝) 和旧的 qigsaw 配置文件 (读取旧的 qigsaw id)

 

(4) Task: qigsawAssemble

  • 打包 base APK, dynamic feature APKs
  • 对 split APK 进行重签名(如果未签名的话,内部有检测)
  • 内置的 split APK 拷贝到基础包中 (assets/libs)
  • 获取每个 split 的信息,参考 qigsaw 配置文件的数据结果,包括 split 名称、url、是否内置、md5 等,保存在内存中

 

(5) Task: qigsawUploadSplit

根据自定义的上传配置,上传非内置的 split apk, 获取 url

 

(6) Task: splitConfigJson

根据 4、5 步骤数据生成 qigsaw 配置文件:

生成 qigsaw config json:

{
	"qigsawId": "1.0.0_1464da0", // 动态更新读取旧的 qigsawId, 否则生成新的
	"appVersionName": "1.0.0",
	"builtInUrlPrefix": "assets://",
	"splits": [{
		"splitName": "java",
		"url": "assets://java.zip",
		"builtIn": true, // 内置 split apk, 拷贝到 assets 或 libs
		"onDemand": false,
		"size": 11714,
		"applicationName": "com.iqiyi.qigsaw.sample.java.JavaSampleApplication",
		"version": "1.1@1",
		"md5": "3e3bf399e35cad19400b84c6fd5774da",
		"workProcesses": [":qigsaw", ""],
		"minSdkVersion": 14,
		"dexNumber": 2
	},{
		"splitName": "native",
		"url": "https://wos2.58cdn.com.cn/FgHcBazYFgLi/cutpackage/native1620620654051.apk", // 上传后的 url
		"builtIn": false, // 非内置,上传后生成 url
                ...
          },
         ...
       ]
       "updateSplits":[...] // 动态更新时会有此 key
}

(7) Task: qigsawProguardConfig

如果使用 proguard, 则 hook proguard task,如果使用 R8, 则 hook R8:

boolean proguardEnable = variant.getBuildType().isMinifyEnabled()
if (proguardEnable) {
    QigsawProguardConfigTask proguardConfigTask = project.tasks.create("qigsawProcess${variantName}Proguard", QigsawProguardConfigTask)
    proguardConfigTask.outputDir = qigsawProguardOutputDir
    proguardConfigTask.initArgs(applicationId)
    proguardTask = AGPCompat.getProguardTask(project, variantName)
    if (proguardTask != null) {
         proguardTask.dependsOn proguardConfigTask
    } else {
         if (r8Task != null) {
            r8Task.dependsOn proguardConfigTask
         }
    }
    proguardConfigTask.mustRunAfter qigsawProcessManifestTask
    //set qigsaw proguard file.
    variant.getBuildType().buildType.proguardFiles(proguardConfigTask.getOutputProguardFile())
}

注入 qigsaw 本身的混淆配置:

static final String PROGUARD_CONFIG_SETTINGS = "-keep class com.google.android.play.core.splitcompat.SplitCompat{n *;n }n" +
            "-keep interface com.google.android.play.core.listener.StateUpdatedListener{n *;n }n" +
            "-keep interface com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener{n *;n }n" +
            "-keep interface com.google.android.play.core.splitinstall.SplitInstallManager{n *;n }n" +
            "-keep class com.google.android.play.core.splitinstall.SplitInstallManagerFactory{n *;n }n" +
            "-keep class com.google.android.play.core.splitinstall.SplitInstallRequest{n *;n }n" +
            "-keep class com.google.android.play.core.splitinstall.SplitInstallException{n *;n }n" +
            "-keep class com.google.android.play.core.splitinstall.SplitInstallRequest$Builder{n *;n }n" +
            "-keep class com.google.android.play.core.splitinstall.SplitInstallHelper{n *;n }n" +
            "-keep interface com.google.android.play.core.splitinstall.model.SplitInstallErrorCode{n *;n }n" +
            "-keep interface com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus{n *;n }n" +
            "-keep class com.google.android.play.core.splitinstall.SplitInstallSessionState{n *;n }n" +
            "-keep class com.google.android.play.core.tasks.Task{n *;n }n" +
            "-keep class com.google.android.play.core.tasks.TaskExecutors{n *;n }n" +
            "-keep class com.google.android.play.core.tasks.Tasks{n *;n }n" +
            "-keep class com.google.android.play.core.tasks.RuntimeExecutionException{n *;n }n" +
            "-keep interface com.google.android.play.core.tasks.OnSuccessListener{n *;n }n" +
            "-keep interface com.google.android.play.core.tasks.OnFailureListener{n *;n }n" +
            "-keep interface com.google.android.play.core.tasks.OnCompleteListener{n *;n }n" +
            "-keep class com.split.signature.**{n *;n }n" +
            "-keep class com.iqiyi.android.qigsaw.core.extension.AABExtension{n public <methods>;n }n" +
            "-keep class com.iqiyi.android.qigsaw.core.splitdownload.Downloader{n *;n }n" +
            "-keep class * implements com.iqiyi.android.qigsaw.core.splitdownload.Downloader{n *;n }n" +
            "-keep class com.iqiyi.android.qigsaw.core.Qigsaw{n public <methods>;n }n" +
            "-keep class com.iqiyi.android.qigsaw.core.extension.ComponentInfo{n *;n }n" +
            "-keep class com.iqiyi.android.qigsaw.core.splitlib.**{n *;n }n"

如果动态更新,则读取旧的 mapping 文件:

    @TaskAction
    void updateQigsawProguardConfig() {
        if (outputDir.exists()) {
            outputDir.deleteDir()
        }
        outputDir.mkdirs()
        File file = new File(outputDir, PROGUARD_CONFIG_NAME)
        QigsawLogger.w("try update qigsaw proguard file with ${file}")
        // Write our recommended proguard settings to this file
        FileWriter fw = new FileWriter(file.path)
        if (applyMappingFile != null) {
            if (applyMappingFile.exists() && applyMappingFile.isFile() && applyMappingFile.length() > 0) {
                QigsawLogger.w("try to add applymapping ${applyMappingFile.path} to build the package")
                fw.write("-applymapping " + applyMappingFile.absolutePath)
                fw.write("n")
            } else {
                QigsawLogger.e("applymapping file ${applyMappingFile.absolutePath} is not valid, just ignore!")
            }
        } else {
            QigsawLogger.e("applymapping file is null, just ignore!")
        }
        fw.write(PROGUARD_CONFIG_SETTINGS + "-keep class ${applicationId}.QigsawConfig{n *;n }n")
        fw.close()
    }

(8) Task: package

gradle < 3.5.0: hook transformDexWithDexSplitter, 重新 merge dex 

packageTask.doFirst {
     if (versionAGP < VersionNumber.parse("3.5.0")) {
        dexSplitterTask = AGPCompat.getDexSplitterTask(project, variantName)
        if (dexSplitterTask != null) {
            def startTime = new Date()
            List<File> dexFiles = new ArrayList<>()
            inputs.files.each { File file ->
                file.listFiles().each { x ->
                    if (x.name.endsWith(".dex") && x.name.startsWith("classes")) {
                        dexFiles.add(x)
                    }
                }
            }
            DexReMergeHandler handler = new DexReMergeHandler(project, variant)
            handler.reMerge(dexFiles)
        }
    }

}              

(9) Task: output

拷贝 qigsaw config json 到 outputs 目录:

  • base.apk
  • output.json
  • qigsaw_{$appVersion}_{$splitVersion}.json

 

 

3. Qigsaw-dynamic-feature-plugin 插件

参考 ResourcesLoaderTransform

 






 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值