支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能

本文涉及的产品
mPaaS订阅基础套餐,标准版 3个月
简介: 本章节我们将围绕《支付宝 App 构建优化解析》另启新系列,细分拆解客户端在“代码管理”、“证书管理”、“版本管理”、“构建打包”等维度的具体实现方案展开讨论,带领大家进一步了解支付宝在 App 构建模块下的持续优化。

1. 前言

本章节我们将围绕《支付宝 App 构建优化解析》另启新系列,细分拆解客户端在“代码管理”、“证书管理”、“版本管理”、“构建打包”等维度的具体实现方案展开讨论,带领大家进一步了解支付宝在 App 构建模块下的持续优化。

本节将主要记录通过对支付宝 Android Apk 文件的重新布局,来改善 IO 性能的过程。

2. 背景

支付宝 App 在 Android 平台上,由于大量业务快速上线,Android 长尾机型等原因,造成启动阶段及部分核心链路上,性能体验不理想,进而影响用户的使用的感受。
从纯业务角度,可以通过优化 UI 布局,优化代码结构,优化 bundle 加载等方式,对性能体验有所改善。作为工程技术团队,按照传统思维来看,似乎无法对性能优化做多少贡献。经过一些方案调研后,我们尝试通过对编译产物的优化,干预构建流程,以提升 App 性能。

3. 原理

布局前后,Apk 中实际的文件并没有本质改变,只有位置发生了变化。那么为什么这样的调整会有性能造成影响?这个原理要追溯到 Linux 的文件系统机制。

如下图所示,Linux 底层文件系统中 VFS 上次 App 进程之间,存在一层 pagecache,pagecache 由内存中的物理 page 组成,其内容对应磁盘上的 block。Pagecache 的大小是动态变化的,可以扩大,也可以在内存不足时缩小。Cache 缓存的存储设备被称为后备存储(backing store),一个 page 通常包含多个 block,这些 block 不一定是连续的。

当内核发起一个读请求时(例如进程发起 read() 请求),首先会检查请求的数据是否缓存到了 pagecache 中。如果有,那么直接从内存中读取,不需要访问磁盘,这被称为 cache命中(cache hit)。如果 cache 中没有请求的数据,即 cache 未命中(cache miss),就必须从磁盘中读取数据。

然后内核将读取的数据缓存到 cache 中,这样后续的读请求就可以命中 cache 了。Page 可以只缓存一个文件部分的内容,不需要把整个文件都缓存进来。对磁盘的数据进行缓存从而提高性能主要是基于两个因素:

  • 第一,磁盘访问的速度比内存慢好几个数量级(毫秒和纳秒的差距)。
  • 第二是被访问过的数据,有很大概率会被再次访问。

结合 Android 系统实际来看,上层 App 每次读取磁盘时,文件系统默认会按 16 * 4k block 去磁盘读取数据,并把数据放到 pagecache 中。如果下次读取文件已经在 pagecache 中,则不会发生真实的磁盘 IO,而是直接从 pagecache中 读取,大大提升读的速度。有缓存就有回收,pagecache 的另一个重要工作是释放 page,从而释放内存空间。Cache 回收的任务是选择合适的 page 释放,并且如果 page 是 dirty 的,需要将 page 写回到磁盘中再释放。

理想的做法是释放距离下次访问时间最久的 page,但是很明显,这是不现实的。基于 LRU改进的 Two-List 是 Linux 使用的策略。这个回收策略非常类似业务开发领域,常见的图片加载的缓存策略。LRU 算法是选择最近一次访问时间最靠前的 page,即干掉最近没被光顾过的 page。原始 LRU 算法存在的问题是,有些文件只会被访问一次,但是按照 LRU 的算法,即使这些文件以后再也不会被访问了,但是如果它们是刚刚被访问的,就不会被选中。

Two-List 策略维护了两个list,active list 和 inactive list。在 active list 上的 page 被认为是 hot 的,不能释放。只有 inactive list 上的 page 可以被释放的。首次缓存的数据的 page 会被加入到 inactive list 中,已经在 inactive list 中的 page 如果再次被访问,就会移入 active list 中。两个链表都使用了伪 LRU 算法维护,新的 page 从尾部加入,移除时从头部移除,就像队列一样。

