主持人:饿了么移动技术部高级ISO工程师高亮亮,他演讲的主题是:新瓶旧酒——换个角度提升APP性能和质量的实践之路”

高亮亮:大家好!今天的话题是旧酒新换个角度提升APP性能和质量。我现在是属于移动技术部系统架构组的工程师,刚加入饿了么的时候做了有一年左右的业务线,主要是商务平台。从1999年开始APM的话题是非常火的,我们移动框架组肯定会针对这一块儿研究我们自己的APM性能平台,也集成CM的构建大平台。在更早之前我是做外文开发的,在座有多少人是做过外文开发的?当时我是W3C组的成员,H5还在测试阶段,很多东西需要改,那时候在做外文阶段。最近有邀请讲这种方向,看很多大家得在讲APM跟平台搭建的问题,我想这个话题不大好找,我最近刚好问在开发APP外文相关的项目,觉得很多东西各个端是共通的。我们APP端能不能借鉴一些东西呢?把之前的老经验带到移动端上做有意思的事情。

第一,移动性能与质量的概述。
第二,所谓的“新”技术概念的介绍,在座的移动人员多一点,这些概念不是特别了解。
第三,讲几点比较有意思的事情,这其中会顺带讲一些困难。刚刚徐老师也讲了他们的困难,刚刚蓝衣服的朋友也问了问题,还有我们饿了么平台的问题。

先说第一部分。性能质量与概述。在饿了么内部有非常多的产品线在跑,在座各位比较熟悉的是饿了么的用户端,它不会出现高峰期的现象,订餐时间都选在中午之前的一到两个小时,量级是非常大的。我们内部还有其他好多业务线,针对与配送人员和商户的客户端,还有供应链,当然还有内部的沟通工具,我们内部会简单地做分析优良中差评做分级。

最早都是以崩溃来算,谁崩溃的多谁差。但后来崩溃在后期不是特别看重的。崩溃率高包括ANR比较多这一类肯定是最差,只能轮为可用的阶段。怎么叫可用呢?就是把问题解决掉达到可用。好用的阶段是怎么修炼达成的呢?除了UPM做指导性的工作,还要做非常深化的业务,我们一个框架部对外一直在输出各种SDK,供内部使用的,这些技术的组件是非常重要的。

根据设备类型,我们除了在跑移动端,当然还有PC,还有外围。不同的特点是不一样的,PC基本上不用跑流量、耗电量的问题。对于移动设备来讲最纠结的就是这两点。还要结合主要的业务场景,因为我们面临的问题,首先是用户端,大家使用的场景自己可以相信一下,打开了用户端点了自己喜欢的菜,选菜的时间可能耽误不是很长时间,迅速下单比较方便。

停留在用户手上的时间是非常短暂的,对于商户端和配送端是开着APP一直在平台,首先它面临充电问题,它不可能随时找点充电。商务平台还有一个问题,APP不是一直停留在界面的,我们有竞品,有美团、百度等。那需要考虑的问题就多了,对配送人员来讲优先考虑的是耗电问题,耗电问题在移动端体现两点。第一点网络,第二点定位,定位是非常耗电的,GPS定位是非常耗电的问题。不停地定位还要提升精度,对物流端的APP是最大的挑战。对商户端考虑的是网络的优化和性能,本身网络环境是相对比较好的,我们主要是提升它的APP到达方面,主要是业务方面的提升。

先讲第一点,可能有经常遇到的回流和重绘问题,这个问题很经典,从最初的页面加载到最后绘制在屏幕上。最需要的注意的点是在右侧Reflow这一块儿,刚刚徐老师讲的重绘问题,最初我们从来没有考虑过回流的问题。我们当时遇到什么问题呢?
回留是在流失布局下,参照元素的布局坐标一旦发生了改变,那所有依赖它的元素都要重排,重新计算布局位置的过程,它是非常消耗UPC的。
重绘是不发生重排的情况下重新布局,现在的GPU都那么强大,性能并不是瓶颈。

