基于Android App Bundle动态化方案Qigsaw重磅开源啦!

很久没有使用简书平台了,这一次回归带来自己开源的全新动态化框架Qigsaw。希望朋友们多多Star。开源地址https://github.com/iqiyi/Qigsaw网页上有QQ群二维码,欢迎加入交流。

在2019GMTC全球大前端大会上,我们完成《基于Android App Bundle动态化方案探索》议题宣讲,并于2019年6月26号正式开源Qigsaw

Qigsaw是爱奇艺自主研发的动态化框架,其核心优势如下:

  1. 利用Android App Bundle开发套件,极速开发体验。
  2. 支持Android App Bundle所有功能特性,"山寨"Play Core Library公开接口实现,开发者阅读官方文档即可愉快开发。
  3. 任何进程均可动态加载插件,支持Android四大组件动态加载。
  4. 如果您的应用有出海需求,可无缝切换至Android App Bundle方案。
  5. 仅一处Hook,少量私有API访问,保证框架稳定性。

Android动态化方案,在国内已蓬勃发展数年之久,其核心目的是减少应用包体积,提升应用安装率。Google在减少应用包体积上的探索也从未停息,下面我们一起来看看Google在这方面的努力。

Google减少应用包体积方案演进

回首Android第一个10年,其应用发布方式如下。

tradition_delivery

从应用开发到上传应用商店,最后再到用户下载环节,参与产物都是APK。

tradition_delivery_disadvantage

您的应用将包含所有CPU架构so文件、所有屏幕分辨率资源文件以及所有语言资源文件,那么存在如下两个问题。

  1. APK文件过大导致用户下载时长增加。

  2. 大量不会被使用的代码和资源侵占用户磁盘空间。

在国内,开发者一般都只会放一种CPU架构的so文件和一种屏幕分辨率资源文件,以此来减少包体积,但这种方式一定程度上会影响用户体验。

根据Google官方数据统计,从2012年至今,应用包体积平均增长了5倍左右,爱奇艺也不例外。

iqiyi_apk_size

经过七年发展,爱奇艺越来越"膨胀"。

Google意识到包体积问题的严峻性,于Android 5.0推出Multiple APK,旨在减少安装包体积。

Multiple APK

Multiple APK是Google Play提供一个功能,它允许您的应用针对不同的设备配置发布不同的APK。通过一张图来了解下其工作流程。

multiple_apk_sample

图中左边手机是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
    }
  }
}

通过densityabi两个配置维度即可生成一系列APKs。

multiple_apk_flow

上图中生成的产物,通过文件名我们可以很清楚知道该APK作用于何种配置的设备。

Android设备的多样性,导致Multiple APK并未朝着Google期待的方向发展。因为您有可能为每个版本构建数百个APKs,大大降低迭代效率。国外开发者对此也并不感冒,这也成为Google的一块心病。

Android App Bundle

Android App Bundle是一种全新的应用上传格式(.aab),它包含所有编译代码和资源。当您上传aab文件至Google Play后,Google Play将aab文件拆分成一系列APKs并签名。

aab_delivery

此外,您也可以在应用项目中添加dynamic feature模块,这些模块并不需要在应用首次安装时一起被下载安装。您可以通过使用Play Core Libray在应用运行过程中动态安装dynamic feature。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。

  1. adb install-multiple [base-apk, split1-apk]
  2. PackageInstaller.

vivo手机不支持adb install-multipl命令。

这里我们重点介绍第二种安装方式,Android 5.0提供PackageInstaller用于安装Base APK和Split APKs。

当第三方应用通过PackageInstaller在应用运行期安装Split APKs时,系统会启动安装器界面供用户选择是否安装此次更新。

install_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;
       }
    }
    ...
    ...
}

SessionParams是PackageInstaller内部类,setDontKillApp可决定当APK安装完成后是否杀死应用进程。setDontKillApp属于系统Api,因此第三方应用无法调用。

Split APKs加载

应用进程所使用到的ClassLoader和Resources均在LoadedAPK中创建。

loadedapk

通过Android 9.0 LoadedAPK源码片段,我们一起了解下Split APKs加载过程。

ClassLoader创建。

create_classloader

通过createOrUpdateClassLoaderLocked方法名,可以知道该方法是用于创建和更新ClassLoader。该方法有两个核心步骤。

  1. 如果mClassLoader为空,则创建PathClassLoader实例。

  2. 如果addedPaths不为空,则更新PathClassLoader实例。

该方法指明,应用进程是可以动态加载Split APKs代码。

Resources创建。

create_resources

通过getResources方法代码片段,可知Split APKs的资源路径作为mResources创建参数。

关于更多Split APKs加载原理细节,请阅读相关Android源码。

Play Core Library

文章开始介绍Qigsaw核心优势有提到,Qigsaw"山寨"Play Core Library公开接口实现,开发者阅读其官方文档即可开发。因此,在此主要介绍下Play Core Library工作流程。

play_core_library_flow

当爱奇艺App在运行过程中,用户需要使用游戏插件,会经历以下过程。

  1. 爱奇艺App通过Play Core Library发起游戏APK安装请求。

  2. 当Google Play收到请求后,首先请求游戏APK相关数据信息,请求成功后开始下载并安装游戏APK。

  3. 在请求、下载以及安装整个过程中,Google Play会将整个过程所有状态返回给爱奇艺App,包括请求结果、下载进度、安装结果等。

  4. 当安装完成以后,爱奇艺App就可以使用游戏APK。

在Android 7.0版本之前,当Split APK安装完成之后,应用无法立即使用Split APK。因此Play Core Library提供SplitCompat模式让App可立即使用Split APK。

