本文由CocoaChina译者小袋子(博客)翻译
提高 Facebook 应用的性能已经成为 Facebook 持续关注的领域。因为我们相信一个高性能的应用能够传递一种吸引人且令人愉悦的体验。每个 Facebook 应用的用户都必须做的一件事是启动应用(我们特指这个动作为 ”应用启动“)。因此,这是一个很好的优化目标。
稳定的指标
实现最好的性能度量标准和相应的目标,鼓舞我们专注于提升应用的品质,并且我们相信这将会产生很大的影响。度量必须易懂、经得起推敲,并且需要精确地捕捉到将要被优化的体验。对基于性能的度量,我们已经发现在使用应用过程中,最好是使用那些被捕捉到感知的交互。理想情况下,这些度量应该和一个通过基础设施的单一执行通道有一对一的联系。对于应用程序的启动,确定用于衡量的关键位置是一个挑战。这需要采取几次迭代去简化我们的测量和移除边界问题。
应用启动是一个特别不固定的概念,因为现在存在很多种应用启动的方式。应用可以在后台或者前台启动,甚至可以在后台启动,但是在完成初始化之前转换为前台。你可以通过点击一条通知或者通过一个 URL 打开应用。Facebook 应用甚至可以通过其他应用来打开,因为他们需要通过 Facebook 来实现第三方登录。在现实场景中,主要的交互还是最直接的方式:你点击桌面的应用图标,然后跳转启动。因而,我们选择这个作为应用启动的入口。
当启动入口明确之后,我们必须去计算出何时应用启动是完成的。同样地,我们观察用户的使用模式,发现用户喜欢打开应用(首先跳转到新闻摘要),然后等待摘要的加载。我们断定“摘要完成加载”是应用启动一个很好的终点。我们采取了一些微调使得这个终点契合用户的使用情况。我们可以通过重复地观察应用的启动,围绕度量标准来提高应用的性能。
一旦确定了我们认为有代表性的启动入口和终点,我们把启动问题分解成两种类型:
冷启动。指的是当应用还没准备好运行时,我们必须加载和构建整个应用。这包括设置屏幕底部的分栏菜单,确保用户是否被合适地登录,以及处理其他更多的事情。“引导”程序是在applicationDidFinishLaunching:withOptions:方法中开始的。
热启动。指的是应用已经运行但是在后台被挂起(比如用户点击了 home 健),我们只需要知道在何时应用进入后台。在这种情况下,我们的应用通过 applicationWillEnterForeground: 接收到前台的事件,紧接着应用恢复。
我们决定主要优化冷启动,主要有两个原因。
首先,冷启动其实是包括热启动的(冷启动初始化应用并获得摘要;热启动只获得摘要),所以有更多的地方需要优化和微调。
其次,冷启动需要做额外的初始化工作,所以相较而言更慢,导致需要更长的启动等待时间。
优化冷启动体验
我们把冷启动问题分解成三个阶段,进而我们可以有针对性地解决。每个阶段都有一些列变数和挑战。
- 请求时间:从应用启动到摘要请求离开设备(译者:应该是向服务器发送URL请求算结束时间)的时间。
- 网络时间:从摘要请求离开设备到服务器响应返回的时间。
- 响应处理时间:从响应返回到新数据展示在屏幕的时间。
我们直观上认为冷启动性能主要被网络请求和响应处理影响了。这个结论是由于我们假定我们在客户端花的时间比较少,并且我们设法让请求的获取更加快速。然而,当我们用 instrument 去检测时,我们发现数据非常出人意料。它展现出了完全不同的结果,我们发现摘要请求花了大部分时间。另外,响应的处理时间也非常短。因此,我们重新把优化的焦点放在初始化阶段。
Feed请求发送的初始化
所以为什么这个阶段花费了那么多时间呢?很多 iOS 应用并没有这样一个问题——他们在那个阶段并没有很多工作需要做,除了初始化视图控制器和发送网络请求。然而,对于 Facebook 来说,大部分时间被用来开始的时候去设置不同功能块。下面是我们应用中的主要功能块的流程概览。
这看起来好像是很复杂的应用启动设置。但需要重视的是,这些功能块对于 Facebook 应用来说是非常重要的提升,可以提高应用体验,并且使得工程师能够在不同的应用规模下更快地开发。
正如我们所关注的这个流程,我们通过优化独立的部分获得了一些主要的成果。然而,由于未来支持新特性的初始化以及额外提供支持的基础设施,这些成果会慢慢地抵消掉。这使得我们重新考虑如何去解决问题。但我们重新开始,我们认为这个阶段的目标是简单地发送摘要的网络请求。但是为什么摘要请求发出去得这么慢?这是由于很多依赖被添加到摘要的初始化中了。然而,他们并不都是必要的 — 对于摘要请求来说,最少的需要一个有效的验证 token 以及摘要光标(新闻摘要的位置)。因此,我们减少了摘要请求的依赖,让它逐渐地更加接近应用的启动。这允许应用的剩余部分在摘要响应的同时进行初始化。由于这些重构,我们获得了显著的收益。
网络和服务器时间
根据我们在第一阶段的经验,我们继续把这个阶段分解成更小的部分。网络请求/响应看起来像这样:
我们注意到,一旦请求正在排队,发送请求出去之后就有一个时间间隔。这很好解释 — 在冷启动中,网络连接并不是一个开放的、安全的 TCP 连接。一个连接的建立需要三次握手,平均为几百毫秒。当摘要请求第一次发送时,无法避免要花掉这些时间。长远来看,这可以通过缓存 SSL 证书来解决。但是再次强调,我们退回来的目的并不是为了发送 TCP 请求,而是为了从服务器通过任何可能的方式获得请求信息。
我们提出了一个创造性的解决方案 — UDP 启动。本质上,我们在通过 TCP 发送摘要请求时,先发送一个编码过的包含摘要请求的 UDP 包到服务器。这样做的目的是唤醒服务器更早地去获取和缓存数据。当真正的摘要请求通过 TCP 到达时,服务器只需见到地从缓存内容中构造出响应,并发回客户端。这个技术使得我们可以减少几百毫秒的耗时。
当我们持续深入研究服务器端时,我们开始尝试使用 层-取(story-fetching)策略。过去我们已经做了一批摘要请求的 3+7 层。原因很简单:下载次数和被下载的层成正比。因此,把请求分割成两块,允许开始的三层先进来,其余的七个随后进来。通过提升我们的基础设施,我们已经能够升级为 1+1+X 策略,这已经接近于流了。这样就减少了服务器必须处理第一层的时间,并且能够减少下载的时间,使得可以在最快的时间内与用户交互。通过这样的努力,这样我们又减少了几百毫秒的耗时。
Feed响应处理
正如在前面提到的那样,我们以为在启动时会在这里花费大量的时间。但是这个想法被证明是错误的。更加使人好奇的是,我们注意到时间并没有花在处理和加工层上面。时间被花在运行应用服务和竞争资源上面。我们注意到这是我们优化网络和服务器时间的副作用,因为摘要请求返回得太早了。尽管大多数的服务是不重要的。因此,我们开发出一个简单的机制去序列化这些工作直到应用完成启动,并且使用先进先出的方式去执行。这样可以用更少的连接去处理所有层,大大地减少了获得响应和展示在屏幕之间的时间。
总结
很难理解我们在过去几个月走了多远。总之,在一对一的比较中,我们发现我们成功地优化了一秒多的耗时。
优化这个特殊的交互是一个长期的过程,需要建立一个稳定的度量,这个度量必须是易懂的、符合真实世界性能特征,此外要不断地重新思考问题,以提出创新的解决方案。我们希望这可以帮助使用 Facebook 的人有更好的、令人愉悦的用户体验。