hi,2021
Android App Bundle为Qigsaw的前置依赖知识点。
Android App Bundle 是Android新推出的一种官方发布格式.aab,可让您以更高效的方式开发和发布应用。借助 Android App Bundle,您可以更轻松地以更小的应用提供优质的使用体验,从而提升安装成功率并减少卸载量。转换过程轻松便捷。您无需重构代码即可开始获享较小应用的优势。改用这种格式后,您可以体验模块化应用开发和可自定义功能交付,并从中受益(PS:必须依赖于GooglePlay)。
qigsaw基于AAB实现,同时完全仿照AAB提供的play core library接口加载插件,开发查阅官方文档即可开始开发。如果有国际化需求的公司可以在国内版和国际版上无缝切换。同时Qigsaw实现0 hook,仅有少量私有 API 访问,保证其兼容性和稳定性。
本篇文章主要讲述Qigsaw相关的plugin。
Qigsaw插件
主工程进行进行apply plugin: 'com.iqiyi.qigsaw.application'插件的依赖;
feature工程进行以下依赖:
apply plugin: 'com.android.dynamic-feature'
apply plugin: 'com.iqiyi.qigsaw.dynamicfeature'
gradle.properties文件中配置QIGSAW_BUILD=true,才会有feature包的一些信息生成。
com.iqiyi.qigsaw.application
com.iqiyi.qigsaw.application.properties文件内容为:
implementation-class=com.iqiyi.qigsaw.buildtool.gradle.QigsawAppBasePlugin
QigsawAppBasePlugin默认会注册一个 SplitComponentTransform,在开启QIGSAW_BUILD=true之后还会注册SplitResourcesLoaderTransform。通过 Transform实现对插件内容的AOP。
QigsawAppBasePlugin除过注册两个Transform之外,为主要的是处理插件和基础包信息生成Qigsaw产物。
com.iqiyi.qigsaw.dynamicfeature
com.iqiyi.qigsaw.dynamicfeature.properties文件内容为:
implementation-class=com.iqiyi.qigsaw.buildtool.gradle.QigsawDynamicFeaturePlugin
QigsawDynamicFeaturePlugin在开启QIGSAW_BUILD=true之后会注册SplitResourcesLoaderTransform以及SplitLibraryLoaderTransform实现对插件内容的AOP。
SplitResourcesLoaderTransform
主要是向Activity、Service和Receiver类中的getResources注入SplitInstallHelper.loadResources(this, super.getResources())。
interface SplitComponentWeaver {
/**
* 链接目标
*/
String CLASS_WOVEN = "com/google/android/play/core/splitinstall/SplitInstallHelper"
/**
* 链接方法
*/
String METHOD_WOVEN = "loadResources"
byte[] weave(InputStream inputStream)
}
相关注入类为:
class SplitResourcesLoaderInjector {
WaitableExecutor waitableExecutor
/**
* 预埋的 Activity
*/
Set<String> activities
Set<String> services
Set<String> receivers
SplitActivityWeaver activityWeaver
SplitServiceWeaver serviceWeaver
SplitReceiverWeaver receiverWeaver
/**部分代码省略**/
}
其中基础包和插件的区别主要是注册的目标不同:
基础包只是读取
build.gradle文件中的qigsawSplit.baseContainerActivities配置的Activity。
而插件需要读取
AndroidManifest.xml文件中的Activity、Service和Receiver。
SplitInstallHelper.loadResources(this, super.getResources());的作用是将所有插件资源路径添加到AssetManager中,这样各个插件就可以访问所有的资源,关键实现代码如下:
static Method getAddAssetPathMethod() throws NoSuchMethodException {
if (addAssetPathMethod == null) {
addAssetPathMethod = HiddenApiReflection.findMethod(AssetManager.class, "addAssetPath", String.class);
}
return addAssetPathMethod;
}
SplitComponentTransform
该Transform主要进行了两个操作 :
- 读取各个插件
apk的Manifest文件,创建ComponentInfo类并将将各个插件apk的Application,Activity,Service,Recevier记录在该类的字段中,字段名称以工程名+组件类型命名,值为各个插件apk包含的组件,如过包含多个用逗号隔开。
//com.iqiyi.android.qigsaw.core.extension.ComponentInfo
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";
}
- 为每个
provider创建代理类 类名为String providerClassName=providerName+"Decorated"+splitName,其中providerName为原始provider类名,splitName为插件apk对应的名称,并且该类继承SplitContentProvider。
public class JavaContentProvider_Decorated_java extends SplitContentProvider {}
provider_deccorated.png
为啥这么做呢?
因为在app启动时provider的执行时机是比较靠前的,
Application->attachBaseContext ==>ContentProvider->onCreate ==>Application->onCreate ==>Activity->onCreate在这个过程中我们的插件apk并没有加载进来,一定会报ClassNotFound。所以我们将插件apk的provider生成一个代理类,然后替换掉,如果插件没有加载进来,代理类什么也不执行就可以了。很好的解决了我们的问题。
SplitLibraryLoaderTransform
SplitLibraryLoaderTransform类进行的操作是向dynamic-feature构建apk的过程中,创建以 "com.iqiyi.android.qigsaw.core.splitlib." + project.name + "SplitLibraryLoader"的类。
// com.iqiyi.android.qigsaw.core.splitlib.assetsSplitLibraryLoader
// com.iqiyi.android.qigsaw.core.splitlib.javaSplitLibraryLoader
// com.iqiyi.android.qigsaw.core.splitlib.nativeSplitLibraryLoader
package com.iqiyi.android.qigsaw.core.splitlib;
public class javaSplitLibraryLoader {
public void loadSplitLibrary(String str) {
System.loadLibrary(str);
}
}
这个类的作用是啥呢?
下面我们来解释一下,你会发现很有趣的。
-
Qigsaw是基于对于com.google.android.play.core对外暴露的方法,进行了自定义实现。因为aab目前只能对google play上发布应用起作用,所以开发者重新实现了一套com.google.android.play.core包名的第三方库,这样就可以做到在国内市场,与国外应用市场无缝迁移。 -
Qigsaw提供两种加载方式加载插件apk,单Classloader和多Classloader模式,单Classloader涉及私有api访问,而多Classloader不涉及私有api访问。
该类的存在就是为了解决多Classloader模式下的so加载问题
System.loadLibrary(str);该方法会使用调用方的classloader从中获取so信息并加载。
//java.lang.System.java
@CallerSensitive
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
由于多Classloader模式下,每个插件都要各自的Classloader,so与dex都在各自的Classloader中记录,所以在多Classloader模式下, System.loadLibrary应由插件apk各自的Classloader调用。具体实现可参考SplitLibraryLoaderHelper类。
//com.iqiyi.android.qigsaw.core.splitload.SplitLibraryLoaderHelper.java
private static boolean loadSplitLibrary0(ClassLoader classLoader, String splitName, String name) {
try {
Class<?> splitLoaderCl = classLoader.loadClass("com.iqiyi.android.qigsaw.core.splitlib." + splitName + "SplitLibraryLoader");
Object splitLoader = splitLoaderCl.newInstance();
Method method = HiddenApiReflection.findMethod(splitLoaderCl, "loadSplitLibrary", String.class);
method.invoke(splitLoader, name);
return true;
} catch (Throwable ignored) {
}
return false;
}
Qigsaw编译解析
Qigsaw打包流程
qigsaw_plugin_flow_chart.png
copySplitManifestDebug
实现feature包下生成的AndroidManifest.xml文件的拷贝。
目标文件和地址:featureName/build/intermediates/merged_manifests/debug/AndroidManifest.xml。
拷贝后的地址:app/build/intermediates/qigsaw/split-outputs/manifests/debug。
拷贝后的文件名:$featureName.xml
ProcessTaskDependenciesBetweenBaseAndSplitsWithQigsaw
触发copySplitManifestDebug任务,将feature包生成的产物和数据输出到qigsawProcessDebugManifest任务中。
extractTargetFilesFromOldApk
将app_debug.apk解压 将assets/目录下所有内容释放到app/build/intermediates/qigsaw/old-apk/target-files/xxx中
qigsawProcessDebugManifest
SplitComponentTransform创建的$ContentProviderName_Decorated_$featureName继承SplitContentProvider代替原有的Provider。
因为Provider 在应用启动的时候就需要加载,避免这个时候feature包没有下载下来,先加载一个代理的Provider。
provider.png
generateDebugQigsawConfig
生成以下文件:
@Keep
public final class QigsawConfig {
public static final String DEFAULT_SPLIT_INFO_VERSION = "1.0.0_1.0.0";
public static final String[] DYNAMIC_FEATURES = {"java", "assets", "native"};
public static final String QIGSAW_ID = "1.0.0_c40ab5d";
public static final boolean QIGSAW_MODE = Boolean.parseBoolean("true");
public static final String VERSION_NAME = "1.0.0";
}
QIGSAW_ID回先获取基础包的id,如果没有那么为当前的QigsawId。
processSplitApkDebug
每个feature都需要执行的任务,分别处理自己的的apk并生成对应的json文件。
-
将
feature包的apk文件解压到app/build/intermediates/qigsaw/split-outputs/unzip/debug/$featureName文件; -
遍历解压
apk中的lib文件目录,找到支持的ABI; -
如果有
lib文件有so文件,那么在该目录生成一个AndroidManifest.xml文件;-
将
lib文件和生成的AndroidManifest.xml压缩为protoAbiApk; -
利用
aapt2工具将protoAbiApk到binaryAbiApk中; -
将
binaryAbiApk进行签名生成app/build/intermediates/qigsaw/split-outputs/apks/debug/$feature-$abi.apk; -
生成
SplitInfo.SplitApkData数据;{ "abi": "x86", "url": "assets://qigsaw/native-x86.zip", "md5": "03a29962b87c6ed2a7961b6dbe45f532", "size": 8539 }
-
-
遍历解压
apk中除过lib之前的文件目录,压缩为$fearure-master-unsigned.apk。签名生成app/build/intermediates/qigsaw/split-outputs/apks/debug/$feature-master.apk; -
生成
SplitInfo.SplitApkData数据;{ "abi": "master", "url": "assets://qigsaw/native-master.zip", "md5": "3b89066aeaf7d2c2a59b4f3a10fef345", "size": 12824 } -
更具
lib文件下的数据生成SplitInfo.SplitLibData数据;{ "abi": "arm64-v8a", "jniLibs": [ { "name": "libhello-jni.so", "md5": "2938d8b40825e82715422dbdba479e4f", "size": 5896 } ] } -
最后生成每个
feature的SplitInfo数据,写入/app/build/intermediates/qigsaw/split-outputs/split-info/debug/$featureName.json文件;public class SplitInfo implements Cloneable, GroovyObject { private String splitName;//feature包名称 private boolean builtIn;//!onDemand||!releaseSplitApk(releaseSplitApk是gradle中配置项) private boolean onDemand;//取自AndroidManifest.xml中的onDemand private String applicationName;//feature应用名 private String version;//feature包中的versionname@versioncode private int minSdkVersion;//feature最低版本 private int dexNumber;//feature包中的dex数量 private Set<String> dependencies;//feature包的依赖; private Set<String> workProcesses;//feature包AndroidManifest.xml中的Activity、Service、Receiver、provider配置的进程; private List<SplitInfo.SplitApkData> apkData;//SplitInfo.SplitApkData数据 private List<SplitInfo.SplitLibData> libData;//SplitInfo.SplitLibData数据 }
qigsawAssembleDebug
- 将
build/intermediates/qigsaw/split-outputs/split-info/debug中的每个feature包生成的json合并; - 将合并之后的文件与基础包中的
Qigsaw配置文件进行对比,生成新的增量Qigsaw配置文件;- 对比规则是
verisonName相等的时候对比split.version,有一个不同就表示有更新; - 如果有更新,那么
QigsawId为基础包的QigsawId,并分析和修改split信息;- 修改
split信息的时候,相同的splitName对比split.version。如果相同那么split使用基础包的split信息,如果不同那么该split的builtIn=false,onDemand=true。并将有更新的split做记录(updatesplits字段值)。此时updateMode值为VERSION_CHANGED=1; - 没有任何修改,那么
updateMode值为VERSION_NO_CHANGED=2; - 如果没有基础包,那么
updateMode值为DEFAULT=0;
- 修改
- 对比规则是
- 分别判断如果
feature包的builtIn是false;- 判断是否有上传服务,如有有那么上传
feature包。上传成功后将对应的url地址修改为可下载的http地址。如果地址为空,或者不是http开头会跑异常。 - 如果没有实现上传服务那么
builtIn置为true;
- 判断是否有上传服务,如有有那么上传
- 格式化
split内容,写到build/intermediates/qigsaw/split-details/debug文件目录下。 - 将
updateMode值写到build/intermediates/qigsaw/split-details/debug/_update_record_.json文件。 - 如果
updateMode值为VERSION_NO_CHANGED,那么将intermediates/qigsaw/old-apk/target-files/debug/assets/qigsaw/qigsaw_*.json文件拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/qigsaw_*.json;- 否则将
app/build/intermediates/qigsaw/split-details/debug/qigsaw_*.json文件拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/qigsaw_*.json;
- 否则将
- 向
app/build/intermediates/qigsaw/split-details/debug/base.app.cpu.abilist.properties写入支持的abi,并将其拷贝到app/build/intermediates/merged_assets/debug/out/下面; - 遍历
feature生成的splitinfo信息,如果builtIn是true;- 如果
updateMode值为DEFAULT=0,将将app/build/intermediates/qigsaw/split-outputs/apks/debug/*.apk拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip; - 如果
updateMode值为DEFAULT!=0,判断该feature是否是在updateSplits中;- 如果是那么将
app/build/intermediates/qigsaw/split-outputs/apks/debug/*.apk拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip; - 如果不是将
app/build/intermediates/qigsaw/old-apk/target-files/debug/assets/qigsaw/*.zip拷贝到app/build/intermediates/merged_assets/debug/out/qigsaw/*.zip;
- 如果是那么将
- 如果
产物
Qigsaw配置文件
{
"qigsawId": "1.0.0_c40ab5d",
"appVersionName": "1.0.0",
"updateSplits": [
"java"
],
"splits": [
{
"splitName": "java",
"builtIn": true,
"onDemand": false,
"applicationName": "com.iqiyi.qigsaw.sample.java.JavaSampleApplication",
"version": "1.1@1",
"minSdkVersion": 14,
"dexNumber": 2,
"workProcesses": [
""
],
"apkData": [
{
"abi": "master",
"url": "assets://qigsaw/java-master.zip",
"md5": "658bc419a9d3c7812a36e61f6c5be4c4",
"size": 12822
}
]
}
{
"splitName": "native",
"builtIn": true,
"onDemand": true,
"version": "1.0@1",
"minSdkVersion": 14,
"dexNumber": 2,
"apkData": [
{
"abi": "arm64-v8a",
"url": "assets://qigsaw/native-arm64-v8a.zip",
"md5": "b01ad63db38a4ec5fad3284c573a02d3",
"size": 8545
},
{
"abi": "master",
"url": "assets://qigsaw/native-master.zip",
"md5": "3c41745a16a31e967cde8247009463f1",
"size": 12824
}
],
"libData": [
{
"abi": "arm64-v8a",
"jniLibs": [
{
"name": "libhello-jni.so",
"md5": "2938d8b40825e82715422dbdba479e4f",
"size": 5896
}
]
}
]
}
]
}
Qigsaw加载的压缩包
app-debug.png
下期研究知识点
- 混淆相关使用操作;
-
Tinker热修改相关使用操作;









网友评论