Google 自8月起要求 Google Play 上架的应用必须采用 AAB 的新格式,对我来说这并非新闻,早在去年12月份官方就提前做了通知:


https://android-developers.googleblog.com/2020/11/new-android-app-bundle-and-target-api.html

令我惊讶的是,这样一条“旧闻”最近却被炒得沸沸扬扬,原来竟还是因为蹭了鸿蒙的热度:

要知道 AAB 的首次亮相是在2018年的 GoogleI/O 上,难道彼时谷歌就遇见到鸿蒙的出现了?

不过客观来说,AAB 虽然早已出现,但在国内很少被提及,因此造成部分媒体的错误解读也有情可原。那么本文就为大家做一个关于 AAB 的科普,打消鸿蒙支持者们的顾虑。

Android App Bundle


Android App Bundle(简称AAB) 是 Google 2018年推出的一种动态化的打包方式。当应用程序以 AAB 的格式上传 Google Play(或其他支持 AAB 的应用市场)后,可以根据不同用户实现 features 或者 resources 的按需下发。Google Play (简称GP) 目前提供的动态化服务都是基于 AAB 实现的(不少文章说这些服务是 AAB 的,这种说法不严谨,准确的说是 GP 的)

  • Play Feature Delivery(PFD) :借助 AAB 实现 Feature 的按需动态加载,这类似于国内流行的“插件化”技术
  • Play Asset Delivery (PAD) :借助 AAB 实现一些资源素材的按需动态下载,这特别适合一些游戏类APP,无需为了适配所有机型保留全部游戏素材

除了游戏资源以外,对于常规资源,AAB 也可以做到按需下发。例如无需同时存在 hdpi、xhdpi 等多套图片,不少 APP 因此在包大小方面有显著提高:

更小的包体积意味着更高的装机率,这在用户推广成本激高的今天至关重要:

App Bundle 文件格式


我们先来看一下 AAB 的文件格式,与传统的 APK 有何不同

解压后的 AAB 中的内容和 APK 很相似,但又有不少区别:

aab fiels descriptions
base/feature1/feature2 base 是应用的基本功能,feature 承载各 DynamicFeature 的内容(后文介绍)
manifest.xml APK 中只有一个 manifest 且是二进制格式,AAB 会存在于每个模块中e中,且使用 ProtoBuf(pb)格式,便于处理
dex 与 APK 不同,AAB 将每个模块的 dex 文件存储在各自目录中
res/assets/libs 该目录与 APK 中相同,当上传 AAB 时,GP 会检查这些目录并仅打包满足目标设备需要的最小文件
resources.pb 类似于 resource.arsc 文件,是一个资源索引表,其中描述了应用程序内部存在的资源和目标的细节,可用于 GP 针对不同设备配置 APK。
assets.pb 相当于应用程序 assets 的资源表,可用于 GP 针对不同设备配置 APK。例如将 assets 资源放到 assets/languages#lang_xx 或 assets/i18n#lang_xx 路径下,则会根据语言配置下发 assets 资源。
native.pb 这相当于native库的资源表,可用于 GP 针对不同设备配置 APK

后三个.bp文件是 AAB 格式的重要部分,它们描述了 APP 的不同服务目标,动态下发根据这些目标从 drawable/hdpilib/armeabi-v7a 或者 values/es 等路径中组织不同资源进行下发。

Split APKs


Split APKs 机制是 AAB 实现动态下发的基础,它允许将一个庞大的 APK 按不同维度拆分成独立的 APK,当用户在 GP 下载应用时,Android Framework 通过 IPC 与 GP 通信,为当前设备匹配并下载最小构成的 APK, 这只在 Android 5.0 以上的设备才有效。

AAB 上传后,GP 通过分析找出所有设备的共同资源, 生成一个 Base APK,当用户下载应用时,Base APK 将被首先安装。

GP 又根据languagedensityabi 等三个维度,分别生成 Configuration APKs(Splits), Splits 与 Base 共享 versionCode 、packageName等,在进程管理器中以一个应用的形式存在。

当用户从市场下载应用时,GP 根据设备类型,为其下发不同的 Splits,实现最小化下发。

如下图,针对三种不同设备下发不同 Splits

当用户的设备发生 Configuration Changed (比如切换了系统语言)时,GP 会下发新的 Splits 到手机,如果此时手机不在线会等待下次上线时自动下发。

Split APKs 的这种动态下发只能用于 Android 5.0 以上设备,对于更旧的设备,AAB 会根据 这些 Splits 的矩阵生成多个 Standalone 的 APK,虽然缺少了动态下发的能力必须一次安装到位,但是相对于传统 APK 仍然减小了一定包大小。

作为开发者,我们无需关心这些具体的下发策略,只需要向市场上传一个 AAB ,后续就交给 FW 和 GP 去处理了。