下面是我们处理商品有订单的问题,订单当时是检测到有很多用户的投诉,订单改版之后性能特别差,最严重的是IOS9系统,因为它比较特殊,到IOS10系统的问题页表滚动的问题已经解决了。在IOS9系统上掉帧能达到46、48帧,我们经常说60帧,但大多数情况下无法达到60帧,保持在55帧已经是非常好了。列表CPU、内存消耗肯定要重用食物单元的。当时的食视图特别复杂,所有的订单的价格,包括它可能依赖于这个订单来决定自己所展现的位置,最终都要算成绝对的坐标。子视图本身还不固定,每一张单不一定有多少食物,这样存在单个食物单位,不同的单数可能也不一样。可以用单个缓存来解决问题。对缓存高度不成问题,但个别订单还有订单展开的操作,是非常可怕的。商品内容滚动式会产生回流,布局是不停变化的。性能主要是卡在CPU上,CPU在计算的时候是非常慢的。其实它不慢,只是被我们拖慢了。

我们查到它缓存高度的计算,调用的频率非常快,它并不是一个性能障碍。问题出在哪呢?就是你重用的时候不停地拉新订单,高度计算过之后因为你不是重用的单元,会存在回流问题。怎么解决呢?要么就是单张订单拆开,每一个订单菜品的视图是固定的,菜品单元是固定的单元,以它重用的话问题就解决了。但有什么坏处呢?单独用没问题,但是整合可能都会受到影响。最后还是选择了大家一直在推的,这个框架是在2014年就已经有了,Facebook出的。这套框架引进来做了很多的考虑,在国内踩坑的人比较少。

我们当时问了微博的人员,微博说我们直接用底层的框架去画的,那对我们来讲我们很难接受,我们为了一张订单,而且当时在跟美团打战役的时候4分钟都要换掉,你让我们自己每天都在那里画,接受不了的。那就采用ADK,它有不少坑点,它的异步计算,所有的计算CPU的异步计算丢在别的线上去搞,一点出现重绘的问题就返回不再做额外的计算消耗了。它里面的线程控制是非常细腻的,你自己去写多线程做异步计算的框架会遇到哪些问题?是非常复杂的。包括安卓、IOS的绘制线程都在主线程跑的,会存在问题的。

最终我们优化结果还是非常好的,大家可以看到底下CPU跑的使用率。它当然也要做布局计算,但在快速滚动的时候帧率是达到非常满意的情况,基本上接近于60帧,大家以后遇到这方面的问题可以再细聊。

这是前端以前经常用的东西,在滚动页面的时候我可能需要做一些效果,你在滚动的时候我监听一下。但这个频率是非常高的,你在回流里面做监测控制在右下角的事情是非常多的。会出现什么问题呢?因为回流的频率相当高,在很高的频率下不停地设计元素的位置,会导致滚动时的卡顿问题,怎么解决呢?前端有非常有意思的用法,这个用法用了非常经典的计算机概念,它是非常老的用法,在前端大放异彩,我们应用端很少采用,就是节流。

大家直接类比一下,Throttle是什么?就是节流,大家想象一下打开水龙头流的水就是函数,但回调的频率非常高的时候函数的调用频率是不是非常快?因为水流非常多,想减少这种不必要的消耗怎么办?就是把水龙头拧紧,有什么特点呢?当你拧到一定频率的时候是不限定的,是在某一个时间间隔内是固定的间隔频率,就是节流。

防抖技术没有应用,但在外部端是应用于搜索框。它有什么用呢?就像弹簧,我按住这个弹簧的时候这个方法是不调用的,你按住的期间发了很多指令,但因为我压着弹簧这个指令无法执行,什么时候执行呢?当你松手的那一刻,有什么用呢?就类似于搜索框里面当用户不停地输入内容的时候,我一直压着弹簧,它不会调用API。直到到我设计的时间预值松开弹簧,用户调令的API才会发出去,才会响应真实的结果,这对减轻服务端压力是非常有好处的。

我们的用法可能不单单是对业务上的用法,还有是我们一直在开发的APM台。我们有一个数据收集的问题,数据收集的数额非常大,频次也比较快,包括用户的轨迹分析的数据是非常多的。之前也跟携程那边聊过,他们也是遇到这种问题。但携程有一个好处是它们的日活跃量没有那么高,不用大家每天都掏出来看看去哪儿旅游,但饿了么就不一样了。

