您当前的位置: 首页 > 

暂无认证

  • 0浏览

    0关注

    92582博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

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

发布时间:2019-07-08 08:50:00 ,浏览量:0


			

640?wx_fmt=jpeg

作者 | 陈家伟

编辑 | Yonie

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

在 2019GMTC 全球大前端大会上,我们完成《基于 Android App Bundle 动态化方案探索》议题宣讲,并于 2019 年 6 月 26 号正式开源 Qigsaw:https://github.com/iqiyi/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 在这方面的努力。

Multiple APK

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

640?wx_fmt=png

图中左边手机是 nexus 5,右边手机是 nexus 6p,它们的 CPU 架构、屏幕分辨率均不同,因此 Google Play 会根据当前设备配置下载对应 APK。

Google 提供打包配置选项,让开发者根据不同设备配置生成不同 APK 文件。

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

640?wx_fmt=png

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

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

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

640?wx_fmt=png

在用户选择安装后,应用将会被系统“杀死”。当应用再次启动之后,Split APKs 就会生效。

在我们实际测试过程中,某些国产手机对 PackageInstaller 有改动,导致无法正常安装 Split APKs。

系统应用可以静默安装 Split APKs,且当 Split APKs 安装完成后,可以决定是否“杀死“应用进程。

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

Play Core Library

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

640?wx_fmt=png

当爱奇艺 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 开发体验

在开发阶段,开发者使用 Android App Bundle 原生开发套件即可开发调试 Split APKs。

640?wx_fmt=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 的上传分发。

640?wx_fmt=png

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

Split APKs 代码加载

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

640?wx_fmt=png

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

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 特性保持一致。

Qigsaw 拓展功能

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

  1. Split APKs 的 Application 初始化。

  2. Split APKs 的 Content Provider 动态加载。

  3. 多进程支持。

  4. 通过 Tinker patch 完成 split APKs 热更新。

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

640?wx_fmt=png

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

640?wx_fmt=png

Qigsaw 在打包过程中会生成关于包含 split 信息的.json文件,该文件存储在 base APK 的 assets 目录下。其命名规则为App 版本号 _Split 信息版本号.json。

json 文件记录的内容如下。

该文件记录着 splits 版本号以及下载地址,如果 Tinker 开启资源修复,我们就可以通过 tinker patch 更新该 json 文件,以此达到热更新 splits 目的。

嘉宾介绍

陈家伟,就职于爱奇艺,五年移动端开发经验,主攻 Android 动态化相关项目。近一年来作为 Qigsaw 项目负责人开展新一代 Android 组件化工作。

{ "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 { // 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 } } } 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; } } ... ...} ... /** {@hide} */ @SystemApi public void setDontKillApp(boolean dontKillApp) { if (dontKillApp) { installFlags |= PackageManager.INSTALL_DONT_KILL_APP; } else { installFlags &= ~PackageManager.INSTALL_DONT_KILL_APP; } } ... ... } /** * 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()); } * 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()); } { "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 }, ... ...} "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 }, ... ... }

PS:鱼哥星球圈2期正在火热开启逆袭计划中,详情了解鱼哥星球圈的介绍,可以看这两篇文章:

今天,我需要你的支持!

曾经甩我30条街的技术大佬同学,最近我竟然和他成为同事了!

640?wx_fmt=png

支付完毕后关注知识星球公众号,然后下载知识星球APP,用微信登录APP后,就可以访问我的知识星球。

关注
打赏
1653961664
查看更多评论
立即登录/注册

微信扫码登录

1.8197s