创建 App Bundle


打包 AAB

使用 Android Studio 可以方便地打包 AAB

此外,也可以使用 Gradle 命令打包,这更适用于一些 CI 流程中。

如下使用 gradle 打包一个 debug 版的 AAB

./gradlew :base:bundleDebug

如果要生成 release 的 AAB 需要配置签名,与 APK 的配置方式是一样的。

AAB 默认会为三种 Configurations 都生成 Splits,当然你可以根据需求自己配置:

bundle {language {enableSplit = false}density {enableSplit = true}abi {enableSplit = true}}

上传应用市场

生成 AAB 后就可以上传应用市场了,GP 中上传 AAB 和 APK 的入口在一起,当然 8 月以后就没有 APK 的上传入口了。

AAB 上传后,通过后台可以查看其详细信息

例如可以查看 AAB 支持的屏幕密度,以及包体积的减少等信息

Bundle Tool


AAB 是无法直接安装到手机的,如果想本地对 AAB 做测试,需要将 AAB 转成 APK,这需要使用 Google 官方提供的 Bundletool 工具

Bundletool 可以获取当前设备信息

bundletool get-device-spec --output=/tmp/device-spec.json

设备的 Configurations 信息输出到指定 json 中

{"supportedAbis": ["arm64-v8a", "armeabi-v7a", "armeabi"],"supportedLocales": ["zh-CN"],"deviceFeatures" : // ..."screenDensity": 480,"sdkVersion": 28
}

Bundletool 根据 json 生成 .apks 中间文件

bundletool build-apks
--bundle=/MyApp/my_app.aab
--output=/MyApp/my_app.apks
--ks=/MyApp/keystore.jks
--ks-pass=file:/MyApp/keystore.pwd
--ks-key-alias=MyKeyAlias
--key-pass=file:/MyApp/key.pwd
--device-spec=file:device-spec.json

apks 的产物分为 splitsstandalones 两个目录,splits 是按照 Configuration维度拆分的 Split APKs,必须依赖 base.apk 一起安装;standalone 必须独立安装,这是为了兼容 Android 5.0 以下的版本。

toc.pb 是 apks 的存档清单,包含 APK 集合信息的描述文件

然后再根据 json 文件,从 apks 中提取 apk :

bundletool extract-apks
--apks=${apksPath}
--device-spec={deviceSpecJsonPath}
--output-dir={outputDirPath}

最后,通过 Bundletool 将 apk 安装到手机上。 注意该命令实际安装 apk 并非 apks

bundletool install-apks --apks=/MyApp/my_app.apks

总结一下 Bundletool 生成 APK 的整体流程:

创建 Dynamic Feature


除了下发 Configuration APKs,还可以以业务模块为单元“插件化”地动态下发,也就是所谓的 Dynamic Features(简称 DF)

IDE 中选择 New 一个 DF 的 Module:

点击 next,选择 DF 的安装时机,例如一次安装到位或是按需安装

创建好的 DynamicFeature Module, 目录和一个普通的 Gadle Module 类似

但是 build.gradle 中 plugin 有所不同:com.android.dynamic-feature

plugins {id 'com.android.dynamic-feature'id 'kotlin-android'
}

build.gradle 中也无需配置 versionCodeversionNamesignConfig等,DF 本质上也是 Split APKs,所以共享 Base APK 的这些信息。

此时再打开 app/ 的 build.gradle,会发现多了如下配置

dynamicFeatures = [':dynamicfeature']

这是 APP 当前支持的所有 DF 的声明

最后,DF 的 Manifeset 也发生了变化:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:dist="http://schemas.android.com/apk/distribution"package="com.github.dynamicfeature"><dist:moduledist:instant="false"dist:title="@string/title_dynamicfeature"><dist:delivery><dist:on-demand /></dist:delivery><dist:fusing dist:include="true" /></dist:module>
</manifest>
  • dist:delivery: 在创建 Dynmaic Feature 的 Module 时选择的下发方式, onDemand 表示方式为按需下发
  • title:当用户确认下载 Module 时,标识相关名称
  • fusing include:设为 ture,意味着 5.0 以下的设备可以以 multi-APK 的形式安装此 Feature,此时必须设置为 onDemand 方式。

安装 Dynamic Feature


当应用支持 DF 之后,我可以按需的请求并安装这些 Features,这需要集成 Play Core SDK

implementation 'com.google.android.play:core:$latest_version'

Play Core 允许用户通过交互的方式请求 DF 的下载安装,并监听下载状态

发起下载请求

DF 的下载需要借助 SplitInstallManager

SplitInstallManager splitInstallManager =SplitInstallManagerFactory.create(context);

创建 SplitInstallRequest, 请求下载 Module