我们在服务器传送数据的时候如果失败的话,基本上为了保证数据传输过去,对于非实质性数据一定要把它传过去,就存在了自动重试的问题。如果这次失败了,3秒钟之后我重新试,直到把数据上传为止。还有一种是忽略错误类型,当你失败的指令回来以后,对前期来讲也不管,直接重试掉了,根本不关心服务器是什么样的。重试的周期同步之后,同步指的是什么?虽然后端会在服务器之前做F5的负载平衡,对你设备高峰期的时候,前一段517大促的时候突然就并发起来了,这个时候有些服务器就跪掉了。前端就有意思了,有些用户采集数据分析就开始重试,本来大促当天这些东西应该是有开关的,但是这些数据又不会说直接关掉,因为大促的数据对我们来说也是非常重要的。

那怎么办呢?一旦跪掉是一片区域的跪掉,这些用户可能同时连着一台服务器,这台服务器跪掉以后这些用户也就同时跪掉了,他们又固定了服务周期重新重试,这有什么问题?服务器会慢慢尝试起来,突然又一波“丧尸”来了,又把他们打跪掉,就恶性循环,慢慢起来打跪掉……最后解决的时候就是依据节流方案。

第一是指数回退,我第一次是5秒,5秒后我调试一次,如果5秒失败了下一次就不是5秒,重新算时间,加10秒,以次递增。如果基层服务器被挖掉了服务器这边是连不上了,对服务器来讲是不停地避免做无用功。本身服务器就挂掉了何必一次一次那么执着呢?除了指数回退,不停地加到3个小时重试一次,3个小时以后还是从5秒。

在此期间还要做其他的,比如说添加抖动,这个时间是随机抖动的,不是5秒那么准,这10个用户刚好那么凑巧那么有缘都是同一时间提供的,给他设5秒的话是不是又影响到刚才的问题?又是刚才的周期。5秒不一定,随机。额外还有标记重试的问题,到后端处理的时候重试了几次,如果重试次数比较多就要优先重新高重试请求。我把它定位为“黄金”重试节流策略。
刚才也提到了实时查询的问题,我们这边不停地有用户在查订单号,减少网络的请求。还有事件响应节流,比如说你按一个按纽,大促的时候抢东西不停地在点,每点一次都要发一个请求吗?不行。不应该做节流,不一定节流是最好的方案,也有可能用防抖技术。界面渲染节流问题,这个是避免某些操作会导致CPU不停地重绘单元的时候,可以通过这样的手段去避免,这样性能卡顿大家也不是都会遇到的,有概念也好。

接下来渐进增强和优雅降级,对移动端来说用的不是很多,这套设计思想是非常好的,现在谷歌也在处理BWA。有什么用呢?Graceful Degradation不是预先设计,不是预先想到做容错。而是说出现某种情况不停地做减法,最初用于哪里呢?对于外围来讲,可能浏览器的碎片化特别重复,安卓端也有这种问题,碎片化严重问题。你在设计的时候就照着最优的减,当时谷歌的PROM认为是最好的。

就以它为设计,如果它的功能不可用,把这个功能减掉,不让它体现了,反正它也不节省,浏览器太弱了。还有渐进式增强,依赖最赞的浏览器IE6,设计一套基本的功能能在上面用,不停地做架构,直到它表现的非常好。之前有见过暴风影音有做过,安卓端碎片化严重的时候根本跑不起来解码,解不动的时候是自动切的。

我们这边主要是做的什么呢?是推送系统,推送对饿了么来讲是非常重要的,不光要求到达率的问题,更追求的是实质性的问题,为什么?简单讲一个数据。
去年我们做了一个非常重要的系统,最后产品回馈的结果是用户的取消单(退单率)降低了75%,我觉得这样的数据很可怕。为什么呢?因为我们在此之前订单系统是以非常LOW的轮巡的形式不停地拉订单,商户这边上一次轮巡之后突然又来了几十个单,但是商户不知道,这种情况怎么办呢?从最初的5分钟一次轮巡调整到2分钟一次轮巡,没什么用,还是会出现漏单率,到了5分钟之后才能接单,用户这边会很着急,就是一直接取消了,这样的问题出了订单损失。

我们最初在测试到达率的时候也是非常LOW,坐在小屋子里,这边发一条收到了没。这是不科学的方法,现在在找高大上的方法,我们没办法彻底弄清楚到达率之前只能提升我们的内部,我们有限自己去推一套系统。刚刚徐老师也说他们也在尝试过,非常难,对我们来说是难上加难,因为用户很多。建立服务对后台来说是非常大的挑战,但是我们还不得不做。而且不同的业务对于推送实时性的要求都不一样,但存在一定的差异,有不同的系统对用户端来讲并不是活动推送,并不要求那么高的时间线,对于商户来讲非常要求。

