爱奇艺动态化框架 Qigsaw 开源!带来极速原生开发体验和更低 crash 率

阅读数:4871 2019 年 7 月 4 日 17:49

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

在日前的 GMTC 全球大前端技术大会上,爱奇艺资深工程师陈家伟发表了《基于 Android App Bundle 动态化方案探索》的演讲,本文整理内容如下。

在 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 年,其应用发布方式如下。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

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

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

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

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

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

Multiple APK

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

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

Android App Bundle

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

此外,您也可以在应用项目中添加 dynamic feature 模块,这些模块并不需要在应用首次安装时一起被下载安装。您可以通过使用 Play Core Libray 在应用运行过程中动态安装 dynamic feature。dynamic feature 类似国内插件化提供的能力,但 dynamic feature 功能更强大。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

通过上图,可以看到 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 时,系统会启动安装器界面供用户选择是否安装此次更新。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

在用户选择安装后,应用将会被系统“杀死”。当应用再次启动之后,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中创建。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

ClassLoader 创建。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

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

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

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

Resources 创建。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

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

Play Core Library

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

当爱奇艺 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开源!带来极速原生开发体验和更低crash率

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开源!带来极速原生开发体验和更低crash率

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。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

Split APKs 代码加载

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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

Split APKs 资源加载。

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

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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开源!带来极速原生开发体验和更低crash率

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开源!带来极速原生开发体验和更低crash率

在爱奇艺 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 多进程功能。以下图场景为例。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

依据 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,提升开发效率。

爱奇艺动态化框架Qigsaw开源!带来极速原生开发体验和更低crash率

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 正式对外开源,地址是: https://github.com/iqiyi/Qigsaw

我们希望有志之士能为 Qigsaw 贡献一己之力,共同完善 Qigsaw 生态,让国内更多开发者体验到 Android App Bundle 的快感。

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

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

嘉宾介绍

陈家伟,2014 年南昌大学毕业,硕士学历,五年移动端开发经验。曾就职于步步高、平安,目前就职于爱奇艺移动架构组,主攻 Android 动态化相关项目。包括 React Native Android 端架构设计、Android 热修复、Android 组件化等,近一年来作为 Qigsaw 项目负责人开展新一代 Android 组件化工作。Qigsaw 基于 Android App Bundle 实现,具有维护简单、开发方式灵活、无缝切换至国际版等优点。

评论

发布
用户头像
从携程 360 腾讯 阿里开源的一些插件化 热加载等等框架,工程开发过程中还是有点复杂度的,小项目用不上,大项目直接拆分多个产品App,加上展示类功能H5化,基本能减少很大部分的APK大小。
感觉国内开源都是把快淘汰的技术放出来,纯技术讨论用。哈哈哈~
2019 年 07 月 08 日 17:04
回复
没有更多了