//动态请求模块
SplitInstallRequest request =SplitInstallRequest.newBuilder().addModule("someDynamicModule").build();

addModule() 可以多次调用,添加多个请求的 DF

使用 SplitInstallManager 启动 Request 进行请求,并设置回调监听为下载状态

splitInstallManager.startInstall(request).addOnSuccessListener {  }.addOnFailureListener {  }.addOnCompleteListener {  }

startInstall() 调用后会立即发起请求。另外还可以使用 deferredInstall 延迟请求, 当应用切到后台启动时才开始请求。

splitInstallManager.deferredInstall(Arrays.asList("someDynamicModule"));

除了请求指定 DF 以外,也可以请求指定的资源,比如安装语言资源

SplitInstallRequest request =SplitInstallRequest.newBuilder().addLanguage(Locale.forLanguageTag(sharedPrefs.getString(LANGUAGE_SELECTION))).build();

发起请求后,会返回一个 Int 值作为 session ID,通过调用 cancelInstall(Int), 可以取消当前的下载。

发起请求后,可能无法正常建立链接,此时会返回错误信息如下

Error Descriptions
ACCESS_DENIED 鉴于当前设备的某些原因,无法下载
ACTIVE_SESSIONS_LIMIT_EXCEEDED 当前应用的请求 session 太多
API_NOT_AVAILABLE 请求 API 目前无法使用
INCOMPATIBLE_WITH_EXISTING_SESSION 请求的 session 中包含了已经请求中的 DF
INTERNAL_ERROR 内部错误
INVALID_REQUEST 无效请求
MODULE_UNAVAILABLE 请求的 DF 不存在
NETWORK_ERROR 网络错误
NO_ERROR 无法获得错误信息
SERVICE_DIED 服务无响应
SESSION_NOT_FOUND 无法获取被请求的 session

下载安装

成功建立了连接后,便进入下载、安装阶段。使用 SplitInstallStateUpdatedListener 能够监听下载安装的状态,可以根据这些状态为对下载进度等进行用户提示

val stateListener = SplitInstallStateUpdatedListener { state ->when (state.status()) {PENDING -> { }DOWNLOADING -> { }DOWNLOADED -> { }INSTALLED -> { }INSTALLING -> { }REQUIRES_USER_CONFIRMATION -> { }FAILED -> { }CANCELING -> { }CANCELED -> { }}
}
splitInstallManager.registerListener(stateListener)
State Description
CANCELED 下载被取消
CANCELING 下载取消中
DOWNLOADED 下载完成,但是尚未安装
DOWNLOADING 下载即将完成
FAILED 下载或安装失败
INSTALLED 成功安装
INSTALLING 安装中
PENDING 下载等待中
REQUIRES_USER_CONFIRMATION 等待用户确认
UNKNOWN 未知

卸载模块

成功安装后,通过 getInstalledModules 可以获取所有已安装的 Module

val installedModules = splitInstallManager.installedModules

另外,通过 deferredUninstall 可以对 DF 进行指定卸载

splitInstallManager.deferredUninstall(listOf("someDynamicModule")).addOnSuccessListener {  }.addOnFailureListener {  }.addOnCompleteListener {  }

AAB 使用效果


根据 Google 官方的数据,AAB 比 APK 的包大小平均会减小 20% ,这同时意味着节省了 20% 的下载流量。 以 Twitter 为例,采用 AAB 之后

  • language 相关资源节省 95%
  • density 相关的 Splits 节省 45%
  • abi 相关资源节省 20%

除了包大小方面的优势以外,使用 AAB 在开发效率上也有收益,无需再针对不同目标点设备,配置多个 Flavor、生成多个 APK 并分别上传,只要上传一个 AAB,剩下的事情交由应用市场去做就好了。

国内的 AAB 使用


Qigsaw 是爱奇艺提供的一套基于 Android App Bundle 的动态化方案,无需 Google Play Service 即可在国内体验 Android App Bundle 开发工具。它支持动态下发插件 APK,采用单类加载器方式,让应用能够在不重新安装的情况下实现动态安装插件。

此外,华为应用市场也早就支持了 AAB 的上传和动态下发,所以不要再说 AAB 是打压华为的产物了

