内容简介:《支付宝客户端架构解析》系列将从支付宝客户端的架构设计方案入手,细分拆解客户端在“本节将介绍支付宝 Android 客户端启动速度优化下的「垃圾回收」具体思路。应用启动时间是移动 App 一个重要的用户体验环节,相对于普通的移动 App,支付宝过于庞大,必然会影响启动速度,一些常规的优化手段在支付宝中已经做得比较完善了,本篇文章尝试从 GC 的层面来进一步优化支付宝的启动速度。
《支付宝客户端架构解析》系列将从支付宝客户端的架构设计方案入手,细分拆解客户端在“ 容器化框架设计 ”、“ 网络优化 ”、“ 性能启动优化 ”、“ 自动化日志收集 ”、“ RPC 组件设计 ”、“ 移动应用监控、诊断、定位 ”等具体实现,带领大家进一步了解支付宝在客户端架构上的迭代与优化历程。
本节将介绍支付宝 Android 客户端启动速度优化下的「垃圾回收」具体思路。
应用启动时间是移动 App 一个重要的用户体验环节,相对于普通的移动 App,支付宝过于庞大,必然会影响启动速度,一些常规的优化手段在支付宝中已经做得比较完善了,本篇文章尝试从 GC 的层面来进一步优化支付宝的启动速度。
背景
相对于 C 语言来说,Java 语言有一些特性,例如开发人员不用考虑内存的分配和回收,然而,进程内存管理又是必不可少的环节,妥协的结果是 Java 语言的设计者们把对象分配和回收放到了 Java虚拟机,这里希望明确一个概念:GC 是有代价的,这个代价包括:阻塞 Java 程序的执行,占用 CPU 资源,占用额外内存等,谷歌的工程师意识到了 GC 对应用的影响,所以把 GC 的日志默认输出到了 Logcat,我们经常能够看到 Logcat 里输出以下几种 GC 日志:
GC_EXPLICIT GC_FOR _ALLOCK GC_CONCURRENT
支付宝启动是一个典型的关键路径场景,我们希望看到尽可能少的 GC_ CONCURRENT
(如果可能, GC_ FOR_ ALLOCK
也应该缩减到最少),然而,通过 Logcat 我们会看到非常糟糕的 GC 行为—大量的 GC_ FOR_ ALLOCK
以及触目惊心的 Java 线程被 WAIT_ FOR_ CONCURRENT_ GC
阻塞,如下图所示,通过简单统计这些GC消耗的时间,我们能够得出GC严重影响应用启动时间的结论。
设计思路
支付宝是 Android 系统的一个应用程序,如何能够通过影响 Dalvik 的 GC 行为来缩短启动时间呢?这个问题可以分解为两步:
- 支付宝是否能影响自身 Dalvik 的行为
- 如何改进 Dalvik,缩短启动时间
第一个问题答案是肯定的,Android 系统的设计思路是每个 Android 应用程序都有独立的 Dalvik 实例,应用启动后可以修改自己的进程空间里的代码和数据,因此支付宝通过修改内存中的 Dalvik 库文件libdvm.so 影响 Dalvik 的行为。
第二个问题的难点在于投入产出比:修改进程空间的代码和数据是面向二进制,难度远远大于源代码,也就是说稍微复杂的 Dalvik 改进工作是不可能的。
基于以上两点,提出了一种设想:启动时 GC 抑制,允许堆一直增长,直到开发人员主动停止 GC 抑制或者 OOM 停止 GC 抑制,这是一种"空间换时间"策略,用更多的内存消耗来换取启动时间的缩短,这种策略可行有两个前提:一是设备厂商没有加密内存中的 Dalvik 库文件,二是设备厂商没有改动 Google 的 Dalvik 源码(或者少量的改动),理论上通过白名单的方式可以覆盖所有设备,但是实现和维护成本都非常高。
GC 抑制的实现
GC 抑制的前提是 Dalvik 比较熟悉,知道如何改变 GC 的行为,解决方案大致如下:首先在源码级别找到抑制GC的修改方法,例如改变跳转分支,其次,在二进制代码里找到 A 分支条件跳转的"指令指纹",以及用于改变分支的二进制代码,假设为 override_A
,应用启动后扫描内存中的 libdvm.so
,根据"指令指纹"定位到修改位置,然后用 override_A
覆盖,这里需要注意的是,"指令指纹"的定义需要有一些编译器和 arm 指令集知识,实现 GC 抑制主要实现了以下 4 个部分:
- 取消 softlimit 检测
- 取消 GC 线程的唤醒
- 取消 GC 例程函数
- OOM 停止 GC 抑制的实现
1. 取消 softlimit 检测:
取消 softlimit 检测的目的是最大限度的分配对象,下图为 softlimit 检查对应的 arm 指令片段,位于 dvmHeapSourceAlloc
函数中,OXE057 对应于"return NULL"的分支,如果我们想永远不进入"return NULL"分支,可以改变 cmp 指令的结果,在具体实现里我们把"0X42"作为"指令指纹"来识别而且修改为 "cmp r0, r0",这样就可以实现取消 softlimit 检查。
7616c: 42a1 cmp r1, r4 7616e: d901 bls.n 76174 <_Z18dvmHeapSourceAllocj+0x20> 76170: 2400 movs r4, #0 76172: e057 b.n 76224 <_Z18dvmHeapSourceAllocj+0xd0> 76174: f8df 90bc ldr.w r9, [pc, #188] ; 76234 <_Z18dvmHeapSourceAllocj+0xe0> 76178: 6a28 ldr r0, [r5, #32] 7617a: f853 3009 ldr.w r3, [r3, r9] 7617e: 7d1a ldrb r2, [r3, #20] void* dvmHeapSourceAlloc(size_t n) { ... if (heap->bytesAllocated + n > hs->softLimit) { /* * This allocation would push us over the soft limit; act as * if the heap is full. / return NULL; 复制代码
2. 取消GC线程的唤醒
取消 GC 线程唤醒的目的是防止 GC 线程频繁唤醒导致的线程抖动。下图是对应的 C++ 代码和 arm 指令片段,这段代码同样位于 dvmHeapSourceAlloc
函数中。在具体实现里我们会依次扫描 libdvm.so
的 dynstr、dynsym、rel.plt 和 plt 区域获取 pthreadcondsignal@plt 的地址,然后遍历 dvmHeapSourceAlloc
中的所有分支跳转,计算跳转目的地址。
如果发现 pthreadcondsignal@plt 和当前分支跳转目的地址配置,擦除这条指令即可。
if (heap->bytesAllocated > heap->concurrentStartBytes) { / * We have exceeded the allocation threshold. Wake up the * garbage collector. */ dvmSignalCond(&gHs->gcThreadCond); } 7621c: 6800 ldr r0, [r0, #0] 7621e: 30b4 adds r0, #180 ; 0xb4 76220: f7a9 ed0e blx 1fc40 76224: 4620 mov r0, r4 76226: e8bd 83f8 ldmia.w sp!, {r3, r4, r5, r6, r7, r8, r9, pc} 复制代码
3. 取消GC例程函数
取消 GC 例程函数采用钩子技术来实现,我们将 GC 抑制封装成了两个 native 接口 doStartSuppressGC
和 doStopSuppressGC
;并且进一步封装为 JNI 接口,便于开发者在 Java 里调用。一般的应用方式是,开发者通过日志看到支付宝在某个场景会触发大量的 GC 且这个 GC 影响用户体验(响应时间慢或者动画卡顿),然后在这个场景前后插入 doStartSuppressGC
和 doStopSuppressGC
。
以支付宝冷启动场景为例,我们在容器 Quinox 的 attachBaseContext
函数里插入 doStartSuppressGC
,在首页加载结束时插入 doStopSuppressGC
。
4. OOM 停止GC抑制的实现
如果仅仅考虑在支付宝启动过程中抑制 GC,不需要考虑 OOM 停止 GC 抑制的实现,因为支付宝启动不足以触发 OOM。但是我们希望 GC 抑制成为一个基础模块,能够应用到更多场景中。如果程序在调用 doStopSuppressGC
前触发了 OOM,则需要在 OOM 发生前停止 GC 抑制。和前面简单的改变分支跳转方向不同,需要在 OOM 发生前注入一个新的的分支跳转,这个新分支的代码由我们来实现。新分支主要功能是,调用 doStopSuppressGC
,然后去掉注入的新分支,最后跳回 Dalvik 执行 OOM。
实现同样采用传统的钩子技术。在钩子函数 dvmCollectGarbageInternal
里:
dvmCollectGarbageInternal
实现中使用了开源的二进制注入框架: github.com/crmulliner/… 。
这里需要注意的是,在热点函数里使用这个框架提供的 pre_hook
和 post_hook
的性能开销非常大。
本文里的设计只会用到一次 pre_hook
,所以不存在性能问题。 看到的这里读者可能会问,这种通过“指令指纹”的方式靠谱么?我的答案是,漏判不影响正确性,误判理论上存在但概率极小(误判指“指令指纹”定位到错误代码位置)。即使误判发生了,我们还有最后一层保障——基础架构组同学实现的容灾机制。当误判导致程序异常无法完成正常启动时,重启支付宝而且在后续的启动中直接放弃 GC 抑制。
效果
上图的启动时间的数据是在内部的 Android 4.x 测试设备上获得的(没有标注 release 表示 debug 版本)。从图表上来看,支付宝客户端的启动时间缩短了 15%~30%。
小结
通过本节内容,我们初步了解了支付宝在 Android 客户端启动性能优化下的「垃圾回收」机制和具体实践,由于篇幅限制,很多技术要点我们无法一一展开。而相应的技术内核,我们同样应用在了 mPaaS 并对外输出,欢迎大家上手体验:
关于 Android 端启动性能优化的设计思路和具体实践,同样期待你们的反馈,欢迎一起探讨交流。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 支付宝客户端架构解析:iOS 客户端启动性能优化初探
- 自己动手做数据库客户端: BashSQL开源数据库客户端
- 客户端HTTP缓存
- 简析移动客户端安全
- 配置Hadoop集群客户端
- Spring Webflux客户端
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
软件框架设计的艺术
[捷] Jaroslav Tulach / 王磊、朱兴 / 人民邮电出版社 / 2011-3 / 75.00元
本书帮助你解决API 设计方面的问题,共分3 个部分,分别指出学习API 设计是需要进行科学的训练的、Java 语言在设计方面的理论及设计和维护API 时的常见情况,并提供了各种技巧来解决相应的问题。 本书作者是NetBeans 的创始人,也是NetBeans 项目最初的架构师。相信在API 设计中遇到问题时,本书将不可或缺。 本书适用于软件设计人员阅读。一起来看看 《软件框架设计的艺术》 这本书的介绍吧!