目录:
- 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