副标题#e#
近日爱奇艺宣布开源基于Android App Bundle的动态化框架 Qigsaw。以下是其官方在演讲中对项目的介绍。
我们于2019年6月26号正式开源Qigsaw。
Qigsaw是爱奇艺自主研发的动态化框架,其核心优势如下:
- 利用Android App Bundle开发套件,极速开发体验。
- 支持Android App Bundle所有功能特性,"山寨"Play Core Library公开接口实现,开发者阅读官方文档即可愉快开发。
- 任何进程均可动态加载插件,支持Android四大组件动态加载。
- 如果您的应用有出海需求,可无缝切换至Android App Bundle方案。
- 仅一处Hook,少量私有API访问,保证框架稳定性。
Android动态化方案,在国内已蓬勃发展数年之久,其核心目的是减少应用包体积,提升应用安装率。Google在减少应用包体积上的探索也从未停息,下面我们一起来看看Google在这方面的努力。
Google减少应用包体积方案演进
回首Android第一个10年,其应用发布方式如下。
从应用开发到上传应用商店,最后再到用户下载环节,参与产物都是APK。
您的应用将包含所有CPU架构so文件、所有屏幕分辨率资源文件以及所有语言资源文件,那么存在如下两个问题。
-
APK文件过大导致用户下载时长增加。
-
大量不会被使用的代码和资源侵占用户磁盘空间。
在国内,开发者一般都只会放一种CPU架构的so文件和一种屏幕分辨率资源文件,以此来减少包体积,但这种方式一定程度上会影响用户体验。
根据Google官方数据统计,从2012年至今,应用包体积平均增长了5倍左右,爱奇艺也不例外。
经过七年发展,爱奇艺越来越"膨胀"。
Google意识到包体积问题的严峻性,于Android 5.0推出Multiple APK,旨在减少安装包体积。
Multiple APK
Multiple APK是Google Play提供一个功能,它允许您的应用针对不同的设备配置发布不同的APK。通过一张图来了解下其工作流程。
图中左边手机是nexus 5,右边手机是nexus 6p,它们的CPU架构、屏幕分辨率均不同,因此Google Play会根据当前设备配置下载对应APK。
Google提供打包配置选项,让开发者根据不同设备配置生成不同APK文件。
- android {
- ...
- splits {
- // Configures multiple APKs based on screen density.
- density {
- ...
- // Specifies a list of screen densities Gradle should not create multiple APKs for.
- exclude "ldpi", "xxhdpi", "xxxhdpi"
- }
- // Configures multiple APKs based on ABI.
- abi {
- ...
- // Specifies a list of ABIs that Gradle should create APKs for.
- include "x86", “x86_64"
- // Specifies that we do not want to also generate a universal APK that includes all ABIs.
- universalApk false
- }
- }
- }
通过density
和abi
两个配置维度即可生成一系列APKs。
上图中生成的产物,通过文件名我们可以很清楚知道该APK作用于何种配置的设备。
Android设备的多样性,导致Multiple APK并未朝着Google期待的方向发展。因为您有可能为每个版本构建数百个APKs,大大降低迭代效率。国外开发者对此也并不感冒,这也成为Google的一块心病。
Android App Bundle
Android App Bundle是一种全新的应用上传格式(.aab),它包含所有编译代码和资源。当您上传aab文件至Google Play后,Google Play将aab文件拆分成一系列APKs并签名。
#p#副标题#e#
此外,您也可以在应用项目中添加dynamic feature模块,这些模块并不需要在应用首次安装时一起被下载安装。您可以通过使用Play Core Libray在应用运行过程中动态安装dynamic feature。dynamic feature类似国内插件化提供的能力,但dynamic feature功能更强大。
通过上图,可以看到dynamic feature可以基于设备配置选取对应的Configuration Split APKs,如此可以进一步减小dynamic feature安装包体积。
更多关于Android App Bundle细节,请阅读官方文档,本文不再赘述。
Android App Bundle之所以能够支持应用运行期间安装dynamic feature,得益于Android 5.0推出的Split APKs功能。
Split APKs
Split APKs是Android 5.0引入的一种全新应用安装机制,其目的是为解决APK体积日益增大问题。Split APK可以将一个完整庞大的APK按照CPU架构、屏幕密度等维度拆分成多个独立APKs。当应用APK下载更新时,依据当前设备配置选取对应配置APKs安装即可。
Android 5.0之前,一个APK代表一个应用。在Split APKs问世之后,一个应用可能对应多个APKs。所有Split APKs拥有相同包名和签名。
Android提供两种方式安装Split APKs。
- adb install-multiple [base-apk, split1-apk]
- PackageInstaller.
vivo手机不支持adb install-multipl命令。
这里我们重点介绍第二种安装方式,Android 5.0提供PackageInstaller用于安装Base APK和Split APKs。
当第三方应用通过PackageInstaller在应用运行期安装Split APKs时,系统会启动安装器界面供用户选择是否安装此次更新。
在用户选择安装
后,应用将会被系统“杀死”。当应用再次启动之后,Split APKs就会生效。
在我们实际测试过程中,某些国产手机对PackageInstaller有改动,导致无法正常安装Split APKs。
系统应用可以静默安装Split APKs,且当Split APKs安装完成后,可以决定是否“杀死“应用进程。
- public static class SessionParams implements Parcelable {
- ...
- /** {@hide} */
- @SystemApi
- public void setDontKillApp(boolean dontKillApp) {
- if (dontKillApp) {
- installFlags |= PackageManager.INSTALL_DONT_KILL_APP;
- } else {
- installFlags &= ~PackageManager.INSTALL_DONT_KILL_APP;
- }
- }
- ...
- ...
- }
#p#分页标题#e#
SessionParams是PackageInstaller内部类,setDontKillApp
可决定当APK安装完成后是否杀死应用进程。setDontKillApp
属于系统Api,因此第三方应用无法调用。
Split APKs加载
#p#副标题#e#
应用进程所使用到的ClassLoader和Resources均在Loa
dedAPK
中创建。
通过Android 9.0 LoadedAPK
源码片段,我们一起了解下Split APKs加载过程。
ClassLoader创建。
通过createOrUpdateClassLoaderLocked
方法名,可以知道该方法是用于创建和更新ClassLoader。该方法有两个核心步骤。
-
如果
mClassLoader
为空,则创建PathClassLoader实例。 -
如果
addedPaths
不为空,则更新PathClassLoader实例。
该方法指明,应用进程是可以动态加载Split APKs代码。
Resources创建。
通过getResources
方法代码片段,可知Split APKs的资源路径作为mResources
创建参数。
关于更多Split APKs加载原理细节,请阅读相关Android源码。
Play Core Library
文章开始介绍Qigsaw核心优势有提到,Qigsaw"山寨"Play Core Library公开接口实现,开发者阅读其官方文档即可开发。因此,在此主要介绍下Play Core Library工作流程。
当爱奇艺App在运行过程中,用户需要使用游戏插件,会经历以下过程。
-
爱奇艺App通过Play Core Library发起游戏APK安装请求。
-
当Google Play收到请求后,首先请求游戏APK相关数据信息,请求成功后开始下载并安装游戏APK。
-
在请求、下载以及安装整个过程中,Google Play会将整个过程所有状态返回给爱奇艺App,包括请求结果、下载进度、安装结果等。
-
当安装完成以后,爱奇艺App就可以使用游戏APK。
在Android 7.0版本之前,当Split APK安装完成之后,应用无法立即使用Split APK。因此Play Core Library提供SplitCompat模式让App可立即使用Split APK。
爱奇艺动态化框架Qigsaw#p#副标题#e#
在2018年上半年,我们就进行动态组件化方案的调研。起初方案是基于Instant App方案实现,当整体功能基本实现后,Google于2018年Google IO大会上推出Android App Bundle。在调研Android App Bundle之后,我们发现Android App Bundle完全符合最初的需求。
依据我们最初设计初衷和Android App Bundle特点,总结出Qigsaw应满足以下核心特点。
- 利用Android App Bundle开发套件,体验原生极速开发体验。
- 少量私有Api访问,保证框架稳定性。
- 如果您的应用有出海需求,可无缝切换至Android App Bundle方案。
关于私有Api访问应该是大家比较关心的,最近一段时间某大厂开源了号称零反射插件化框架,但是通过阅读其源码,我们发现它还是做了PathClassLoader的parent ClassLoader反射替换。另外它也调用了Resources构造方法创建Resources实例,虽然这样做并没有任何私有Api访问,但是通过查看Resources构造方法源码,我们可知该方法属于过时方法,且注释写明第三方应用不应该创建Resources实例。
- /**
- * Create a new Resources object on top of an existing set of assets in an
- * AssetManager.
- *
- * @deprecated Resources should not be constructed by apps.
- * See {@link android.content.Context#createConfigurationContext(Configuration)}.
- *
- * @param assets Previously created AssetManager.
- * @param metrics Current display metrics to consider when
- * selecting/computing resource values.
- * @param config Desired device configuration to consider when
- * selecting/computing resource values (optional).
- */
- @Deprecated
- public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
- this(null);
- mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
- }
所以插件化框架不应该仅仅以是否零反射为目标,我们应该从开发流程及产品形态选取合适方案,助力开发效率。
Qigsaw开发体验
在开发阶段,开发者使用Android App Bundle原生开发套件即可开发调试Split APKs。
Android App Bundle为dynamic feature提供全新插件com.android.dynamic-feature
,它的编译产物是.apk
文件。当您的项目编译完成后,Android Studio通过命令adb install-multiple
命令将base apk和split apks安装至您的手机。如果您的开发手机系统版本低于5.0,则会依据当前手机设备组装成一个完整apk文件安装至该手机。
vivo手机不支持split APKs功能,因此在开发过程中请选取其他手机。或者使用Qigsaw打包插件提供的qigsawAssemble${variantName}命令
在发布阶段,Qigsaw提供打包插件让开发者享受一条龙服务,开发者不必关心dynamic feature的上传分发。
Qigsaw打包插件支持内置dynamic feature,所有内置dynamic feature都会被拷贝至base apk的assets目录。对于非内置dynamic feature,Qigsaw打包插件会将其上传至CDN服务器,解决业务方后顾之忧。
Qigsaw原理
Qigsaw借助Android App Bundle开发套件完成dynamic feature的打包,大大降低Qigsaw开发维护成本。因此Qigsaw关心的重点落在如果安装加载dynamic feature生成apk上。
#p#副标题#e##p#分页标题#e#
第三方应用利用PackageInstaller安装split APKs体验极其不友好,且某些国产手机对split APKs功能支持不完善,所以我们最终还是按照一般插件化方式安装加载split APKs。
依据上图,如果需要动态加载split APKs,需要解决代码、资源以及四大组件的加载。
Split APKs代码加载
#p#副标题#e#
针对splits代码加载,Qigsaw采用单类加载器方式,即base APK和split APKs采用同一ClassLoader加载。
在DexPathList中,为每个split创建对应的Element
和NativeLibraryElement
实例即可。关于单类加载器更多细节,本文不再赘述,相关原理已非常成熟。
Split APKs资源加载。
Splits资源加载相较于代码加载会复杂,因为不同系统版本或不同手机厂商都会存在一些兼容性问题。
Android Gradle Plugin在资源打包时,会对res
目录下资源文件分配一个唯一Id。
Id前两位PP
为Package Id,代表应用类型。是系统应用、第三方应用、Instant App或Dynamic Feature等。
Id中间两位TT
为Type,代表资源类型。是drawable、layout或string等。
Id后四位EE
为Entry,代表该资源顺序。
所有第三方应用base APK资源Package Id均为7F,Android App Bundle对splits资源打包时会基于7F依次递减分配Package Id。因此,即使我们将split APKs资源添加到当前应用Resources实例中,也不会出现资源冲突问题,splits访问base资源也更加方便。
Instant Apps资源打包是基于7F依次递增。
通过Android App Bundle解决splits资源打包问题,那么splits资源如何加载呢?我们来看一段代码。
Qigsaw提供loadResources
方法加载split APKs资源。为避免开发者写大量模板代码,Qigsaw打包插件采用字节码操作方式自动写入该方法。
Split APKs四大组件加载
Android App Bundle在Manifest文件合并过程中,会将split APKs manifest文件内容合并至base APK中。因此,所有split APKs四大组件信息都是已经声明在base APK中。
Android App Bundle这种处理方式不支持Manifest更新,例如新增四大组件,所以Qigsaw也不支持新增四大组件。在正常开发迭代过程中,动态新增splits四大组件需求极少,所以Qigsaw与Android App Bundle特性保持一致。
Split APKs安装过程
前文我们介绍了Play Core Library是如何安装、加载split APKs,Qigsaw安装、加载split APKs与Play Core Library类似。首先,通过一张图来了解。
在爱奇艺App运行过程中,当X
进程发起安装游戏APK请求时,会经历以下步骤。
-
X
进程通过Qigsaw Core Library发起游戏APK安装请求。 -
当主进程收到请求后,开始下载并安装游戏APK。
-
在下载、安装整个过程中,Qigsaw Core Library会将整个过程所有状态返回给爱奇艺App,包括下载进度、安装结果等。
-
当安装完成以后,爱奇艺App就可以使用游戏APK。
Qigsaw下载、安装split APKs均在主进程处理,split APKs的加载则发生在X
进程。Qigsaw安装、加载split APKs原则是,哪个进程发起split APKs安装请求,就在哪个进程加载split APKs。
Qigsaw拓展功能
在实际开发过程中,Android App Bundle所支持的功能特性并不满足我们需求。因此,Qigsaw在Android App Bundle基础上拓展了几个功能。
- Split APKs的Application初始化。
- Split APKs的Content Provider动态加载。
- 多进程支持。
- 通过Tinker patch完成split APKs热更新。
在此,我们首先介绍Qigsaw多进程功能。以下图场景为例。
依据Qigsaw安装、加载split APKs原则,当游戏APK安装完成后,就会在主进程完成加载。在游戏APK中有两个Activity,他们所处进程不同。当启动GameActivity01
时,页面正常启动。但当启动GameActivity02
,您的App会出现崩溃。原因是GameActivity02
运行在:game
进程,游戏APK仅在主进程加载,并未在:game
进程加载,因此系统会抛出ClassNotFoundException异常。
为解决这类问题,Qigsaw提供了如下解决方案。
- 在进程启动之初即
Applicatin#attachBaseContext
调用时,加载所有已安装splits。 - Hook PathClassLoader。
第一种方案解决的场景是:game
进程首次启动,即启动GameActivity02
之前:game
进程从未启动过。
第二种方案解决的场景是:game
进程已经启动并正在运行。
Hook PathClassLoader具体做了如下事情。
- 当出现ClassNotFoundException时,判断该类是否为splits四大组件。
- 当异常类为splits四大组件时,加载所有已安装未加载split APKs。
- 如加载完所有已安装未加载split APKs后依然出现ClassNotFoundException异常,则返回空四大组件类,防止进程崩溃。
如果split APKs某Activity的
exported
熟悉为true,那么该Activity可能会在split未安装的情况下被外界调起。当出现这种情况时,Qigsaw返回空Activity类防止进程崩溃。
#p#副标题#e#
国内很多App都接入Tinker用于修复线上bug,爱奇艺同样也接入。Qigsaw本身提供热更新能力,但在实际开发过程中发现,Qigsaw能借助Tinker Patch热更新split APKs,提升开发效率。
Qigsaw在打包过程中会生成关于包含split信息的.json
文件,该文件存储在base APK的assets目录下。其命名规则为App版本号_Split信息版本号.json
。
json文件记录的内容如下。
- {
- "qigsawId": "1.0.0_ddddf54",
- "appVersionName": "1.0.0",
- "splits": [
- {
- "splitName": "java",
- "url": "assets://java.zip",
- "builtIn": true,
- "size": 13915,
- "version": "1.1@1",
- "md5": "9ea0f98381dea0d16a313ea9c09cc4aa",
- "workProcesses": [
- ":qigsaw",
- ""
- ],
- "minSdkVersion": 14,
- "dexNumber": 4
- },
- ...
- ...
- }
该文件记录着splits版本号以及下载地址,如果Tinker开启资源修复,我们就可以通过tinker patch更新该json文件,以此达到热更新splits目的。
Qigsaw的未来希望有你参与#p#副标题#e##p#分页标题#e#
Qigsaw 在 2019 年 1 月正式在爱奇艺 App 上线,半年间经过数亿用户验证,由 Qigsaw 引起的崩溃率占总崩溃率已不足千分之一。在爱奇艺 App 中,小程序以及小游戏框架均由 Qigsaw 动态加载,目前已推广至全公司五个业务线团队使用。2019 年 6 月 26 日,Qigsaw 正式对外开源,我们希望有志之士能为 Qigsaw 贡献一己之力,共同完善 Qigsaw 生态,让国内更多开发者体验到 Android App Bundle 的快感。
最后,如果您认可Qigsaw,欢迎大家献上自己的小星星,star关注我们吧!