爱奇艺动态化框架Qigsaw

在2018年上半年,我们就进行动态组件化方案的调研。起初方案是基于Instant App方案实现,当整体功能基本实现后,Google于2018年Google IO大会上推出Android App Bundle。在调研Android App Bundle之后,我们发现Android App Bundle完全符合最初的需求。

依据我们最初设计初衷和Android App Bundle特点,总结出Qigsaw应满足以下核心特点。

  1. 利用Android App Bundle开发套件,体验原生极速开发体验。
  2. 少量私有Api访问,保证框架稳定性。
  3. 如果您的应用有出海需求,可无缝切换至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。

qigsaw_debug_flow.png

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_release_flow

Qigsaw打包插件支持内置dynamic feature,所有内置dynamic feature都会被拷贝至base apk的assets目录。对于非内置dynamic feature,Qigsaw打包插件会将其上传至CDN服务器,解决业务方后顾之忧。

Qigsaw原理

Qigsaw借助Android App Bundle开发套件完成dynamic feature的打包,大大降低Qigsaw开发维护成本。因此Qigsaw关心的重点落在如果安装加载dynamic feature生成apk上。

第三方应用利用PackageInstaller安装split APKs体验极其不友好,且某些国产手机对split APKs功能支持不完善,所以我们最终还是按照一般插件化方式安装加载split APKs。

apk_fomat

依据上图,如果需要动态加载split APKs,需要解决代码、资源以及四大组件的加载。

Split APKs代码加载

针对splits代码加载,Qigsaw采用单类加载器方式,即base APK和split APKs采用同一ClassLoader加载。

split_load_code

在DexPathList中,为每个split创建对应的ElementNativeLibraryElement实例即可。关于单类加载器更多细节,本文不再赘述,相关原理已非常成熟。

Split APKs资源加载。

Splits资源加载相较于代码加载会复杂,因为不同系统版本或不同手机厂商都会存在一些兼容性问题。

split_load_resources

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资源如何加载呢?我们来看一段代码。

split_load_resources_sample

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类似。首先,通过一张图来了解。

qigsaw_core_library_flow

在爱奇艺App运行过程中,当X进程发起安装游戏APK请求时,会经历以下步骤。

  1. X进程通过Qigsaw Core Library发起游戏APK安装请求。

  2. 当主进程收到请求后,开始下载并安装游戏APK。

  3. 在下载、安装整个过程中,Qigsaw Core Library会将整个过程所有状态返回给爱奇艺App,包括下载进度、安装结果等。

  4. 当安装完成以后,爱奇艺App就可以使用游戏APK。

Qigsaw下载、安装split APKs均在主进程处理,split APKs的加载则发生在X进程。Qigsaw安装、加载split APKs原则是,哪个进程发起split APKs安装请求,就在哪个进程加载split APKs。

Qigsaw拓展功能

在实际开发过程中,Android App Bundle所支持的功能特性并不满足我们需求。因此,Qigsaw在Android App Bundle基础上拓展了几个功能。

  1. Split APKs的Application初始化。
  2. Split APKs的Content Provider动态加载。
  3. 多进程支持。
  4. 通过Tinker patch完成split APKs热更新。

在此,我们首先介绍Qigsaw多进程功能。以下图场景为例。

multiple_processes_support_sample

依据Qigsaw安装、加载split APKs原则,当游戏APK安装完成后,就会在主进程完成加载。在游戏APK中有两个Activity,他们所处进程不同。当启动GameActivity01时,页面正常启动。但当启动GameActivity02,您的App会出现崩溃。原因是GameActivity02运行在:game进程,游戏APK仅在主进程加载,并未在:game进程加载,因此系统会抛出ClassNotFoundException异常。

为解决这类问题,Qigsaw提供了如下解决方案。

  1. 在进程启动之初即Applicatin#attachBaseContext调用时,加载所有已安装splits。
  2. Hook PathClassLoader。

第一种方案解决的场景是:game进程首次启动,即启动GameActivity02之前:game进程从未启动过。

第二种方案解决的场景是:game进程已经启动并正在运行。

Hook PathClassLoader具体做了如下事情。

  1. 当出现ClassNotFoundException时,判断该类是否为splits四大组件。
  2. 当异常类为splits四大组件时,加载所有已安装未加载split APKs。
  3. 如加载完所有已安装未加载split APKs后依然出现ClassNotFoundException异常,则返回空四大组件类,防止进程崩溃。

如果split APKs某Activity的exported熟悉为true,那么该Activity可能会在split未安装的情况下被外界调起。当出现这种情况时,Qigsaw返回空Activity类防止进程崩溃。

国内很多App都接入Tinker用于修复线上bug,爱奇艺同样也接入。Qigsaw本身提供热更新能力,但在实际开发过程中发现,Qigsaw能借助Tinker Patch热更新split APKs,提升开发效率。

split_info_json_in_base

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的未来希望有你参与

Qigsaw在 2019 年 1 月正式在爱奇艺 App 上线,半年间经过数亿用户验证,由Qigsaw引起的崩溃率占总崩溃率已不足千分之一。在爱奇艺 App 中,小程序以及小游戏框架均由Qigsaw动态加载,目前已推广至全公司五个业务线团队使用。2019 年 6 月 26 日,Qigsaw 正式对外开源,我们希望有志之士能为Qigsaw贡献一己之力,共同完善Qigsaw生态,让国内更多开发者体验到Android App Bundle的快感。

最后,如果您认可Qigsaw,欢迎大家献上自己的小星星,star关注我们吧!

有任何问题,请邮箱联系kissonchen@qiyi.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容