这种怎么解决呢?就是利用更优组件,三方作为备选。操作效率会出现问题,操作效率和速度是随着失效部件的增加逐渐下降的。我的设计就是这样的框架,设计是非常简单的,一说大家都懂,先建长连,可控可靠,存在异常就降级。

这套东西有什么用呢?我们在内部测试的时候是测到达率还是可以的,这是因为内部网络环境相对良好,对于一些偏远地区比如说西藏,因为我们会抓到他们的数据,订单解析会有非常大的延迟,那种情况下去统计不是很好统计,只是简单实现数据统计是非常好的,大家以后如果有这方面需要的话也可以考虑一下。我们也要想办法给后端去减少点压力。

下面讲法则。之前我一直在做业务的时候,后来到框架部也是不停地关注性能这一块儿。发现很有意思的东西,首先是零崩溃零错误等于好用。启动时间Main后比Main前重要。二进制大于资源,耗件优化,硬件大于软件。

第一崩溃率和错误的问题当时拿到了性能白皮书看到了当时能评为有限的软件崩溃率IOS是在零到千分之三,安卓是在零到千分之二。当时我也做过好多次的实验,准历史遗留问题。超过千三的十倍了,可以说崩溃率降低的还不错。而且对于商户性的启动量每天是500万次左右,对于用户的平台是千万级别的,去年高峰期的时候订单能下千万单,用户的启动量也是非常可怕的。

这样的概率、这样的崩溃率还不认为它是最好的。因为我们当时用了大量的DP,为了避免崩溃的问题,用DP法则不停地处理问题。出现崩溃就防,不让它崩溃,包括最近知乎上聊的挺热,就上它崩是不是更好的思想思路呢?当时确实是做了大量的工作,每走一步都要慎重又慎重,看看前面给我的数据是不是有效的。最终用户体验并不好,因为好多错误出来了,操作人员接受不到这种反馈,处理的时候也不是很好处理。崩的时候好歹让其他人知道,这里概率要出现异常数据,用户那边的表现是没什么效果。

时网络错误率给的是千八优秀,我们统计下来崩溃率网络错误率已经降低到非常低的概率了,但每天还是会回到大量的投诉。我们也认为网络和崩溃是两点,针对于APP好不好用,这两点做好了只是达到可用的级别。

启动时间Main后比Main前重要。Main前有优化点,Dylib Loading,我觉得不是那么靠谱,在测试冷启热启之前对性能的影响,发现它本来就不稳定,每一次得到的数据偏差非常大,也不是很好做,最后还是关注在Main后。大量的复杂操作大家都有做过,我们应该更多考虑Main后之后做哪些东西。

包体优化是迭代以后不停地加东西,我们每天都加班,做迭代出来的东西虽然感觉速度上没什么区别,但包是明显在增大,上半年和下半年的区别。我们到底做了什么才上包成倍地扩增呢?我们本身的平台也支持帮你分析包,每次会自动地帮你做静态分析、资源分析的操作。

分析完以后发现我们公司的十几款都是这样的,基本上都是二进制的体验,剩下的资源基本上能优化的都优化了,再优化品质就降低到不能看了。对于比重问题当时也不停地在看,发现点在哪里呢?就是因为移动框架部出的SDK加到APP里面以后体积明显变大了。我们发现问题出在哪呢?我们本身出的APM平台,有什么问题呢?每次形成基础文件仔细看里面的自动化生成的PD文件会存在大量的没用的代码,对OC代码来讲可能会做优化,但你发现没用,本身它是OC的,你不能确保它能不能用运营的方法,PD文件是一方面及还有一方面是数据压缩,对我们每天成吨的数据压缩量是首先要考虑的,最初考虑了压缩,它那个库本身打出来就有大概18兆,虽然最后有优化,多个架构可能会去裁减。这个库18兆已经是不能忍了,那么大。当时我们做了很多次调研,这样的效果是最好的。本来一个UBT数据有多大?发过来的可能有1、2兆了。