AAB 扶正!APK 将退出历史舞台相关推荐

  1. PHP即将退出,PHP4即将退出历史舞台

    http://www.phpeye.com/bbs/viewthread.php?tid=92 PHP官方团队在php.net上发布了一则公告: Today it is exactly three y ...

  2. 互联网晚报 | 04月05日 星期二 |​ ​​​考研调剂系统明日开通;微软Edge浏览器宣布10天后退出历史舞台...

    微软Edge浏览器宣布10天后退出历史舞台 4月5日消息,在愚人节当天,微软Edge浏览器官方微博发布重要通知:"对所有 Edge浏览器的使用者.爱好者说一声抱歉" .IE浏览器将 ...

  3. 感谢3G,告别3G:开启移动互联网时代的“功臣”退出历史舞台

    整理 | 章雨铭 责编 | 屠敏 出品 | CSDN(ID:CSDNnews) 移动互联网时代由3G开始,时过境迁,5G时代已经到来,3G这位开启移动互联网时代的功臣将要退出历史舞台. 图源视觉中国 ...

  4. 为什么说 Web 开发永远不会退出历史舞台?

    早在 PC 崛起之际,Web 从蹒跚学步一路走到了主导市场的地位,但是随着移动互联网时代的来临,业界曾有不少人猜测,"Web 应该被杀死,App 才是未来".不过时间是检验真理的唯 ...

  5. Windows 7 彻底退出历史舞台

    整理 | 禾木木 出品 | CSDN(ID:CSDNnews) 今日一条 #Windows7将彻底退出历史舞台#的话题冲上了热搜,引发了网友们的热议. 1 月 10 日开始,微软将不再为专业版和企业版 ...

  6. 雷军卸任小米软件技术公司董事长;微软洽谈向 OpenAI 投资 100 亿美元;Windows 7 彻底退出历史舞台|极客头条

    「极客头条」-- 技术人员的新闻圈! CSDN 的读者朋友们早上好哇,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧. 整理 | 梦依丹 出品 | CSDN(ID:CSDNnews ...

  7. 2G、3G要退出历史舞台了?为何3G比2G淘汰更快?

    进入2020年,我国加速推进5G网络商用,加上4G的快速普及,2G.3G网络现在到了该被淘汰的时候了吗?全国超过3亿的2G.3G用户怎么办? 我国5G商用牌照发布已满一周年.一年以来,我国5G用户规模 ...

  8. 腾讯soso退出历史舞台,搜索结果全部由搜狗提供

     腾讯soso退出历史舞台,搜索结果全部由搜狗提供 前段时间腾讯4.48亿美元战略投资搜狗搜搜注入搜狗,现在soso的搜索结果已经全部由搜狗提供了,上图哈:

  9. 时隔27年,微软IE浏览器正式退出历史舞台,一个时代的结束

    关注并星标 从此不迷路 计算机视觉研究院 公众号ID|ComputerVisionGzq 学习群|扫码在主页获取加入方式 计算机视觉研究院专栏 作者:Edison_G 很遗憾「目睹」IE 浏览器退出历 ...

最新文章

  1. 宝塔的服务忽然挂掉解决方法
  2. delphi中checkcombobox最大长度_并行光信号传输中的信道间传播时间偏差
  3. 荣耀v30pro搭载鸿蒙吗,荣耀麒麟30pro+,可以搭载鸿蒙的顶级荣耀,你买了吗?...
  4. [Go] 正则表达式 示例
  5. SAP BRF+ Interpretation模式与Generation模式的区别
  6. 断言、触发器、存储过程
  7. 工业交换机的几大“择机”标准,你学会了吗?
  8. Win32ASM学习[13]:移位指令SHL,SHR,SAL,SAR,ROL,ROR,RCL,RCR,SHLD,SHRD
  9. 云版 Android 系统来了?
  10. IBM将剥离传统IT基础设施部门;迅雷前CEO陈磊涉嫌职务侵占罪被调查 ;Python 3.9发布|极客头条
  11. Windows Mobile获取通话记录 C#
  12. 基于Chrome内核(WebKit.net)定制开发DoNet浏览器
  13. 从我那句名言“系统上线之日,需求开始之时”谈大型信息化系统建设的那些坑
  14. ChatGPT|微信快速接入ChatGPT
  15. 带你入门NoSQL(真的是太全了)
  16. ROS IDE —— RoboWare Studio
  17. mac设计师系列 Adobe “全家桶” 15款设计软件 值得收藏!
  18. 什么是JAVA中的强制类型转换
  19. 五、MUX-VLAN QinQ技术
  20. 普通石粉的用途_地面铺水泥,用石粉做底层有什么作用

热门文章

  1. 【CSS】987- 十几个 CSS 高级技巧汇总
  2. 激活window10专业版的方法。
  3. unity 加载关卡_Unity5.0_Application.isLoadingLevel 正在加载关卡_软件教程_资源库
  4. 前端之vue3的setup和setup的2个的形参、响应式页面
  5. 在线版区间众数 hzw的代码。。
  6. NXP Nfc模块Framework层移植遇到的坑【二】
  7. 2010年IT十大人物猜想
  8. Docker入门详解
  9. python访问陌生人qq空间_使用Python+Selenium模拟登录QQ空间
  10. 【评测】无血清细胞冻存液