如果 active list 中 page 的数量远大于 inactive list,那么 active list 头部的页面会被移入 inactive list 中,从而维持两个表的平衡。简单的说,通过文件重布局的目的,就是将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。

4. 落地方案

在了解原理之后,就需要考虑怎么用工程化的方案在支付宝 App 上落地,主要从以下三个流程来设计方案并落地。

  • 度量:

重布局的前提必须是精确的度量,定位到那些可以调整,需要调整的文件。这个过程需要足够的准确,否则会导致重布局之后的效果不佳。
度量的最终目的是要,统计到支付宝启动阶段,哪些文件加载了,并且是发生真实的磁盘IO,还是命中了 pagecache 缓存。我们提供了一个度量工具,通过修改 kernel 源码,dump 出文件系统的 IO 行为,在特定的 Android ROM 上打个补丁,用来统计启动时刻文件行为。部分数据如下:

数据中,第一列的数据表示发生 IO 行为的文件,第二列表示该文件中此偏移量对应的部分发生了 IO 行为。

第一列表示发生 IO 的位置,如果为 0,则表示发生了真实的磁盘 IO;如果为 1,则表示从pagecache 缓存中读取了内容。

通过数据可以发现,Apk 中部分文件,实际上是发生了磁盘 IO,可以尝试将启动阶段, Apk 中所用到的文件排布到一起,期望通过少量的 IO,就将所有的文件全部读到。之后的工作,需要通过解析 zip 包结构,将上述结果中,文件偏移量对应到详细的文件名。首先需要得到安装包中的文件排布情况,可以通过类似 010 Editor 的工具得到,为了工程化的考虑,也可以参考 zip 格式定义通过脚本分析 zip 文件实现。

然后通过解析结果和先前的统计结果对应分析,就能找到 zip 中哪些文件,在启动阶段被读到,为重布局提供数据支撑。

  • 重布局:

在得到一个启动阶段的文件列表后,第二步工作,就是根据这个文件列表,在构建打包阶段,在 Apk 中把这部分文件排布在一起。这里需要修改 7z 压缩工具的源码。支付宝构建流程,为了提升压缩效率,减少包大小,使用 7z 工具进行最后压缩出 Apk 的过程。这里在简单阐述下,重排布的原因,无论是那种压缩工具,zip 中文件顺序是文件系统的默认顺序,即按照阿拉伯数字和字母顺序。如果想指定文件排在一起,必然要打破这种规则。
修改 7z 源码的过程,简单思路如下,扩展一个命令行参数,我们使用了上箭头'^'(表意性强,提前的意思),可以传入 list.txt,然后 7z 执行输出文件流时候,按照 list 中的文件顺序,改变最后的输出顺序,从而达到重排布的目的。例如如下命令,就是将 source 目录中,所有文件压缩,并且把 list 中指定文件排布在 zip 包的开始位置。

7z a -tzip archive.zip source* ^list.txt

通过这种方式,就实现了文件重排布的简单过程,当然在支付宝的构建流程中,较为复杂,中间还涉及到重打包,重签名等一系列流程。后续内容会提到。
这里有一个小插曲,在刚开始调整文件顺序时,我们通过测量发现效果并不好。后来发现了原因,原先我们调整的文件列表,只是度量阶段发现,所有发生磁盘 IO 的文件,把他们排布到一起,错误的认为,只要他们调整了,整体 IO 情况就会改善。可是忽略了“此消彼长”的问题,如果只调整这些文件,那么原先排布在这些文件后面,利用预读机制进缓存 cache 的文件,如果在启动阶段用到,可能会发生新的磁盘 IO。正确的调整方式,应该能精确按时间顺序统计启动阶段的所有文件,排布在一起,这样发生少量 IO,就能全部读到 cache 中。
简单看下某一次实验主 Apk 中文件调整前后的效果如下,几个和配置相关的移到文件头部。

调整前

调整后

  • 回归测试:

按照所以计划将文件全部调整完毕后,就到了验证效果的环节。主要有以下几种验证方式和思路:

  • 线下录屏,然后拆解视频帧,测直观的启动时间。
  • 线下使用工具度量 IO 情况,观察启动阶段磁盘 IO 数量是否减少,量化一个“cache miss 率”的概念。
  • 线下通过埋点的方案,通过脚本,多次模拟冷启动,取平均值测量,消除可能误差,观察趋势。
  • 线上灰度在其他优化和代码类似情况下,只通过调整 IO,比较两个版本的启动时间变化。 在重布局方案实验阶段,使用一二两种方案较多,后续工程化落地和常态化优化时,应采用三四种方案。

5. 演进

通过上述落地方案,在线下以及某些线上灰度版本中完成初步实验后,我们考虑工程化,常态化的进行这件事情。在工程化之前,先对度量流程进行了扩充,探索出了一种较为简单的度量手段。

  • 度量优化:

原先的度量方案,具备较深的技术含量,在这个方案中,需要对 Linux 底层文件系统非要熟悉和了解,并且还需具备修改源码的能力,此方案是由其他资深专家指导下实现,短期内,团队暂时无法独立这个方案。
为了让整体方案可控,我们想到了直接在 Android 源码的资源加载流程中记录日志,然后通过日志直接分析,这样启动阶段文件加载一目了然,当然缺陷也很明显,无法通过判断文件读取是通过磁盘 IO 还是 pagecache 缓存。
干预资源加载记录,要不通过 hook 方式,要不就是直接改 framework,刷个 ROM,考虑到工程化自动化测试的因素,采用了修改 framework 的方式,方便后续有测试平台,直接使用特定手机跑脚本执行即可。
以 Android 7.0 版本为例,主要修改 drawable 相关流程和 xml 相关流程。其他版本如果做测试度量机型的化,修改方式类似。

  • xml 加载流程修改,在解析 xml 文件流程,直接打日志。
  /**
     * Loads an XML parser for the specified file.
     *
     * @param file the path for the XML file to parse
     * @param id the resource identifier for the file
     * @param assetCookie the asset cookie for the file
     * @param type the type of resource (used for logging)
     * @return a parser for the specified XML file
     * @throws NotFoundException if the file could not be loaded
     */
    @NonNull
    XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
            @NonNull String type)
            throws NotFoundException {
        if (id != 0) {
            try {
                synchronized (mCachedXmlBlocks) {
                    if (!getResourcePackageName(id).equalsIgnoreCase("android")) {
                        Log.i("AlipayRes", "ResourceId: " + Integer.toHexString(id) + "  ResourcePackage name: " + getResourcePackageName(id) + "  Loading xml: " + file);
                    }
                    final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
                    final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
                    final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
                    // First see if this block is in our cache.
                    final int num = cachedXmlBlockFiles.length;
                    for (int i = 0; i < num; i++) {
                        if (cachedXmlBlockCookies[i] == assetCookie && cachedXmlBlockFiles[i] != null
                                && cachedXmlBlockFiles[i].equals(file)) {
                            return cachedXmlBlocks[i].newParser();
                        }
                    }
            ……
            ……
    }
  • drawable 修改
 /**
     * Loads a drawable from XML or resources stream.
     */
    private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
            Resources.Theme theme) {
        if (value.string == null) {
            throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
                    + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value);
        }

        final String file = value.string.toString();

        if (TRACE_FOR_MISS_PRELOAD) {
            // Log only framework resources
            if ((id >>> 24) == 0x1) {
                final String name = getResourceName(id);
                if (name != null) {
                    Log.d(TAG, "Loading framework drawable #" + Integer.toHexString(id)
                            + ": " + name + " at " + file);
                }
            }
        }

        if (DEBUG_LOAD) {
            Log.v(TAG, "Loading drawable for cookie " + value.assetCookie + ": " + file);
        }
        if (!getResourcePackageName(id).equalsIgnoreCase("android")) {
            Log.i("AlipayRes", "ResourceId: " + Integer.toHexString(id) + "  ResourcePackage name: " + getResourcePackageName(id) + "  Loading drawable: " + file);
        }
        ……
        ……
    }

刷入 ROM,替换修改后 framework 后,冷启动支付宝,清楚缓存,通过日志过滤即可得到完整启动文件加载列表。

adb shell am force-stop com.eg.android.AlipayGphone
adb shell
echo 1 > /proc/sys/vm/drop_caches

  • 工程化:

所以单点能力都基本具备单点能力都具备后,需要找到一个能尽可能自动化的方案。具体流程图如下。
后续对于 ReApk (优化Apk)流程,可以扩展其他的构建构建产物优化方案。

6. 结果与展望

目前整体方案,已上线支付宝钱包 Android App,该单项,启动性能,在整体全量用户下有 5% 左右的优化效果,低端机上效果较明显,根据不同机型,能有10%左右的启动性能优化效果。

Facebook 的工具链优化方案 Redex,对于 dex 的优化,从度量到回归测试,开源出了一整套解决方案,对于 zip 的重布局,希望未来能将此整套方案,做到尽可能的“开箱即用”,赋能公司内外更多的 App。

7. 小结

通过本节内容,我们初步了解了支付宝在 Android 客户端如何通过安装包重排布来优化 IO 性能。由于篇幅限制,很多技术要点我们无法一一展开。而相应的技术内核,我们同样应用在了 mPaaS 并对外输出,欢迎大家上手体验:

https://tech.antfin.com/docs/2/49549

关于 Android 端启动性能优化的设计思路和具体实践,同样期待你们的反馈,欢迎一起探讨交流。

往期阅读

《支付宝客户端架构解析:iOS 容器化框架初探》

《支付宝客户端架构解析:Android 容器化框架初探》

《支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」》

《支付宝客户端架构解析:iOS 客户端启动性能优化初探》

关注我们微信公众号「mPaaS」,获得第一手 mPaaS 技术实践干货

目录
相关文章
|
21天前
|
存储 消息中间件 人工智能
【08】AI辅助编程完整的安卓二次商业实战-修改消息聊天框背景色-触发聊天让程序异常终止bug牵涉更多聊天消息发送优化处理-优雅草卓伊凡
【08】AI辅助编程完整的安卓二次商业实战-修改消息聊天框背景色-触发聊天让程序异常终止bug牵涉更多聊天消息发送优化处理-优雅草卓伊凡
106 10
【08】AI辅助编程完整的安卓二次商业实战-修改消息聊天框背景色-触发聊天让程序异常终止bug牵涉更多聊天消息发送优化处理-优雅草卓伊凡
|
7月前
|
存储 缓存 网络协议
阿里云特惠云服务器99元与199元配置与性能和适用场景解析:高性价比之选
2025年,阿里云长效特惠活动继续推出两款极具吸引力的特惠云服务器套餐:99元1年的经济型e实例2核2G云服务器和199元1年的通用算力型u1实例2核4G云服务器。这两款云服务器不仅价格亲民,而且性能稳定可靠,为入门级用户和普通企业级用户提供了理想的选择。本文将对这两款云服务器进行深度剖析,包括配置介绍、实例规格、使用场景、性能表现以及购买策略等方面,帮助用户更好地了解这两款云服务器,以供参考和选择。
|
7月前
|
存储 缓存 负载均衡
阿里云服务器实例选择指南:热门实例性能、适用场景解析对比参考
2025年,在阿里云的活动中,主售的云服务器实例规格除了轻量应用服务器之外,还有经济型e、通用算力型u1、计算型c8i、通用型g8i、计算型c7、计算型c8y、通用型g7、通用型g8y、内存型r7、内存型r8y等,以满足不同用户的需求。然而,面对众多实例规格,用户往往感到困惑,不知道如何选择。本文旨在全面解析阿里云服务器实例的各种类型,包括经济型、通用算力型、计算型、通用型和内存型等,以供参考和选择。
|
7月前
|
JavaScript Linux 网络安全
Termux安卓终端美化与开发实战:从下载到插件优化,小白也能玩转Linux
Termux是一款安卓平台上的开源终端模拟器,支持apt包管理、SSH连接及Python/Node.js/C++开发环境搭建,被誉为“手机上的Linux系统”。其特点包括零ROOT权限、跨平台开发和强大扩展性。本文详细介绍其安装准备、基础与高级环境配置、必备插件推荐、常见问题解决方法以及延伸学习资源,帮助用户充分利用Termux进行开发与学习。适用于Android 7+设备,原创内容转载请注明来源。
1432 77
|
4月前
|
安全 Java Android开发
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
192 0
为什么大厂要求安卓开发者掌握Kotlin和Jetpack?深度解析现代Android开发生态优雅草卓伊凡
|
5月前
|
缓存 编解码 Android开发
Android内存优化之图片优化
本文主要探讨Android开发中的图片优化问题,包括图片优化的重要性、OOM错误的成因及解决方法、Android支持的图片格式及其特点。同时介绍了图片储存优化的三种方式:尺寸优化、质量压缩和内存重用,并详细讲解了相关的实现方法与属性。此外,还分析了图片加载优化策略,如异步加载、缓存机制、懒加载等,并结合多级缓存流程提升性能。最后对比了几大主流图片加载框架(Universal ImageLoader、Picasso、Glide、Fresco)的特点与适用场景,重点推荐Fresco在处理大图、动图时的优异表现。这些内容为开发者提供了全面的图片优化解决方案。
182 1
|
7月前
|
存储 机器学习/深度学习 应用服务中间件
阿里云服务器架构解析:从X86到高性能计算、异构计算等不同架构性能、适用场景及选择参考
当我们准备选购阿里云服务器时,阿里云提供了X86计算、ARM计算、GPU/FPGA/ASIC、弹性裸金属服务器以及高性能计算等多种架构,每种架构都有其独特的特点和适用场景。本文将详细解析这些架构的区别,探讨它们的主要特点和适用场景,并为用户提供选择云服务器架构的全面指南。
802 18
|
7月前
|
XML JavaScript Android开发
【Android】网络技术知识总结之WebView,HttpURLConnection,OKHttp,XML的pull解析方式
本文总结了Android中几种常用的网络技术,包括WebView、HttpURLConnection、OKHttp和XML的Pull解析方式。每种技术都有其独特的特点和适用场景。理解并熟练运用这些技术,可以帮助开发者构建高效、可靠的网络应用程序。通过示例代码和详细解释,本文为开发者提供了实用的参考和指导。
182 15
|
7月前
|
监控 Shell Linux
Android调试终极指南:ADB安装+多设备连接+ANR日志抓取全流程解析,覆盖环境变量配置/多设备调试/ANR日志分析全流程,附Win/Mac/Linux三平台解决方案
ADB(Android Debug Bridge)是安卓开发中的重要工具,用于连接电脑与安卓设备,实现文件传输、应用管理、日志抓取等功能。本文介绍了 ADB 的基本概念、安装配置及常用命令。包括:1) 基本命令如 `adb version` 和 `adb devices`;2) 权限操作如 `adb root` 和 `adb shell`;3) APK 操作如安装、卸载应用;4) 文件传输如 `adb push` 和 `adb pull`;5) 日志记录如 `adb logcat`;6) 系统信息获取如屏幕截图和录屏。通过这些功能,用户可高效调试和管理安卓设备。
|
7月前
|
存储 弹性计算 安全
阿里云服务器ECS通用型规格族解析:实例规格、性能基准与场景化应用指南
作为ECS产品矩阵中的核心序列,通用型规格族以均衡的计算、内存、网络和存储性能著称,覆盖从基础应用到高性能计算的广泛场景。通用型规格族属于独享型云服务器,实例采用固定CPU调度模式,实例的每个CPU绑定到一个物理CPU超线程,实例间无CPU资源争抢,实例计算性能稳定且有严格的SLA保证,在性能上会更加稳定,高负载情况下也不会出现资源争夺现象。本文将深度解析阿里云ECS通用型规格族的技术架构、实例规格特性、最新价格政策及典型应用场景,为云计算选型提供参考。

热门文章

最新文章

推荐镜像

更多