我们有大量的业务是跑在企业级的,这样的包是走内部流量的,也是不能忍受的。当时也是有事故,6分钟内主服务器没有走CBN,6分钟左右损失了60万,因为主服务器的数据流量是非常贵的,所以包体也是不容忽视的。怎么做呢?用各种二进制去拆,做这方面的优化,时间问题不再细讲,这种文章网上搜一大堆。

耗电问题有很小的图,我在老的文献上找到的关于网络硬件的,手机设备在通讯的时候处于休眠期,当你有需求的时候会自动开启活跃期,活跃期和停歇期切换频繁的话,电量就掉的非常快,怎么办呢?你要降低耗电周期怎么办?一个是弱网节流问题,我们没有跟微信大咖聊的时候那么牛逼,直接到非常底层的弱网判断控制,包括底层的代码优化。我们的弱网判断是针对与它的响应时间,本身我们自己做的网络框架是可以知道所有的DSR包括数据时间的。

我们拿来之后发现超时间,网络响应时间太长。这种情况下会做节流层,要么不传数据,要么降低发送频率。合理的缓存和批量的传输。刚刚有遇到过,大家有时候也会要求实质性非常高的数据往后端发,用户点击一搜就把数据转换成事件,这样的情况下会出现什么问题呢?还是千万人吃饭的问题,瞬间发送服务器还是会崩溃。我们做一个简单的调整,就是做忍受值,我们认为数据从生成劳动发送到传输,传输的时候查过最慢的DNS解析是80秒,是非常非常差的网络插件。本身就会有一定的网络传输的消耗,我何必那么追求于毫秒级的立马给你发呢?我们认为有忍受值,目前设定的单位是60秒钟,60秒钟的数据都认为它是有实时属性、架构的才往后端传。一旦出现问题怎么办呢?我这边已经出现那么多的问题了,检查数据的生成时间是不是过了60秒?这一条数据就已经超过我们的忍受时间了,从传输队列去掉就好了。对我们传输的间隔也会调整,除了刚刚一系列网络的节流优化,加上这样一套实施策略,极大地提升了网络的效率和节点问题,这是非常好的方案。

最终我们还会发现通过APM平台会发现错误率特别高的,就是主机解析率特别高的,能达到86%。解析率错误率这么高还不是我们厂自己的,是依赖外部的服务,具体哪个厂就不再说了。存在这样的失败率的网络软件,而且还是每台不停地发,能看到它发送的频率。我们是天网系统,是会开源的,服务器端的代码也会开源的,只是服务不会去做,大家也可以自己去查一下自己的APP里面是不是有大量的偷偷摸摸在发数据而且网络还非常差的,微信也有,如果你没有接入微信分享的SDK,它也会不停地发用户信息。怎么处理大家自己肯定也知道。
基本上就讲这么多,谢谢大家!这是我的微信,大家有必要的话可以加我的微信我们再聊一下。这是我们的技术社区,有兴趣可以加入饿了么的技术社区我们一起交流。谢谢!

主持人:有问题吗?

提问:我有一个问题,你刚刚提到在保持服务的前提下固定重新频率,我想知道如何确定重试频率?
高亮亮:我们现在也在试错,最开始会搭配置,目前固定的间隔是按照自己拍脑袋,第一次是5秒,下一次不让它10秒,我认为这个时间段内服务器还是很好的,我下一次可能还是5秒,再下一次再开始加倍。
提问:我还是之间的问题,推送的问题,我看你有用到先是长连接,长连接不行用第三方推送,第三方推送不行开始多通道连,不是也会达到同时性吗?
高亮亮:我们服务在切的时候目前采用的推动框架是最开始同时发的,为了保证不丢数据是同时在发。因为现在不敢做到某一条推送不到但是我还不知道没有反馈的情况下只采用一套。
提问:谢谢!

提问:你刚才说到商家在接受订单的时候一开始尝试定时发会导致它可能有延时的状况,你后来说解决方案是用更优的组件和第三方的服务,通过更优的组件和第三方的服务是通过什么原理来解决这个问题的?
高亮亮:就是通过长连优先来解决实质性的问题,目前业界没什么更好的方法。大家对于多端走可能走协议会稍微变一点,我们现在大部分大家都知道通过BST连接是没问题的,可能会走上层的方案去做。
提问:谢谢!