Android App Bundle为Qigsaw
的前置依赖知识点。
Android App Bundle 是Android
新推出的一种官方发布格式.aab
,可让您以更高效的方式开发和发布应用。借助 Android App Bundle
,您可以更轻松地以更小的应用提供优质的使用体验,从而提升安装成功率并减少卸载量。转换过程轻松便捷。您无需重构代码即可开始获享较小应用的优势。改用这种格式后,您可以体验模块化应用开发和可自定义功能交付,并从中受益(PS:必须依赖于GooglePlay
)。
qigsaw
基于AAB
实现,同时完全仿照AAB
提供的play core library
接口加载插件,开发查阅官方文档即可开始开发。如果有国际化需求的公司可以在国内版和国际版上无缝切换。同时Qigsaw
实现0 hook
,仅有少量私有 API 访问,保证其兼容性和稳定性。
Github:Qigsaw
本篇文章主要讲述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 activities
Set services
Set 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 {}
为啥这么做呢?
因为在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打包流程
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
。
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
dependencies;//feature包的依赖; private Set workProcesses;//feature包AndroidManifest.xml中的Activity、Service、Receiver、provider配置的进程; private List apkData;//SplitInfo.SplitApkData数据 private List 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
加载的压缩包
下期研究知识点
- 混淆相关使用操作;
-
Tinker
热修改相关使用操作;