前言
为了满足不同用户的多样性购物需求,过去两年里手机淘宝的业务不断膨胀,已经从单一的购物工具成为了购物内容平台。在业务快速增长的同时,也带来一些副作用,很多操作环节和页面因为承载功能太多,展示速度变慢,用户等待时间变长,性能优化势在必行。
通过大半年的摸索总结,从实践中我们得出了App性能优化的七大策略:
1.建立监控体系,善用分析工具
2.完善网络基础建设,不断调优
3.利用本地缓存,建立离线化
4.任务分级,合理并行,主线程移除多余操作
5.业务模块懒加载
6.优化页面结构和层次
7.做好图片下载缓存工作
首先是监控体系的建立:根据手机淘宝用户的操作习惯,我们对购物主要链路进行了划分,分为启动,首页,搜索,购物车,下单,支付环节,订单查看等七个环节,每个步骤和模块都做到监控,以量化数据为指导,有的放矢进行优化。
对于手机淘宝购物主要链路的七个业务环节,本文将抽取三个做重点介绍:启动、首页、购物车;然后针对网络调优,图片下载缓存两个基础能力的优化和如何善用分析工具来做详细介绍。
一.启动优化
通过线下分析工具,线上灰度数据和代码review,发现启动慢主要有三个原因:
-
提前引入了许多模块的初始化;
-
在主线程,存在耗时的非必要阻塞操作;
-
部分锁操作,导致主线程wait时间较长。
由此制定出优化的方案:
1)采用优化策略3--利用本地缓存,建立离线化
从用户点击图标到首页第一次展示,整个过程执行严格的无网策略,不做任何网络交互,所有数据通过缓存或预置的方式获取。
通过将耗时的网络IO操作后移,减少了时间开销。
2)采用优化策略4--任务分级,合理并行
将启动中的所有任务进行梳理和分级,根据级别来调整执行对应任务的时机:
-
一级:阻塞启动的任务,比如基础SDK的初始化,首页页面的创建等。
-
二级:可延迟到首页加载成功后再执行的任务,比如自动登录,配置信息和运营数据拉取等。
启动时只执行一级任务,二级任务延迟到启动完成后串行执行,一级任务必须没有锁操作,保证主线程不会被阻塞。
3)采用优化策略5--懒加载
优化前,启动过程存在很多业务的初始化操作;优化后,采用懒加载策略,真正使用时才进行初始化加载,同时懒加载机制可以结合缓存或预置数据的方式来达到更好的效果。
比如在手机淘宝的五个TAB中,优化前,启动时会将五个TAB对应的页面全部创建出来,而实际上只有首页可见,其他页面的创建,包括页面中业务逻辑的请求发送,都会对整个启动造成性能损耗。使用懒加载的方式优化后,启动只创建可见页面即首页,其它页面只在用户点击对应TAB时才进行创建显示,此优化一举减少了近0.5秒的耗时。
在上述三个主要策略的优化指导下,手机淘宝的启动流程图调整为如图1所示,主线程中启动阶段只保留了必要的初始化,其他非必要的操作都被懒加载或者异步化。
二.首页优化
作为曝光量最高的页面,快速打开,稳定可用,及时更新是三大目标。
在首页,展示的内容大致分为四类:
-
二级页面的入口,图标,标题和位置相对固定;
-
顶部的轮播图,作为不同运营模块或活动的入口;
-
根据用户身份运算出来的推荐商品,店铺
-
顶部的消息盒子入口,带未读消息数字图标。
不同的内容采用不同的处理策略:
对于第1,2类内容,采用策略3,即进行本地cache化工作,将入口文字图标缓存在本地。渲染时优先展示上次cache内容,即使遇到无网的情况,首页的整体框架页面和cache过的图片和文字都能绘制出来。
本地cache通过时间戳来保证版本内容的及时更新。
在今年双十二的活动中,手机淘宝率先推出了图标运营,通过更新云端上的配置,待客户端上前一次cache过期就自然过渡到此版本,给用户带来了惊喜。
对于第3类内容,采用策略6,优化页面结构和层次:推荐商品放在页面最下部,默认不显示,当用户滚动上滑时做拉取绘制,避免页面一次拉取数据内容过多。
对于第4类内容则采用策略5,即懒加载,在首屏其他内容完成基础绘制后,才调用接口拉取未读消息数量。
此外,首页数据量相对较大,这些数据的加载,解析,拼装都是非常耗时的,而目前主流手机都是多核处理器,可以充分利用多核优势,将这些耗时操作放在异步线程里完成,保证主线程正常调度。当数据准备完毕,才在主线程中进行UI的渲染更新,从而保障了主线程的流畅性。
三.购物车优化
购物车已经成为用户的“第二收藏夹”,用户通过多终端(PC,手机)不断更新购物车的内容。在本地建立缓存保存数据,及时展示给用户是提升打开购物车页面的必然手段。
但由于优惠规则和总价计算必须在服务端完成,客户端在更新购物车时,不但要拉取商品数量的变化,也要拉取总价的变化。
以往是采用主动刷新时全量更新的简单方法,现在优化为差量更新,不但流量减少,更有效地提升了拉取和刷新展示的速度。
四.网络优化
上面从三个业务环节讲述了优化策略,现在从基础服务角度来描述优化手段。主干互联网传输消耗的时间,主要包括三部分:DNS查找、TCP/TLS握手、数据传输。如何降低这三部分的耗时是网优的重要手段:
-
IP直连:
自行实现承载在HTTP协议上的DNS解析协议。能节省DNS查找时间,规避传统DNS可能存在的劫持问题。2013年业界曾经发生过一次公网DNS Server被攻击,手机淘宝未受任何影响。
-
建立长连接:
通过SPDY实现,减少TCP/TLS握手,降低建立连接成本。对于从CDN下载图片速度帮助很大。
-
域名收敛:
收敛域名至公司的主力CDN域名。将请求集中在少数几个域名下,以提高长连接的复用率.
-
TCP调优:
无线网络特点是丢包率高、RT长,针对此特点可以做针对性的TCP调优。
-
报文缩减:
逐步由JSON协议向类PB协议转换。
五.图片方面优化
图片是电商App使用场景中最多的元素,如何节省流量,快速渲染是电商App都非常关注的。
在建立长连接,域名收敛等网络优化之外,手机淘宝还建立图片的分级机制:按分辨率,质量,锐化,格式四个纬度,对同一张图片生成了不同组合的衍生文件。
设置了一系列匹配规则,针对不同的屏幕,机型,处理能力和网络环境,配置出合适的图片大小质量,保证图片既节省流量又满足用户视觉体验。
其中一个经验是当锐化程度高时,即使图片质量较低,图片色彩清晰度也都能让用户满意。
其中一个经验是当锐化程度高时,即使图片质量较低,图片色彩清晰度也都能让用户满意。
六.工欲善其事必先利其器
在整个手淘的启动优化过程中,系统的工具帮了我们很大的忙,Android的主要是自带的 TraceView工具,IOS 主要是Instruments自带的Time Profiler,System Trace等工具,它们都是数据采集和分析工具,主要用于分析应用程序中的hotspot,都非常强大。工具的具体使用方法不在本文论述范围内,但是这些工具都提供了程序中的所有线程使用状况,而且线程中的每一次的调用都可以看到具体的堆栈信息、耗时等详细信息。通过对这些调用的分析,就可以找到启动过程中相对耗时的调用。分析出具体的瓶颈点以后,就可以有针对性地进行具体优化了。
比如 手淘Android启动阶段以前有一个加密存储的模块,它会调用系统的SecretKeyFactory.getInstance()方法来生成加密的 key,我们是通过 TraceView 才发现这个函数调用会耗时300ms 以上,通过 TraceView看里面的调用堆栈发现它里面存在锁操作,所以比较耗时,找到这个瓶颈点之后,手淘Android调研了多种加密存储方式,最后换了一种比较轻量的加密存储模块,优化了该瓶颈点。
最后总结七大原则:
1.善用性能分析工具,建立监控体系
2.做好网络基础建设和网络调优
3.离线化,本地缓存
4.懒加载
5.任务分级,合理并行
6.在主线程移除多余操作
7.简化合并复杂视图
历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)
作者|手淘用户体验提升项目组
出品|阿里巴巴新零售淘系技术部
导读:自阿里在11年提出 All in 无线之后,手淘慢慢成长为承载业务最多,体量巨大的航母级移动端应用。与之相应的,手淘离轻量,快速,敏捷这些关键词却越来越远,启动慢,使用卡逐步成为用户使用过程中的主要体验问题。为此,手淘的技术团队启动了极速版项目,其目标是还给用户一个更加流畅的淘宝。整个项目历时近1年,横跨几十个团队,经历了数百次的数据实验,涉及代码上百万行,最终使得手淘的性能有一个质的飞跃。
下面,我们一起来看手淘团队在性能优化过程中的一些思考和实践。
启动框架的思考
▐ 启动框架在手淘的意义
启动性能,是用户在使用APP 过程中的第一感观,可见是相当重要的。相信很多同学都能说出一些常规的手段,比如只加载必要的模块,延迟加载等。从大的策略上说,是没有问题的,也是手淘做启动性能优化的一个方向,也得了一些效果,但仍存在一些问题。
前面提到,手淘承载的业务非常多,为了更好支撑业务,使用了动态化技术及一些非常复杂的策略,就首页本身依赖的模块和任务就非常多,相互关系也复杂,只加载必要任务,仍然是一笔不小的开销。于是,为了更加极致的优化,我们不得不继续思考性能优化的本质。
通常我们为了更快的达到目标,把与目标无关的事情,提到完成目标之后,通过减少执行代码从而减少执行时间的方式,叫着软优化。相对的,对于提升系统的吞吐效率,对于相同的代码用更少的执行时间完成,叫着硬优化。硬优化是面向硬件资源,包括CPU,内存,网络,磁盘 IO等的调度,减少等待时间,最大化利用硬件资源,保持系统负载在合理范围内。
这次优化我们有一个大的原则,要求基本不能影响业务需求,也就是要在不减任何业务代码的情况下进行优化。
对手淘而言,因为启动包含很多基础 SDK,SDK 的初始化有着一定的先后顺序;业务 SDK 又是围绕着多个基础 SDK 建立的。
那么如何保证这些 SDK 在正确的阶段、按照正确的依赖顺序、高效地初始化?怎么合理调度任务,才不至于让系统负载过高?如何最大化利用设备的性能,承接越来越多的业务?
其实启动框架就是一个任务调度系统,是手淘启动的“大管家”。各个业务模块我们称之为启动任务,管家要做的事情就是把它们的关系梳理得明明白白,有条不紊,合理安排位置、调度时间,同时提升硬件资源的利用率。
▐ 启动框架的思路
总结下来无非就是两点:一是 如何保证时序 ;二是 怎么控制拥塞,提高吞吐,充实不瞎忙。我们先看一组实验数据,在并发下面的 IO 性能。
启动任务 | 高并发 IO耗时 | 低并发 IO 耗时 |
InitPH | 331ms | 139ms |
InitCM | 315ms | 179ms |
InitSM | 270ms | 182ms |
InitPA | 245ms | 66ms |
InitAV | 159ms | 64ms |
由表上的数据可以看到降低 IO 的并发,整体的执行时间大幅降低。
我们借鉴了很多任务调度系统。比如谷歌新出的 WorkManager,再比如 Spark 的 DAGScheduler。
从 Spark 的 DAGScheduler 中领悟到它的核心思想,面向阶段调度(Stage-Oriented Scheduler):把应用划分成一个个的阶段(Stage),再把任务(Task)安排到各个阶段中去,任务的编排则是通过构建 有向无环图(DAG),把任务依赖通过图的方式梳理得 井井有条。因为它分阶段执行,先集中资源把阶段一搞定,再齐心协力去执行阶段二,这样即能控制拥塞,又能保证时序,还能并发执行,让设备性能尽可能得到发挥,岂不美哉:
▐ 阶段划分
什么阶段做什么事情,前面打基础,只有夯实了基础,后期才能顺理成章。我们把手淘的启动阶段做了以下细分:
启动流程如下:
可以看到:整个流程很清晰,分阶段、多任务并发执行,不存在老框架下几条初始化链路交错在一起的情况,首页那一块位置不受干扰。
▐ 任务编排
无锁化,得益于“有向无环图”,通过构建任务间的依赖,启动框架严格按照图的顺序执行各项 SDK 的初始化,真正做到时序可预期,原本需要靠锁来保证状态同步的,现在转变成了“无锁化”。
开箱即用,对一项启动任务而言,极致的体验应该是:无论我身处何处,所依赖的基础库、中间件们都应该“开箱即用”。
多任务并发,早期任务少、业务简单,基础尚未成型,单流水线作业就够了;但随着业务日益膨胀,基础只会越来越厚,就必须多流水线齐头并进,协同作业,提高吞吐率。
无锁化的好处:
-
代码执行效率高,SDK 的初始化基本上都需要考虑多线程安全问题,如果从时序上能保证顺序,也即不存在竞争,等同于“无锁”;
-
减少 ANR,降低卡顿故障,比如我们之前查的网络库在 vivo y85a 上启动长时卡顿达 1s 以上的问题,如果我们能正确梳理各项 SDK 之间的依赖,类似的问题就可以避免了;
任务编排是重中之重,是决定成败的重要因素:依赖梳理不当,执行效率上不去。
▐ 任务调度
要支持多任务并发,那肯定绕不开线程池,既然要用到线程池,那线程池大小需要一个比较合理的设置。
★ 核心思想
阶段(Stage)+ 线程池(ThreadPool Executor)
★ 线程池大小
因为我们的 SDK 大多涉及到 so 的加载、文件的读写,线程等待时间占比比较高,所以我采用了一个通用的估算方法:2N + 1,N 是 CPU 个数。
★ 线程优先级
把先于首页(落地页)的阶段的线程优先级都调高一些,以求得到优先调度,尽快执行;进入 idle 阶段后,性质原因,慢任务居多,调整线程池大小,同时把优先级调低,做到尽量不干扰 UI 主线程,在后台慢慢跑。
实际运行的 DAG 图
优化效果:
启动环境是应用中最为复杂环节,任务多,负载重,资源争抢下,不管是 CPU ,内存,网络,IO都有可能成为瓶颈,启动框架的引入,让我们在面对这些挑战时,有了一个明确的方向,给出一个稍微系统化的解。当然,系统资源调度优化是个非常深刻的课题,加上手机各种硬件配置多样性,我们在这个领域仍然面临更大的挑战,当前只是一个开始。
网络的链路优化
▐ 问题定义
可以看到,手淘首次安装冷启动30s内,网络请求数达到 400上下。非首次冷启动30s内,请求数相比首装冷启减少,但依然在 100+。启动场景下,存在着以下几个问题:
-
请求过多:重复请求、请求滥发情况严重。
-
数据量过大:资源文件的下载占流量的80%以上。
-
业务方请求时机不合理:非首页&启动必要请求需延后。
过量的请求集中在启动阶段导致原本就有限的网络带宽和端上处理能力更加严峻。
▐ 深入剖析
更深入些来看,我们尝试以一个请求的全链路出发来看,探寻每个请求真正耗时的点在哪里。
为何是全链路请求分析?
一直以来,性能埋点方案均为独立的模块,更多针对各个SDK关注自身的请求性能。但是从一个数据或图片请求链路上来看,一个完整的请求往往跨越多个核心SDK。特定场景内(启动),每个环节的耗时都会牵一发而动全身影响请求的性能,剥离完整请求和特定场景单纯从某个中间模块看整体性能往往不能发现最根本的问题。就现状而言,独立SDK的埋点方案显然不能够把一个请求串联起来,以一个场景切入做更精准的分析。因此,亟需从特定场景下请求的完整链路角度来分析,以揪出各个阶段的耗时请求。
对于请求整个链路,我们把请求的关键耗时阶段抽象为以下几点。
-
发送处理:本地处理耗时,包含数据或图片库处理,网络库处理耗时
-
网络库耗时:纯网络传输时间
-
返回处理:包括网络库响应处理回调和上层图片库的处理(json解析/图片解码)操作
-
回调消息-回调执行:任务dispatch到主线程并开始消费的耗时,反映主线程的流畅程度
-
回调执行-回调返回:业务在回调内部执行处理的耗时
从首次安装冷启动的场景切入,我们线下针对启动30s内的请求在图片库、网络库内部进行了日志打点统计,以获取请求全链路各个关键阶段的耗时情况。如下图:
分析启动请求耗时阶段,针对每个阶段得出以下结论和优化点:
-
发送处理阶段:网络库bindService影响前x个请求,图片并发限制图片库线程排队。
-
网络耗时:部分请求响应size大,包括 SO文件,Cache资源,图片原图大尺寸等。
-
返回处理:个别数据网关请求json串复杂解析严重耗时(3s),且历史线程排队设计不合适。
-
上屏阻塞:回调UI线程被阻,反映主线程卡顿严重。高端机达1s,低端机恶化达3s以上。
-
回调阻塞:部分业务回调执行耗时,阻塞主线程或回调线程。
▐ 请求治理
对于应用启动,尽快地完成启动展现可交互页面给用户是第一要务。有限的网络带宽和端上处理能力,意味着过多的请求势必会导致资源争抢更加严重。首页无关&不合理请求很大程度上回阻塞启动主链路请求的响应耗时。
针对启动阶段请求,我们开展了请求治理行动,每个请求责任到人,横向推动业务方评估请求的必要性。主要从以下几个方面展开:
-
多次重复的请求,业务方务必收敛请求次数,减少非必须请求。
-
数据大的请求如资源文件、so文件,非启动必须统一延后或取消。
-
业务方回调执行阻塞主线程耗时过长整改。我们知道,肉眼可见流畅运行,需要运行60帧/秒, 意味着每帧的处理时间不超过16ms。针对主线程执行回调超过16ms的业务方,推动主线程执行优化。
-
协议json串过于复杂导致解析耗时严重,网络并发线程数有限,解析耗时过长意味着请求长时间占用MTOP线程影响其他关键请求执行。推动业务方handler注入使用自己的线程解析或简化json串。
▐ 结果
优化后的数据看,首装冷启动请求减少300+,请求数优化至30个左右,整体减少60%,带宽流量减少75%。
请求数的减少带来的是更快的首页展现。 首屏图片渲染耗时中高端机上从4.2s减少至2.1s,低端机上从12.7s减少至7.8s。
效果评估
所有的优化结果,都应该是客观的,稳定的,可重复的。为此,我们专门搭建了一套优化效果的自动化评估方案。当然,我们首先要定义,我们的结果数据怎么体现。
▐ 数据指标定义
第一个维度-指标:定义合适的数据指标,结合业务场景,多方位评估启动和页面的用户体感性能。
1、数据指标:经过手淘用户体验提升项目组的讨论,定义了如下指标来衡量用户的体感数据, 之前大部分的响应时长只规定了渲染完成时长, 可以反映应用的部分性能情况,但是渲染完成后用户多久可以对应用进行操作, 是否有卡顿,无法通过该指标观察到。因此新增了两个指标,可交互时长和可流畅交互时长,可以比较直观的反映用户最早可以对应用进行交互的时间。
-
渲染时长:点击进入页面,页面80%以上内容渲染完毕。
渲染中 | 渲染完毕 |
|
|
-
可交互时长:页面渲染完毕后立即开始滑屏操作,页面能响应滑屏事件那一刻即为可交互时长。
渲染完毕 | 可交互 |
|
|
-
可流畅交互时长:页面进入可交互状态后,匀速连续上下滑动屏幕,直至屏幕上下滚动跟手势同步次数超过3次以上即可判断为可流畅交互。
可交互 | 可流畅交互 |
|
|
|
|
2、业务场景:不论是应用启动还是在应用中打开页面都会有不同的业务场景。只有从多个不同的场景下对应用进行多角度评估, 获得的数据才能够全面反映用户在不同情况下的真实感受。
-
启动:可按照不同的安装方式、启动方式、启动发起方分为不同的启动业务场景。
-
页面打开:可按照不同的页面进入方式氛围不同的页面响应时间业务场景。
第二个维度-自动化:自动化手段可以支撑实现体感数据的高效采集和3个用户体感数据的准确计算。
第三个维度-流程:通过指标定义, 以及对应指标数据的自动化采集,我们可以在发布前、发布中、发布后的全研发流程中对应用的用户体感性能进行评估。
▐ 自动化测试方案
人工测试方案,虽然能达到了我们的目标,可以较为准确的反应应用的用户体感性能,但是存在两个问题:效率较低,产出数据需要时间比较长 ;不同人的操作可能不一致,造成数据采集标准不一致;因此我们需要把人工测试方案转化为自动化方案,达到高效、稳定产出数据的目的。
▐ 关键点识别
主要思路是从视频中找出来渲染完成、可交互完成、可流畅交互完成几个节点的关键特征,通过程序算法去进行识别。
考虑过的几种识别算法:
-
算法1: 相邻两帧变化趋于平稳,无变化时,认为渲染完成, 经过实验后发现对于存在动画的页面,该算法的结果会比实际情况要长。
-
算法2: 使用参考帧概念,将业务页面渲染完成的图片作为参考, 比较每一帧与该参照图的相似度, 当相似度>=门限时,认为启动完成。该算法的缺点是对于一些变化频繁的页面, 比如首页更换了banner图或氛围,变了投放元素,原来的参考图就无效了,需要进行更换且更换成本较高。
-
算法3: 检测关键特征,如8个icon, 5个tab都出现认为启动完成。这个算法的难点在于不同页面的特征提取,需要比较多的调整工作,而且在不同分辨率的手机上特征出现情况可能不一样, 还需要根据屏幕适配。
-
算法4:通过OCR提取图片中的文字信息作为关键特征。该算法的优势:1. 在于应用页面上基本都是有文字的, OCR也可以识别到图片上的文字, 文字出现则图片加载完成, 和用户体感是一致的;2. 文字作为特征,过滤掉了很多图片特征可能带来的噪声, 减少了算法调试的工作量;另外阿里集团内有非常成熟和优秀的OCR服务——读光,文档识别率超过99.7%, 使用水滴平台封装的OCR服务,可以快速接入和使用。最终的识别方案就是基于OCR识别来进行的,以下介绍下基于OCR的识别方案的改进过程。
通过观察视频,我们可以发现这样一个规律, 中转页, 开始进入页面, 页面渲染完成,页面可滑动这几种状态下, OCR字符串长度是不一样的,并且由于操作的固定性(进入页面,来回滑动)这个曲线存在一定的模式,基本可以分为两种, 一种是可滑动后滑动到的页面字数比渲染完成要多, 另一种是可滑动后滑动到的页面字数比渲染完成要少。
进入页面前的中转页面,这个页面总是字数较少的。
页面渲染完成, 页面元素比较丰富,字数也比较多。
页面可交互,字数相对渲染完成时的情况要多一点,或者少一点,存在两种不同的曲线模式。
识别到上面的模式之后,我们的识别算法也基本确定下来。
基于上面的自动化方案,从自动化驱动到自动识别渲染、可交互、可流畅交互时长,几乎不需要人的参与。
-
可以动态的适应不同业务场景,对手淘主要业务场景进行一次评测从1天1人力减少到了2小时0人力,并且可以自动产出版本报表。
-
在研发进行性能优化的阶段, 每日自动产出各业务用户体验时长数据, 为优化提供决策参考。某个优化是否要集成,集成之前也需要先产出下数据,评估其价值, 如果提升不明显风险较高, 则放弃该优化。
小结
性能优化是老生常谈的问题,说简单也不简单,需要一个系统化的视角来分析和解决。找问题,不仅仅是要看到某段区间慢了,更要去深入分析,为什么慢了。trace 上一段方法执行时间过长,有可能是本身逻辑复杂,或是有 IO 等耗时操作,也有可能是因为 CPU 调度,IO 竞争等原因,因此,在分析上一定要能系统化进行全局思考。
工具是性能优化利器,除了使用像 trace 及 systrace,过渡绘制等常见的工具,还用到一些 linux 命令,直观的观察系统内各进程及线程的运行情况,当前系统负载情况等,当然,原生工具还是有一些局限性,特别是像IO 的读写分析这样特别领域,还是显得有些力不从心,为此在优化过程中我们也沉淀了不少的工具,比如细粒度方法级耗时监控,及IO 读写的监控,有了合适的工具,能极大的提高效率。
整个优化过程中,发现问题不难,难的是对解决方案技术决策。这次优化过程中,我们发现比较大的一个问题是代码规模迅速膨胀,功能堆砌式累积,启动整个系统运行时的效率偏低,当前手淘的架构不能满足对极致体验的要求。因此我们的主要手段是对启动框架重新定义,包括前面提到的对任务进行按序编排,对网络资源的合理使用,减少排队情况,以此提升系统的吞吐率。优化过程除了拼智力,还得拼体力。手淘的业务规模十分复杂,上百个启动任务需要重新 reivew,梳理特性编排顺序,还有数百个网络请求的清理,用阿里的土话说,脑力,心力,体力,缺一不可。
一般说在缓存的使用场景上,通常是借助于 LRU 算法或是其变种,提升 cache 的命中率。智能化预加载,是我们在优化过程进一步尝试,希望在命中率与下载缓存数上寻找到一个最适合的奇点。这次针对 H5 的缓存优化,我们尝试使用了机器学习的方式,通过统计用户的使用习惯及 H5 的访问频次来设计H5的缓存下载,在大辐降低下载缓存数量的同时,又保证了命中率的基本稳定。
无人化验证优化数据,在整个性能优化过程是非常重要的一环,能够快速验证优化是否有效。除了性能本身的收益之外,我们更需要关注优化对业务的影响。对于手淘来说,要在前进中,重构架构无疑是相当于飞行中更换引擎,任何不经意一句代码,都可能对业务造成严重的影响。而 AB 实验,在优化过程中扮演着非常关键的决策的角色,我们的优化项是否能真正上线,一切以 AB 实验的结果为依据。借助于AB 实验,隔离掉无关因素,认真核对实验中的数据是否存在不可预期的变化,及时控制其中风险。
521 性能优化项目揭秘
亿万用户都会在双十一这一天打开手机淘宝,高兴地在会场页面不断浏览,面对琳琅满目的商品图片,抢着添加购物车,下单付款。为了让用户更顺畅更方便地实现这一切,做到“如丝般顺滑”,双十一前夕手机淘宝成立了“521”(我爱你)性能优化项目,在日常优化基础之上进行三个方面的专项优化攻关,分别是:
1)H5页面的一秒法则;
2)启动时间和页面帧率提升20%;
3)Android内存占用降低50%。
优化过程中遇到的困难,思考后找寻的方案,实施后提取的经验将会在下面这篇文章中详细地介绍给大家。
第一章 一秒法则的实现
“1S法则”是面向Web侧,H5链路上加载性能和体验方向上的一个指标,具体指:
-
“强网”(4G/WIFI)下,1秒完全完成页面加载,包括首屏资源,可看亦可用;
-
3G下1秒完成首包的返回
-
2G下1秒完成建连。
在移动网络环境下,http请求和资源加载与有线网络或者PC时代相比有着本质区别,尤其是在2G/3G网络下,往往一个资源请求建连的时间都会是整个Request-Response流程里面的大头,一些小资源上拖累效应尤其明显。例如一个1k的图片,即使在10k/s 的极慢网速下,理论上0.1秒可下载完毕,但由于建立连接的巨大消耗,这样一个请求会要耗上好几秒。
仅仅“建连”这一个点,就能说明移动时代的Web侧性能优化和PC时代目标和方式都相去甚远,要求我们必须从更底层,更细致的去抓,才能取得看起来相对有效的结果。
15年初的性能情况
平均LoadTime-WIFI | 平均LoadTime - 4G | 平均LoadTime - 2G |
3.35s | 3.84s | 14.34s |
可以看到优化前,平均时间很难接近1秒。为了实现优化目标,在技术和实施抓手层面,由底层往上,做了四方面事情:
-
网络节点:HttpDNS优化
-
建连复用:SSL化,SPDY建连高复用
-
容器层面:离线化和预加载方案
-
前端组件:请求控制,域名收敛,图片库,前端性能CheckList
网络节点:HttpDNS优化
DNS解析想必大家都知道,在传统PC时代DNS Lookup基本在几十ms内。而我们通过大量的数据采集和真实网络抓包分析(存在DNS解析的请求),DNS的消耗相当可观,2G网络大量5-10s,3G网络平均也要3-5s。
针对这种情况,手淘开发了一套HttpDNS-面向无线端的域名解析服务,与传统走UDP协议的DNS不同,HttpDNS基于HTTP协议。基于HTTP的域名解析,减少域名解析部分的时间并解决DNS劫持的问题。
手淘HttpDNS服务在启动的时候就会对白名单的域名进行域名解析,返回对应服务的最近IP(各运营商),端口号,协议类型,心跳等信息。
优点
1.防止域名劫持
传统DNS由Local DNS解析域名,不同运营商的Local DNS有不同的策略,某些Local DNS可能会劫持特定的域名。采用HttpDNS能够绕过Local DNS,避免被劫持;另外,HttpDNS的解析结果包含HMAC校验,也能够防止解析结果被中间网络设备篡改。
2.更精准的调度
对域名解析而言,尤其是CDN域名,解析得到的IP应该更靠近客户端的地区和运营商,这样才能有更快的网络访问速度。然而,由于运营商策略的多样性,其推送的Local DNS可能和客户端不在同一个地区,这时得到的解析结果可能不是最优的。HttpDNS能够得到客户端的出口网关IP,从而能够更准确地判断客户端的地区和运营商,得到更精准的解析结果。
3.更小的解析延迟和波动
在2G/3G这种移动网络下,DNS解析的延迟和波动都比较大。就单次解析请求而言,HttpDNS不会比传统的DNS更快,但通过HttpDNS客户端SDK的配合,总体而言,能够显著降低解析延迟和波动。HttpDNS客户端SDK有几个特性:预解析、多域名解析、TTL缓存和异步请求。
4.额外的域名相关信息
传统DNS的解析结果只有ip,HttpDNS的解析结果采用JSON格式,除了ip外,还支持其它域名相关的信息,比如端口、spdy协议等。利用这些额外的信息,APP可以启用或停止某个功能,甚至利用HttpDNS来做灰度发布,通过HttpDNS控制灰度的比例。
建连复用:SSL化,SPDY建连高复用
出于安全目的,淘宝实现了全站SSL化。本身和H5链路性能优化没有直接的关系,但是从数据层面看,SSL化之后的资源加载耗时都会略优于普通的Http连接。
有读者会有疑惑,SSL化之后每个域名首次请求会额外增加一个“SSL握手”的时间,DNS建连也会比http的状态下要长,这是不可避免的,但是为什么一次完整的RequestRespone 流程耗时会比http状态下短呢?
合理的解释是:SSL化之后,SPDY可以默认开启,SPDY协议下的传输效率和建连复用效益将最大化。SPDY协议下,资源并发请求数将不再受浏览器webview的并发请求数量限制,并发100+都是可能的。
同时,在保证了域名收敛之后,同样域名下的资源请求将可以完全复用第一次的DNS建连和SSL握手,所以,仅在第一次消耗的时间完全可以被SPDY后续带来的资源传输效率,并发能力,以及连接复用度带来的收益补回来。甚至理论上,越复杂的页面,资源越多的情况,SSL化+SPDY之后在性能上带来的收益越大。
容器层面:离线化和预加载方案
收益最明显,实现中遇到困难最多的就是离线化或者说资源预加载的方案。预加载方案是为了在用户访问H5之前,将页面静态资源(HTML/JS/CSS/IMG...)打包预加载到客户端;用户访问H5时,将网络IO拦截并替换为本地文件IO;从而实现H5加载性能的大幅度提升。
手淘实现要比上面的通用示意图复杂:因为Android和iOS安装包已经很大,所以预加载Zip包(以下简称“包”)都是从服务器端下载到客户端;本地需要记录整体包状态,并在合适的时机与服务器通信并交换状态信息。在包发布更新的过程中要注意,本地版本和服务端最新包之间的差量同步,必要的网络判断,WiFi下才下载等。
面对亿级UV,并且在服务器资源很有限的情况下搞定这个流程,需要借助CDN来扛住压力,实际上CDN扛住了约98%的流量。需要注意的是预加载实际上也是一种缓存,更新比H5稍慢一些,主要受几个因素影响:推送到达率(用户是否在线,用户所在网络质量),总控,服务端策略等,所以需要通过推拉结合的触发策略并优化下载包的体积(增量包)来提升到达率。
除了优化到达率,手淘还做了url解CDN Combo后再映射的优化工作,若 URL 是 Combo URL,那么会对 URL 解 Combo,解析出其中包含的资源。然后尝试从本地读取包含的资源,如果所有资源都在本地存在,那么将本地文件内容拼装为一份完整文件并返回;否则 URL 直接走线上,不做任何操作。
提升到达率和解CDN Combo再映射,这两个容器侧对于离线化方案的优化对于本次H5链路上整体性能的提升有着至关重要的意义。
前端组件:请求控制,域名收敛,图片库,前端性能CheckList
严格执行性能方面的CheckList,主要有三个点:
-
图片资源域名全部收敛到gw.alicdn.com;
-
前端图片库根据强弱网和设备分辨率做适配;
-
首屏数据合并请求为一个。
在执行中,性能的检查和校验一定要纳入到发布阶段,否则就不是一个合理的流程。性能的工具和校验一定应该是工程化,研发流程里面的一部分,才能够保障性能自动化,低成本,不退化。
通过以上优化方案,H5页面的平均Loadtime在Wifi,4G下均如期进入1秒,3G和2G也有80%多达成1s法则的目标。
第二章 启动时间和页面帧率20%的提升
很多App都会遇到以下几个常见的性能问题:启动速度慢;界面跳转慢;事件响应慢;滑动和动画卡顿。
手机淘宝也不例外。我们分为两部分来做,第一部分是启动阶段优化,目的解决启动任务繁多,缺乏管控的问题,减少启动和首页响应时间。第二部分是针对各个界面做优化,提升界面跳转时间和滑动帧率,解决卡顿问题。双十一性能优化目标之一就是将启动时间和页面帧率在原有基础上继续优化提升20%,接下来就从这两部分的优化过程来做一一介绍。
一、启动阶段的优化
手机淘宝作为阿里无线的航母,接入的业务Bundle超过100个,启动初始化任务超过30个,这些任务缺少管控和性能监控。
那么首要任务就是:
建立任务管理机制
所有的初始化任务可以用两个维度来区分:
-
任务必要性:有些任务是应用启动所必需的,比如网络、主容器;有些任务则不是必需的,仅仅实现单个业务功能,甚至是为了业务自身体验和性能而考虑在启动阶段提前执行,其合理性值得推敲。
-
任务独立性:将应用的架构简单分成基础库、中间件、业务三层,这三层中业务层最为庞大,其初始化任务也最多。对于中间件来说,其初始化可能依赖于另外一个中间件。但对于一个独立的业务模块来说,其初始化任务应该也具有独立性,不存在跟其他业务模块依赖关系。
启动阶段任务管理机制包含了如下几方面的内容
-
任务可并行
既然很多初始化任务是独立的,那么并行执行可以提高启动效率。
-
任务可串行
虽然我们期望所有初始化任务都相互独立,但是在实际中不可避免会存在相互依赖的初始化任务。为了支持这种情况,我们设计任务的异步串行机制,这里主要借鉴了前端的Promise思想实现。
-
任务可插拔
面对这么多不同优先级的初始化任务,任何一个出现异常都会导致应用不能启动,给稳定性带来严重挑战。因此我们设计了可插拔机制,当某一项初始化任务出现问题时能够跳过该任务,从而不影响整个应用的启动使用。这里我们根据初始化任务的必要性做了区分,只有非必要的初始化任务才会应用可插拔的特性,这也是为了防止出现不执行一个必要的初始化任务导致应用启动使用出现问题。
-
任务可配置
在ios上通过plist指定每一项启动任务, 其中字段optional表示该项是否是必需的,当之前运行出现crash或者异常时,若值为YES则可以不执行该项。
有了任务管理机制,并引入懒加载的理念,可以持续地合理有效管控启动阶段的各项初始化任务,是大型app必不可少的环节。
检测超时方法,优化主线程
性能优化前,初始化代码都在主线程中执行,为了启动性能已将部分初始化任务放入后台线程或者异步执行。但是随着手淘业务发展和人员变更,还是出现了在主线程中执行很重的初始化任务。为此,在ios实现了一套应用运行时方法耗时检测机制,能够对应用中所有类的方法调用做耗时统计。方便的找到超时的方法调用之后,就可以有针对性的做出修改,或删除或异步化。这种方法调用耗时检测机制同样适用于APP运行过程中,从而找到导致应用卡顿的根本原因,最后做出对应修改。
多线程治理
分析各个模块的线程数量,检查线程池的合理性。通过去掉不必要的线程和线程池,再控制线程池的并发数和优先级。进一步通过框架层的线程池来接管业务方的线程使用,以减少线程太多的问题。
减少IO读写
从自身业务出发,去除若干初始化阶段不必要的文件操作,以及将若干非实时性要求的文件操作延后处理。Android上对于频繁读写数据库和SharedPreference以及文件的模块,通过增加缓存和降低采样率等手段减少对IO的读写。对于SharedPreference进行了专门的优化,减少单个文件的大小,将毫无联系的存储键值分开到不同文件中,并且防止将大数据块存储到SharedPreference中,这样既不利于性能也不利于内存,因为SharedPreference会有额外的一份缓存长期存在。
降级部分功能
例如摇一摇功能,测试发现应用场景不频密,但业务使用了高频率的游戏模式,会耗电及占用主线程时间。对该功能做了降级处理,降低检测频率。同理,对于其他非必须使用但又占据较多资源的模块也都做了适当的降级处理。
热启动时间的缩短
在安卓手机上我们把启动分为两类进行检测和优化:冷启动和热启动。冷启动是程序进程不存在的情况下启动,热启动是指用户将程序切换到后台或者不断按Back键退出程序,实际进程还存在的情况下点击图标运行。
之前安卓手淘在按Back键退出时整个首页Activity销毁了,热启动会经过一个比较长的过程。优化后首页在退出的时候并不销毁Activity,但是会释放图片等主要资源,在下次热启动时就能更快的进入。另外,将手淘欢迎页的界面从其它bundle转移到首页的模块,在进入欢迎页时就开始初始化首页资源,做到更快展示。
在经过一系列的优化后,启动方面已经有了明显的改善,在进入首页的时候不会卡顿,GC次数也减少了一半以上。
二、各个界面的优化
各界面优化我们也是围绕着提高帧率和加快展现而展开的,手淘的几个主链路界面,都是相对比较复杂的,既使用多图,也使用了动态模板的技术。功能越复杂,也越容易产生性能问题,所以常遇到布局复杂、过渡绘制多、Activity主要函数耗时、内容展示慢、界面重新布局(Layout)、GC次数多等问题。
优化GPU的过渡绘制
通过开发者选项的GPU过渡绘制选项检查界面的过渡绘制情况。该优化并不复杂,通过去掉层叠布局中多余的背景设置、图片控件有前景内容的时候不显示背景、界面背景定义到Activity的主题中、减少Drawable的复杂Shape使用等手段就可以基本消除过渡绘制,减少对GPU和CPU的浪费。
优化层级和布局
层级越多,测量和布局的时间就会相应增加,创建硬件列表的时间也会相应增加。有时我们会嵌套很多布局来实现原本只要简单布局就可以实现的功能,有时还会添加一些测试阶段才会使用的布局。通过删除无用的层级,使用Merge标签或者ViewStub标签来优化整个布局性能。比如一些显示错误界面、加载提示框界面等,不是必须显示的这些布局可以使用ViewStub标签来提升性能。
另外要灵活使用布局,并不是层级越多就会性能越差,有时候1层的RelativeLayout会比3层嵌套的LinearLayout实现的性能更糟糕。
除了灵活使用布局,另外我们还通过提前inflate以及在线程中做一些必要的inflate等来提前初始化布局,减少实际显示时候的耗时。对于一些复杂的布局,我们还会自己做复用池,减少inflate带来的性能损耗,特别是在列表中。
加快界面显示
-
可以通过TraceView工具找出主线程的耗时操作和其他耗时的线程并作优化。另外减少主线程的GC停顿,因为即使并行GC,也会对heap加锁,如果主线程请求分配内存的话,也会被挂起,所以尽量避免在主线程分配较多对象和较大的对象,特别是在onDraw等函数中,以减少被挂起的时间。另外可以通过去掉ListView ,ScrollView等控件的EdgeEffect效果,来减少内存分配和加快控件的创建时间。
-
利用本地缓存,主要界面缓存上次的数据,并且配合增量的更新和删除,可以做到数据和服务端同步,这样可以直接展示本地数据,不用等到网络返回数据。
-
减少不必要的数据协议字段,减少名字长度等,并作压缩。还可以通过分页加载数据来加快传输解析时间。因为JSON越大,传输和解析时间也会越久,引发的内存对象分配也会越多。
-
注意线程的优先级,对于占用CPU较多时间的函数,也要判断线程的优先级。
优化动画细节
通过TraceView工具发现,一些Banner轮播广告和文字动画在移出可视区域后,仍然存在定时刷新,不仅耗电也影响帧率。优化措施是在移出可视区域后停止动画轮播。
阻断多余requestLayout
在ListView滑动,广告动画变化等过程中,图片和文字有变化,经常会发现整个界面被重新布局,影响了性能。尤其布局复杂时,测量过程很费时导致明显卡顿。对于大小基本固定的控件和布局例如TextView,ImageView来说,这是多余的损耗。我们可以用自定义控件来阻断,重写方法requestLayout、onSizeChanged,如果大小没有变化就阻断这次请求。对于ViewPager等广告条,可以设置缓存子view的数量为广告的数量。
优化中间件
中间件的代码被上层业务方调用的比较频繁,容易有较多的高频率函数,也容易产生细节上的问题。除了频繁分配对象外,例如类初始化性能,同步锁的额外开销,接口的调用时间,枚举的使用等等都是不能忽视的问题。
减少GC次数
安卓上的GC会引起性能卡顿,必须重点优化。除了第三章会详细介绍对于图片内存引起GC的优化,我们还做了如下工作:
-
减少对象分配,找出不必要的对象分配,如可以使用非包装类型的时候,使用了包装类型;字符串的+号和扩容;Handler.post(Runnable r)等频繁使用。
-
对象的复用,对于频繁分配的对象需要使用复用池。
-
尽早释放无用对象的引用,特别是大对象和集合对象,通过置为NULL,及时回收。
-
防止泄露,除了最基本的文件、流、数据库、网络访问等都要记得关闭以及unRegister自己注册的一些事件外,还要尽量少的使用静态变量和单例。
-
控制finalize方法的使用,在高频率函数中使用重写了finalize的类,会加重GC负担,使得性能上有几倍的差别。
-
合理选择容器,在性能上优先考虑数组,即使我们现在习惯了使用容器,也要注意频繁使用容器在性能上的隐患点:首先是扩容开销, HashMap扩容时重新Hash的开销较大。其次是内存开销,HashMap需要额外的Map.Entry对象分配 ,需要额外内存,也容易产生更多的内存碎片。SparseArray和ArrayList等在内存方面更有优势。再次是遍历,对于实现了RandomAccess接口的容器如ArryList的遍历,不应该使用foreach循环。
-
用工具监控和精雕细琢:在页面滑动过程中,通过Memory Monitor查看内存波动和GC情况,还可通过AlloCation Tracker工具观察内存的分配,发现很多小对象的分配问题。
-
利用Trace For OpenGL工具找出界面上导致硬件加速耗时的点,例如一些圆角图片的处理等。
通过多种工具和手段配合,手淘各个界面性能上有了较大的提高,平均帧率提高了20%,那么内存节省50%又是如何实现的哩,请看下文。
第三章 Android手机内存节省50%
Android上应用出现卡顿的核心原因之一是主线程完成绘制的周期过长引起丢帧。而影响主线程完成绘制时间的主要有两方面,一方面是主线程处于运行状态时需要做的任务太多但CPU资源有限,另外一方面是GC时Suspend时直接挂起了所有线程包括主线程。GC对总体性能的影响在4.x的系统上尤为突出,一部分是单次GC pause总时长,一部分是用户操作过程中GC发生的次数。而决定这两部分的因素就是Dalvik内存分配。那么在手淘这样的大型应用中到底是谁占用了内存大头呢?
谁占用了内存
基于双11前的手淘Android版本,我们在魅蓝note1(4.4 OS)上滑动完首页后,dump出其Dalvik Heap,整体内存占用的分布情况如下图。可以看出,byte数组(a)占用空间最大,绝大多数是用来存放Bitmap的像素数据(Pixel Data)。另外(c)与(d)一起占用了18.4%, byte数组加上Bitmap、BitmapDrawable总共占用了64.4%,成为内存占用的主体。
这也从侧面说明了手淘是以图片为浏览主体内容的大型应用。而往往图片需要较大的内存块,在分配时引起GC的可能性也往往最大。那我们能不能将图片这部分需要的内存移走而不在Dalvik Heap分配呢?如果能,那么不单GC会明显减少,同时Dalvik Heap总大小也会下降50%左右,对整体性能会有显著的提升。
何处安放的Pixel Data
Ashmem即匿名共享内存,使用的核心过程是创建一个/dev/ashmem设备文件,控制反转设置文件的名字和大小,最终把设备符交给mmap就得到了共享内存。在Android系统中Binder进程间通信的实现就是依赖Ashmem完成不同进程间的内存共享。但此处并不利用其共享特性,而是使用它在Native Heap完成内存分配。
图片空间如何才能使用Ashmem,答案在Facebook推出的Fresco中已有提及,那就是解码时的purgeable标记,这样在系统底层解码位图时会走Ashmem空间分配,而非Dalvik Heap空间。这样就解决了像素数据存放由Dalvik到Native的问题了吗?
-
BitmapFactory.Options options = new BitmapFactory.Options();
-
/*
-
* inPurgeable can help avoid big Dalvik heap allocations (from API level 11 onward)
-
*/
-
options.inPurgeable = true;
-
Bitmap bitmap = BitmapFactory.decodeByteArray(inputByteArray, 0, inputLength, options);
小心Bitmap空包弹
事实并非那么简单,最后实际解出来Bitmap没有像素数据(没有到Ashmem分配任何空间),根本没有去完成jpeg或者png解码。此时的Bitmap是个空包弹!它所做的只是把输入的解码前数据拷贝到了native内存,如果把这个Bitmap交给ImageView渲染就糟了,在View.draw()时Bitmap会在主线程进行图片解码。
而且不要天真的以为Bitmap解码一次之后再多次使用都不会引起二次解码,在系统内存紧张时底层可能回收Ashmem里这部分内存。回收后该Bitmap再次渲染时又将在主线程完成一次解码。如果就这样直接使用该机制,性能上无疑雪上加霜。
那么怎样才能避免这个隐形炸弹呢?还好SDK预留了一个C层方法AndroidBitmap_lockPixels。而lockPixels底层完成的工作大致如下图所示。第一步是prepareBitmap完成真正的数据解码,在工作线程调用AndroidBitmap_lockPixels避免了在主线程进行数据解码;第二步是完成对分配出来的Ashmem空间的锁定,这样即使在系统内存紧张时,也不会回收Bitmap像素数据,避免多次解码。
貌似解决了Bitmap渲染的所有问题,但在手淘中则不然。为了兼容低版本系统以及提升webp解码性能,我们使用了自己的解码库libwebp.so,怎样把它解码出来的数据也存放到Ashmem呢?
libwebp借鸡生蛋
如果自有解码库libwebp.so要解码到Ashmem,通过SkBitmap、ashmem_create_region实现一套类似的机制是不太现实的。一方面Skia库的源码编译兼容会存在很大问题,另一方面很多系统层面的核心接口并没有对外。所以实现这点的关键还是要借助系统已经提供的purgeable到Ashmem的机制,借鸡生蛋,稳定性和成本上都能得到保证:
-
依据图片宽高生成空JPEG。
-
走系统解码接口完成Ashmem Bitmap生成。
-
覆写Pixel Data地址在libwebp完成解码。
更进一步,迁移解码前数据
上面谈到的内存迁移都是针对Decoded像素数据的,而Encoded图像数据在解码时会在Dalvik Heap保存一份,解码完成后再释放;Ashmem方式解码时在底层又会拷贝一份到Native内存,这份数据直到整个Bitmap回收时才释放。那能否直接将网络下载的Encoded数据存放到Native内存,省去Dalvik Heap上的开销以及解码时的内存拷贝呢?
的确可以,将网络流数据直接转移到MemoryFile可实现,但遗憾的是真机测试中发现,小米及其他国产“神机”(自改ROM),多线程使用MemoryFile获取fd到BitmapFactory解码,会出现系统死机,怀疑是在并发情况下系统代码级别的死锁造成。手机淘宝放弃了这种方案,改用ByteArrayPool复用池技术来减少Dalvik Heap针对Encoded Image的内存分配,效果也不错。如果应用能接受单线程解码,还是MemoryFile方案更具优势。
是放手的时候了
上文提到Bitmap像素数据存放到Ashmem,有读者可能担心数据回收问题,其实还是由GC来触发Ashmem内存的回收。在Dalvik层如果一个Bitmap已经不被任何地方引用,那么在下一次GC时该Bitmap就会从Ashmem中回收,大致流程示意如下图。
再看内存占用
我们再次在魅蓝note1中dump出首页滑动后的内存,如下图可以看出,原来byte数组(k)大量占用已经不存在了,Bitmap(c)与BitmapDrawable(已不在前14名当中)的占用也急骤下降。应用的总体内存下降近60%。
在双11版本上,针对一些热门机型在搜索结果页不断滚动使用,进行了不同版本的内存占用对比分析,如下图。可以看出,除华为3c和vivo这类系统内存偏小使用上一直受到控制、内存较为紧张的外,大部分机型内存的下降幅度都达到45%以上。
挠走GC之痒
内存下降不是最终目的,最终要将GC对性能的影响降到最低。仍然以魅蓝note1打开首页后滑动到底的内存堆积图来做对比。可以看到旧版本内存占用上升趋势相当明显,一路带有各式“毛刺”直奔70MB,每形成一个毛刺就意味一次GC。而双11版本中,内存只在初期有上升,而后很快下降到21MB左右,后期也显得平滑得多,没有那么多的“毛刺”,就意味着GC发生的次数在明显减少。
旧版本
双11版本
同时使用一些热门机型,针对双十一版本在首页不断滑动,进行前后版本的GC_FOR_ALLOC次数对比。热门机型GC次数下降了4~8倍,效果非常明显。
通过上文描述的各个优化方案,手机淘宝于双十一前在大部分机型上达到了521目标-Android手机内存节省50%,启动时间和页面帧率提升20%,H5页面实现1s法则。
从持续不断的优化中,我们也得到了一套优化的经验闭环,由观察问题现象到分析原因,建立监控,定下量化目标,执行优化方案,验证结果数据再回到观察新问题。每一次闭环只能解决部分问题,只有不断抓住细微的优化点“啃”下去,才能得到螺旋上升的良好结果。
当然,随着手机机型的日益碎片化,程序功能的复杂化多样化,性能调优是没有止境的,在部分低端机和低内存手机上手淘性能问题依然不容乐观。欲穷千里目,还需更上一层楼,接下来我们还会努力通过更多更细致的优化方案来达到“如丝般顺滑”。