-
Notifications
You must be signed in to change notification settings - Fork 267
Home
Qigsaw 是爱奇艺提供的一套基于 Android App Bundle 的动态化方案,无需谷歌 Play Service 即可在国内体验 Android App Bundle开发工具。它支持动态下发插件 APK,让应用能够在不重新安装的情况下实现动态安装插件。
Android App Bundles(以下简称AAB)是2018年Google I/O大会带来的一款全新动态化框架,与Instant App不同,AAB是借助Split Apk完成动态加载。AAB的技术特点如下:
-
不同于传统的App以整个Apk为单位,一个App被分割成了一个base Apk和多个split Apk。
-
Apk在安装后,可以按需请求下发或者更新模块Apk。
-
split Apk可以根据配置进行更细的划分,并根据当前运行的设备特征来请求特定的Apk。
-
请求与安装过程必须通过Google Play商店进行,Google进一步统一标准和巩固自身生态。
-
split Apk功能只在Android 5.0(API 21)及以上机型使用,针对Android 4.4及以下机型Google Play商店仍会下发完整的Apk,以此来向下兼容。
以下是AAB运行时动态加载Apk与传统方式下发Apk的对比示意图:
AAB是一个动态化框架,它是利用Android Framework提供的split apks功能完成。所有安装split apk工作均是通过IPC交由google play完成。
由于国内无法使用Google Play商店,开发者即使将app打包成AAB格式也无法将其拆解下发到客户端。所以Qigsaw利用AAB开发套件,“山寨”Play Core Library公开接口实现,支持AAB所有功能特性,给开发者带来原生般的极速开发体验。Qigsaw的核心优势如下:
-
使用AAB原生的开发套件,带来极速的开发体验。
-
支持AAB所有功能特性,"山寨"Play Core Library公开接口实现,开发者可直接阅读Google官方文档进行开发。
-
任何进程均可动态加载插件,支持Android四大组件动态加载。
-
如果您的应用有出海需求,可无缝切换至Android App Bundle方案,无需进行二次开发。
-
仅一处Hook(Android9.0+无需Hook),少量私有API访问,保证框架稳定性。
国内 Android 插件化方案早已百花齐放,比如 DroidPlugin、Replugin、VirtualAPK 等。但这些插件化方案开发、维护成本都较高。
插件化方案一般是基线工程和插件工程独立开发,基线工程提供基础 SDK 供插件工程编译使用,如此可以避免类重复问题。这种开发模式对于大公司来说可以接受,因为人力充足,业务线丰富。但对于对于高速发展的创业公司来说,反而会成为一种负担,因为需要额外开发人力服务于插件工程。
另外国内插件化方案一般将插件 APK 运行插件:plugin
进程。如此您提供IPC方案,用于跨进程通信。
Qigsaw 完全利用 Android App Bundle 开发套件,插件作为一个模块和基线工程一起编译。
Android App Bundle 提供新的Gradle-Plugin com.android.dynamic-feature
, 用于编译插件。在整个工程中,Dynamic-Feature 模块可以使用其他任何模块,包括 Library、 Application、甚至其他Dynamic-Feature模块。您将以上帝视角俯视整个App工程,插件不再是围墙之外的孤儿。
开发阶段,你将享受 Android App Bundle 极速开发体验。
当您的项目编译完成后,Android Studio通过adb install-multiple
命令将 base APK 和插件 APK 安装至手机中。如果您的开发手机系统版本低于5.0,则会依据当前手机设备组装成一个完整apk文件安装至该手机。
vivo手机不支持多 APK 安装功能,因此开发过程中请选取其他手机。或者使用Qigsaw打包插件提供的qigsawAssemble${variantName}命令
发布阶段,你将享受 Qigsaw 提供的一条龙服务,插件开发者不必关心插件 APK 的上传分发。
Qigsaw打包插件支持内置插件,所有内置插件都会被拷贝至base apk的assets目录。对于非内置插件,Qigsaw打包插件会将其上传至CDN服务器(需要接入方实现上传CDN接口),解决业务方后顾之忧。
Qigsaw 目前在爱奇艺内部已有五个业务团队在使用,包括爱奇艺App和一些创新项目。国内的插件化方案,对于小团队来说开发维护成本很大,不仅需要维护Android工程相关SDK,还需要开发插件发布平台。Qigsaw 很好的解决这些问题,在爱奇艺一些创新项目中,Qigsaw发挥很大作用,为他们减少约20%~30%包体积。
Qigsaw 打包插件做的事情很少,因此当Android Gradle Plugin 或者 Gradle 升级时,Qigsaw 适配起来非常简单。然而国内插件化方案打包插件升级进度很慢,有些根本不再适配,严重阻碍开发效率。
在 Android Studio 3.2 或更高版本、Unity 2018.3 与 2017.4.17,以及 Cocos Creator 2.0.9 或更高版本中受支持。Qigsaw完全利用AAB的开发工具实现,开发环境与AAB要求一致。
Android Studio版本是3.2+,那么Gradle版本最好配置4.10.1(低于5.0版本),否则可能会引起一些编译问题。
Qigsaw提供了与AAB相似的接入方式,开发人员只需在gradle中进行配置引入。
在你的项目中新建名为qigsaw_feature
的dynamic feature模块,File
->New
->New Module
->Dynamic Feature Module
,默认填入相关配置信息完成插件创建。
在app/build.gradle
中会自动生成dynamicFeatures
数组,记录所有插件名称。
android {
compileSdkVersion 28
buildToolsVersion "29.0.0"
defaultConfig {
applicationId "com.example.qigsaw_sample"
minSdkVersion 14
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
//所有插件会被记录在数组dynamicFeatures中
dynamicFeatures = [":qigsaw_feature"]
}
在项目根目录下的build.gradle文件中增加qigsaw-gradle-plugin
作为依赖。
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.2'
classpath 'com.iqiyi.android.qigsaw:gradle-plugin:1.1.4'
}
}
在app/build.gradle
应用com.iqiyi.qigsaw.application
插件,并引入qigsaw-core-library
依赖。
apply plugin: 'com.android.application'
apply plugin: 'com.iqiyi.qigsaw.application'
...
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
......
implementation 'com.iqiyi.android.qigsaw:splitcore:1.1.4'
......
}
在app/build.gradle
中配置qigsawSplit
拓展选项。
qigsawSplit {
/**
* 可选项,默认为null
* 如果插件需要热更新,必须指定old apk文件路径。
*/
oldApk = projectDir.getPath() + "/qigsaw/app.apk"
/**
* 可选项, 默认为'null'
* 限制插件运行的进程,如果配置插件名,插件仅运行在其AndroidManifest文件声明的所有进程(android:process标签)。否则,所有进程均可运行该插件。
*/
restrictWorkProcessesForSplits = ['qigsaw_feature']
/**
* 可选项,默认为1.0.0
* 插件信息 JSON 文件版本号,当插件更新时,必须修改其值。
*/
splitInfoVersion '1.0.0'
/**
* 可选项,默认为null
* 如果插件需要热更新,需要指定old apk生成的mapping文件。
*/
applyMapping projectDir.getPath() + '/qigsaw/mapping.txt'
/**
* 可选项,默认为false
* 控制app编译期间是否上传插件至CDN。推荐Debug阶段为false,Release阶段为true。
*/
releaseSplitApk false
}
插件是否上传至您的CDN服务器,由两个因素解决。
一是
releaseSplitApk
必须设置为 true。二是插件 AndroidManifest 文件
onDemand
标签值设置为 true。
在dynamicFeature/build.gradle
中应用com.iqiyi.qigsaw.dynamicfeature
插件。
apply plugin: 'com.android.dynamic-feature'
apply plugin: 'com.iqiyi.qigsaw.dynamicfeature'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 14
targetSdkVersion 28
versionCode 1
versionName "1.0"
}
}
...
Qigsaw在app编译过程中将插件上传至您的CDN服务器,Qigsaw-Gradle-Plugin
提供SplitApkUploader接口供开发者实现插件上传逻辑。
interface SplitApkUploader {
/**
* 建议插件上传路径后缀拼接带上插件文件md5值,仅仅通过插件名无法唯一确定插件上传地址。
*/
String uploadSync(Project appProject, File splitApk, String splitName) throws SplitApkUploadException
}
具体实现可参考qigsaw-android-sample
中 buildSrc相关代码逻辑。
Qigsaw-Core-Library提供Downloader接口供开发者实现插件下载逻辑。考虑到app自身一般都有下载器模块,因此Qigsaw仅给出接口定义。
public interface Downloader {
/**
* 组任务立即下载,拥有最高下载优先级。
*/
void startDownload(int sessionId, List<DownloadRequest> requests, DownloadCallback callback);
/**
* 延时下载,即组任务设置为较低优先级再开始下载。
*/
void deferredDownload(int sessionId, List<DownloadRequest> requests, DownloadCallback callback, boolean usingMobileDataPermitted);
/**
* 根据sessionId取消对应组任务下载,注意必须是同步执行。
*/
boolean cancelDownloadSync(int sessionId);
/**
* 蜂窝网络情况下,允许插件下载总大小的阈值,Android App Bundle阈值设定是10M。
*/
long getDownloadSizeThresholdWhenUsingMobileData();
/**
* 设置延时组任务是否仅仅在wifi网络下载。
*/
boolean isDeferredDownloadOnlyWhenUsingWifiData();
}
自定义Downloader需满足以下功能:
-
sessionId唯一标识组任务。
-
支持组任务下载,取消组任务。
-
所有文件下载完成算作一次任务成功,下载过程中能返回所有已下载文件大小。
-
根据sessionId,能查询组任务下载状态。
-
能够设置组任务下载优先级,用于立即或延时安装插件。
-
如果文件已经下载,则不再下载。
qigsaw-android-sample
提供基于英语流利说FileDownloader实现的组任务下载器downloader,开发者可参考该示例完成自定义Downloader实现。
自定义Application,通过SplitConfiguration可对Qigsaw进行各项初始化配置等。具体操作可查看qigsaw-android-sample
->com.iqiyi.qigsaw.sample.QigsawApplication
。
public class QigsawApplication extends Application {
//qigsaw将只工作在主进程和名为qigsaw的进程
//null或"",代表主进程
private static final String[] workProcesses = {"", ":qigsaw"};
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(base);
SplitConfiguration configuration = SplitConfiguration.newBuilder()
.workProcesses(workProcesses)
.logger(new SampleLogger())
//当app工程AndroidManifest文件声明的包名与其build.gradle文件声明的applicationId不一致时,需要设置manifest生命的包名。
.manifestPackageName(base.getPackageName())
//插件加载状态回调通知。
.loadReporter(new SampleSplitLoadReporter(this))
//插件安装状态回调通知。
.installReporter(new SampleSplitInstallReporter(this))
//插件更新状态回调通知。
.updateReporter(new SampleSplitUpdateReporter(this))
//是否在Application的onCreate阶段加载所有已安装插件,可能会带来一定性能损耗。
.loadInstalledSplitsOnApplicationCreate(true)
//自定义用户确认对话框,当插件下载总大小超过蜂窝网络下允许下载的阈值
.obtainUserConfirmationDialogClass(SampleObtainUserConfirmationDialog.class)
.build();
Qigsaw.install(this,
new SampleDownloader(),
configuration);
}
@Override
public void onCreate() {
super.onCreate();
//将Application#onCreate事件通知给qigsaw。
Qigsaw.onApplicationCreated();
}
@Override
public Resources getResources() {
//将Application#onResource事件通知给qigsaw。
Qigsaw.onApplicationGetResources(super.getResources());
return super.getResources();
}
}
Android App Bundle 中有规定当使用蜂窝网络下载插件时,插件总大小超过 10M 需要用户确认是否继续下载。Qigsaw 同样采用该策略,不过相比 Android App Bundle 一刀切的方案,Qigsaw 提供更灵活接入方式。
- 开发者可以设置蜂窝网络下允许插件下载的总大小阈值。
- Qigsaw提供默认用户确认框,同时支持接入方自定义弹框样式。
默认弹框样式如下。
如需自定义弹框样式,只需继承ObtainUserConfirmationDialog实现弹框样式即可,您可以参考SampleObtainUserConfirmationDialog
public final class SampleObtainUserConfirmationDialog extends ObtainUserConfirmationDialog {
private boolean fromUserClick;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//调用checkInternParametersIllegal检查传入参数是否非法
if (checkInternParametersIllegal()) {
finish();
return;
}
//getModuleNames获取当前下载的所有插件
SplitLog.d(TAG, "Downloading splits %s need user to confirm." + getModuleNames().toString());
setContentView(R.layout.activity_sample_obtain_user_confirmation);
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
setFinishOnTouchOutside(false);
TextView descText = findViewById(R.id.sample_user_conformation_tv);
DecimalFormat df = new DecimalFormat("#.00");
//getRealTotalBytesNeedToDownload获取当前下载插件总大小
double convert = getRealTotalBytesNeedToDownload() / (1024f * 1024f);
descText.setText(String.format(getString(R.string.sample_prompt_desc), df.format(convert)));
findViewById(R.id.sample_user_confirm).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!fromUserClick) {
fromUserClick = true;
//用户确认时,必须调用onUserConfirm方法
onUserConfirm();
}
}
});
findViewById(R.id.sample_user_cancel).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!fromUserClick) {
fromUserClick = true;
//用户取消时,必须调用onUserCancel方法
onUserCancel();
}
}
});
}
}
完成qigsaw相关配置就可在Gradle侧边栏中(projectName
->Tasks
->qigsaw
)看到qigsaw提供的打包命令。
通过qigsawAssembleDebug
或qigsawAssembleRelease
命令打包会生成文件名前缀为qigsaw
的JSON文件。打包生成的Apk目录结构如下图。
如果插件内置,则插件将会以dynamicfeatureName.zip
文件命名形式内置在base Apk的assets目录下。
插件信息文件以qigsaw_appVersionName_splitInfoVersion.json
文件命名形式内置base Apk的assets目录下,该文件记录插件名、下载地址,插件大小等信息。
{
"qigsawId": "1.0.0_813d13d",
"appVersionName": "1.0.0",//app版本号
"splits": [
{
"splitName": "java", //插件名
"url": "assets://java.zip", //插件下载地址,内置插件以"assets://"开头。
"builtIn": true, //是否为内置插件
"size": 11897, //插件apk文件大小
"version": "1.1@1", //插件版本号
"md5": "8c9111d88509fd5767f8eb8b6520c790", //插件apk文件md5值
"workProcesses": [ //插件工作进程,读取的是插件AndroidManifest文件中"android:process"的指
":qigsaw",
""
],
"minSdkVersion": 14, //最低支持的sdk版本,可大于base app中声明的最低版本
"dexNumber": 2 //插件apk中dex的数目
}
{
"splitName": "native",
"url": "assets://native.zip",
"builtIn": true,
"size": 17808,
"version": "1.0@1",
"md5": "989f02a21e97e7881b26e21d4e384d48",
"minSdkVersion": 14,
"dexNumber": 2,
"libInfo": {
"abi": "armeabi-v7a",//插件so支持的abi
"libs": [
{
"name": "libhello-jni.so",//插件so文件名
"md5": "5597a187d58e395e746e65c8ff282446",//插件so文件md5值
"size": 13948//插件so文件大小
}
]
}
}
]
}
开发过程中,提供两种方式安装插件:
-
一种是,点击 Android Studio 的
Run
按钮,此方式是借助 Android App Bundle 开发,base Apk 和 split Apks 将会一起被安装至设备上(如果设备系统版本大于等于5.0)。 -
另一种是,通过
qigsawAssembleDebug
,将打包生成的app.apk
(插件内置在base Apk的assets目录下)通过abd install [apk-path]
安装至设备即可。
Qigsaw 完成"山寨" Play Core Library 公开接口实现,因此插件启动实现您可以阅读官方开发文档。
另外,您也可以参考QigsawInstaller实现,该文件实践了插件启动核心逻辑和注意事项。
//初始化SplitInstallManager
SplitInstallManager installManager = SplitInstallManagerFactory.create(this);
//创建一个新的安装请求
SplitInstallRequest request = SplitInstallRequest.newBuilder().addModule(name).build();
//installManager获取请求,请求将交由SplitInstallService进行处理,该过程可设置监听
installManager.startInstall(request);
监听设置及具体实现可参考源码中的qigsaw-android-sample
进行。
Qigsaw本身支持插件的热更新,用户无需重新安装base Apk就可以下载更新插件。Qigsaw不支持AndroidManifest修改,例如新增四大组件。
为你的代码创建插件更新所需的分支。
完成插件代码更新后,在dynamicfeature/build.gradle文件中修改插件版本号(如果不修改插件版本号插件将无法更新)。
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 14
targetSdkVersion 28
versionCode 1
//versionName "1.0.0"
versionName "1.0.1"
}
}
- 配置mapping文件。
- 配置old apk,存放路径与mapping文件目录一致。
- 修改插件信息版本号。
qigsawSplit {
/**
* 可选项,默认为null
* 如果需要更新,需要将上一版本的apk放在指定目录下
*/
oldApk = "./qigsaw/app.apk"
/**
* 设置插件信息版本号,默认为‘1.0.0’
* 每次通过qigsawAssembleDebug或者qigsawAssembleRelease命令编译时,会在app/build/outputs/apk/debug(or release)/目录下生产插件信息文件,文件名为"qigsaw_appVersionCode_splitInfoVersion.json"
* 插件更新时必须修改该版本号
*/
//splitInfoVersion ‘1.0.0’
splitInfoVersion ‘1.0.1’
/**
* 当插件需要更新时,需要应用当前版本渠道包对应的mapping文件。
*/
applyMapping projectDir.getPath() + '/qigsaw/mapping.txt'
}
- mapping文件应用规则
插件首次更新,应用app首次发布生成的mapping文件。
插件二次更新,应用插件第一次更新生成的mapping文件。以此类推。
- old apk应用规则
插件更新情况下,始终应用app首次发布生成的apk文件。
- 关于不同渠道
某些应用不同渠道其mapping文件不一样,因此需要针对这些渠道分别打包发布。
上传新生成的插件信息JSON文件至您的发布后台。
完成插件更新打包配置后,开发者需在app运行时适时调用Qigsaw#updateSplits方法,传入新的插件信息版本号及新版本json文件的路径,updateSplits将更新插件版本号,并在插件下一次启动时判断是否有更新,如有则进行下载安装:
public static boolean updateSplits(Context context,
@NonNull String newSplitInfoVersion,
@NonNull String newSplitInfoPath)
国内很多App都已接入Tinker,并在热修方面发挥着很大的作用。兼容性方面,Qigsaw与Tinker能够达到完美结合,但是需要Tinker的版本支持AAPT2
。
此外通过Tinker也能够更新Qigsaw的插件,在爱奇艺重磅开源基于Android-App-Bundle动态化方案Qigsaw文章中有相关介绍。
Qigsaw-Gradle-Plugin
会将与Qigsaw相关Gradle项目配置信息注入至App工程的BuildConfig
中,如下。
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true");
public static final String APPLICATION_ID = "com.iqiyi.qigsaw.sample";
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "";
public static final int VERSION_CODE = 271;
public static final String VERSION_NAME = "1.0.0";
/**
* split-info版本号。形式为appVersion_splitInfoVerison。
*/
public static final String DEFAULT_SPLIT_INFO_VERSION = "1.0.0_1.0.0";
/**
* qigsawId,用于匹配split apks和base apk关系。
* 其规则是app版本号加上当前git的commit id。
*/
public static final String QIGSAW_ID = "1.0.0_ddddf54";
/**
* 记录所有split名字。
*/
public static final String[] DYNAMIC_FEATURES = {"java", "assets", "native"};
}
Tinker Patch打包过程中BuildConfig
内容会被改变,因此您需要忽略BuildConfig
内容改变。在tinker.gradle
文件增加如下配置。
dex {
......
/**
* necessary,default '[]'
* Warning, it is very very important, loader classes can't change with patch.
* thus, they will be removed from patch dexes.
* you must put the following class into main dex.
* Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*/
//指app工程生成的BuildConfig.java文件。
loader = ["${yourPackageNameInAppManifestFile}.BuildConfig"
]
}
在loader
配置${yourPackageNameInAppManifestFile}.BuildConfig
类,yourPackageNameInAppManifestFile
指的是您app工程manifest文件所声名的包名。
-
Q:“必须实现SplitApkUploader才能将split APK上传到您的CND服务器。”描述中的CND服务器,是指我们自己搭建的服务器还是各大应用市场的?
**A:**接入方需要将插件Apk上传到自己的CDN服务器进行管理。
-
Q:qigsaw到底是组件化框架还是插件化框架?
**A:**您可以称它为动态组件化技术。
-
Q:qigsaw对android5.0以下不支持的split apk是如何做到兼容的?
**A:**qigsaw只是利用 AAB 开发套件,并没有采用和它一样的插件加载方式。采取和一般插件化框架类似的动态加载技术。
-
Q:提交到国内应用市场的是aab文件还是apk文件?
**A:**提交到应用市场的是apk文件。
-
Q:上传插件必须实现吗,以及必须用groovy写吗?
**A:**开发者必须实现上传插件才能实现动态下发,groovy与Java是兼容的,可以使用Java语法代替。
-
Q:base Apk中通过
getResources().getIdentifier
访问插件资源为什么访问不了?A:
getResources().getIdentifier("hello_world","string", getPackageName() + "." + "${插件名}");
第三参数参入的资源文件包名需要加上插件名。
-
Q:为什么打debug包不会报错打release包会出现报错?
**A:**插件必须配置签名,打debug包如果未配置签名系统会采用默认的签名,但打release包必须使用开发者自己的签名。
-
Q:打包过程中插件中包含多CPU架构平台的so为什么会打包失败?
**A:**该错误是没有在该插件配置abi过滤导致的,Qigsaw 目前只支持单CPU架构平台的so。
android{ defaultConfig{ ndk{ abiFilter 'armeabi-v7a' } } }
- 从
qigsaw-android-sample
中可了解更多信息。 - 查看Wiki或FAQ以获取帮助。
- 邮箱联系,[email protected]。
- 加入QQ群了解最新信息。