前言

《支付宝客户端架构解析》系列将从支付宝客户端的架构设计方案入手,细分拆解客户端在“容器化框架设计”、“网络优化”、“性能启动优化”、“自动化日志收集”、“RPC 组件设计”、“移动应用监控、诊断、定位”等具体实现,带领大家进一步了解支付宝在客户端架构上的迭代与优化历程。

启动应用是用户使用任何一款应用最必不可少的操作,从点击 App 图标到首页展示,整个启动过程的性能,严重影响着用户的体验。支付宝客户端作为一个超级 App,启动的性能当然是我们关注的重要指标之一,下文将从三方面来介绍支付宝在 iOS 端启动性能优化的具体设计思路。

启动时间优化

分析启动时间之前,先看一下 App 启动的两种方式。

  • 热启动:启动应用时,应用的进程和数据已经存在于系统内存中,系统只是将应用的状态从后台切换到前台。
  • 冷启动:启动应用时,应用不存在于系统内核的 buffer cache 中,比如应用首次启动或者重启设备之后的启动。

相比而言,冷启动比较重要,通常我们分析启动时间,都是指的冷启动。

要想分析启动时间,还需要了解启动的过程,iOS应用的启动大概分以下几个阶段:

  • 针对 pre-main() :

整个 pre-main() 阶段的耗时可以通过添加环境变量 DYLD_PRINT_STATISTICS=1 来获取,如下图所示。

这些阶段都是系统进行管控,具体在这些阶段内如何进行优化,可以参照 WWDC2013 Session(文章尾部附地址)中提供的方案进行,这里不详细说明。

  • 针对 post-main() :

这部分主要是启动的框架初始化,首页数据获取,首页渲染等业务逻辑,这一部分我们只把必要的初始化操作保留,尽量把逻辑后置或者放在 background 线程执行。 这里的优化方案需要结合实际的业务场景和应用的架构来进行分析,采取对应的策略。

Background Fetch

除了这些通用的优化方案之外,我们也探索了一些创新的方式。 在介绍 Background Fetch 之前,我们先看这样一个案例:

操作:

首先,启动支付宝,按 Home 键切入后台。然后,重新启动手机,进入桌面。放置 10-30 秒。

现象:

此时,点击桌面的支付宝(以及淘宝等几乎所有 App)都与平时的冷启动一样,整个启动过程至少 1 秒以上。

虽然对冷启动的时间已经进行了优化,但是能不能每次启动都做到“秒起”呢?(秒起定义为:启动时显示 LaunchScreen 约 500ms 后马上进入首页) 我们发现系统提供了这样一个 Background Fetch 特性,决定在这个上面做一些尝试。

Background Fetch 简介

Background Fetch 类似一种智能的轮询机制,系统会根据用户的使用习惯进行适应,在用户真正启动应用之前,触发后台更新,来获取数据并且更新页面。

摘自苹果官方文档

Background Fetch lets your app run periodically in the background so that it can update its content. Apps that update their content frequently, such as news apps or social media apps, can use this feature to ensure that their content is always up to date. Downloading data in the background before it is needed minimizes the lag time in displaying that data when the user launches the app.

Background Fetch 具有下面几个特性:

  • 系统调度
  • 适应设备上各应用的实际使用模式
  • 对电量和数据的使用敏感
  • 与应用实际的运行状态无关

举个例子,比如用户习惯在下午1点使用某新闻类app,系统就会学习并且适应这个习惯,在用户使用之前,后台进行调度来启动应用并执行数据更新。下图比较清晰的说明了系统是如何学习用户的使用模式的。

针对这样的策略,大家可能会有疑虑,这种频繁的后台启动会不会增加耗电量? 当然不会,系统会根据设备的电量和数据使用情况来调用频率控制,避免在非活跃时间频繁的获取数据。而且,进程启动后后存活的时间很短,多数情况下会立即 suspend,对电量影响很少(相比压后台后很多 app 还要存活接近3分钟的情况很少)。

Background Fetch 使用

按照官方资料,Background Fetch 的用法很简单,整体流程如下图所示。

  1. Info.plist 中 UIBackgroundModes 节点配置 fetch 数值
  2. didFinishLaunching 时配置
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
复制代码

这一步配置的minimum interval,单位是秒,只是给系统的建议,系统并不会按照给定的时间间隔按规律的唤醒进程。

  1. 实现下面的回调,并调用 completionHandler
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
复制代码

由于 Background Fetch 机制是为了让App在后台拉取准备数据,但支付宝只是为了实现”秒起“。调用 completionHandler 后系统将把 App 进程挂起。且系统必须在30秒内调用 completionHandler,否则进程将被杀死。此外根据文档,系统会根据后台调用 completionHandler 的时间来决定后台唤起App的频率。因此,认为可以“伪造“1秒的延迟时间,即1秒后调用 completionHandler。类似下面的代码:

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{completionHandler(UIBackgroundFetchResultNewData);});
}
复制代码

Background Fetch 实践

苹果推出这种特性的动机在于,后台触发获取数据并更新页面,确保用户使用时看到的永远是最新的内容。然而,支付宝只是为了实现“秒起”,所以看似简单的实现,却隐藏着巨大的风险。 在测试过程中就发现了这些问题:

  1. 进程快速挂起导致 Sync 成功率下降

灰度期间,开发同学发现同步服务 Sync 成功率下降很多,找来找去发现原因:由于进程唤醒后,网络长连接线程被激活并马上建立长连接,而1秒后调用completionHandler,进程又被挂起。服务器端的sync消息则发送超时。

  1. 进程频繁挂起、唤醒导致网络建连次数增加

系统预测用户使用 App 的时间,并在用户实现 App 前唤醒 App,给予 App 后台准备数据的机会。再加上预测的准确性问题,这样进程被唤醒的次数远大于用户使用的次数。进程唤醒后,网络长连接会立即建立。因此导致网络建连次数大增,甚至翻倍。

  1. 由于进程挂起,导致定时器、延迟调用等时间“与预想的时间不同”

例如,一个间隔间隔时间为 60 秒的定时器,由于进程挂起时间超过 60 秒,则下次进程唤醒时会立刻触发到时。(延迟调用 dispatch_after 等类似)。对于进程自身来说,可能定时器有点不正常,需要排查所有的定时器逻辑,是否会因为挂起导致“业务层面的异常”。

  1. 获取时间戳

由于进程挂起,导致前后获取的时间戳间隔很大。

为解决以上遇到的、以及预测到的问题,经过讨论,决定在 Background Fetch 后台唤醒的时候,不建立长连接。

  • 延后 10 秒调用 completionHandler。

后台唤醒存在两种情况:进程从无到有,进程从挂起到恢复。前者需要有充足的时间完成 App 的后台冷启动过程,因此定义了 10 秒的时间。

  • 后台 Background Fetch 的时间内不建立长连接。

”后台 Background Fetch 的时间“定义为:performFetchWithCompletionHandler 被回调并一直到 completionHandler 调用的时间内。

我们维护了一个全局变量 underBackgroundFetch 用于标识这段时间。处于这段时间的所有网络请求都被阻塞,并增加重试判断。App 进入前台(willEnterForeground)时主动重新建立长连接。在一些其他后台需要建立长连接的情况下(例如 WatchApp 的连接、PUSH 快速回复),也主动修改标记,并通知网络层建立长连接。underBackgroundFetch 的修改是在主线程执行,但网络长连接的建立是在子线程,且进程被唤醒后早于 underBackgroundFetch 的修改。目前首次回调 performFetchWithCompletionHandler 时,仍然会存在这个“间隙”导致网络长连接建立,但后续的 Background Fetch 时状态是准确的。(这个间隙如何更加准确,必要性及方案在讨论中,目前还没有带来无法解决的问题)

  • 后台不建连导致的网络请求阻塞异常,避免产生 Toast 等弹窗。

为获取所有在后台 Background Fetch 时间内被拦截的 RPC,拦截操作增加了埋点。灰度期间收集出所有的 RPC,并逐个找到 Owner,让大家评估影响、以及避免产生 Toast 等弹窗提示。确保所有 RPC 异常的最外层异常捕获处,不因 RPC 拦截的异常而 Toast。

  • 超时判断

由于进程挂起导致的定时器、延迟调用的超时判断,需要修改业务逻辑。不能过度依赖假想的时序,进程运行在操作系统上,不能受进程的挂起与恢复影响。

虽然使用这么多的方案来保证应用的稳定性,但是实际上线也避免不了一些奇怪的问题:

  1. completionHandler 调用两次

灰度期间发现少量用户存在 completionHandler 调用两次导致闪退。捞取用户日志发现 performFetchWithCompletionHandler 在1秒内连续被系统回调了两次。而 completionHandler 被存储为 AppDelegate 的成员变量,在10秒超时到期后,同一个 completionHandler 被调用了两次。

为避免此问题,可以避免采用成员变量存储 completionHandler ,而采用 dispatch_after 来直接让 block 捕获 completionHandler,但这样又会带来另一个 libdispatch 中 block 为空的极小概率的闪退。

因此采用成员变量存储 completionHandler,而在 performFetchWithCompletionHandler 的首行判断存储的 completionHandler 与传入的 completionHandler 是否相同。大致代码如下:

- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{if(_backgroundFetchCompletionHandler && _backgroundFetchCompletionHandler != completionHandler){// 避免performFetch被快速重复调用,如果completionHandler不同,则先完成上一个completionHandler;如果相同,则避免调用两次。[self callBackgroundFetchCompletionHandler]; // 内部调用completionHandler}_backgroundFetchCompletionHandler = completionHandler; // 复制给成员变量//...
复制代码
  1. iOS7 闪退

这个闪退 StackOverflow 上有人遇到,但点赞最多的答案实际上也没解决问题。

这个闪退仅在 iOS7 上产生,经过各方资料认为是 iOS7 系统的 bug。那么在 iOS7 设备上则不再启用 BackgroundFetch。

if ios 7 :
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
else ...
复制代码

Background Fetch 机制让 iOS App 也能做到“热启动”,但带来的进程挂起、唤醒次数大量增加,给已经稳定运行很久的代码带来一种”不稳定“的运行方式,必须要认真考虑每一个细节。

图片预加载

[UIImage imageNamed:@"xxx"] 是 iOS 中加载图片的 API,它的使用频率是比较高的,那么它的性能如何呢。我们在分析启动性能的过程中,发现这个方法的耗时很多,iPhone5S 下每个耗时都在 20ms 到 50ms 之间,首页加载过程中有10多张这种方式加载的图片。针对整个现象,在支付宝中,我们使用了一种图片预加载的方式来进行优化。

设计思想

在看 [UIImage imageNamed:] 文档时发现一句话

In iOS 9 and later, this method is thread safe.

看到它之后立刻想到,能否在进程启动早期通过子线程预先加载首页图片。为什么在早期呢?通过 Instruments 分析可看到在支付宝启动早期,CPU 占用是不那么满的,为了让启动过程中充分利用 CPU,就尽量在早期启动子线程。

首先通过 hook 方式,获取首页的所有 imageNamed 加载的图片,然后,大致代码如下:

int main(){@autoreleasepool{//if >= iOS9dispatch_async(dispatch_get_global_queue(0, 0), ^{NSArray<NSString*> *images = @[// 10.0@"Launcher.bundle/TabBar_BG",@"Launcher.bundle/TabBar_HomeBar",//.... 省略10多个图片];for (NSString *name in images) {[UIImage imageNamed:name];}}// AppDelegate....}
}
复制代码

问题与解决

在优化之后,也伴随而来一些不稳定的问题:

  • App 启动会有小概率的 Crash。

根据分析,我们决定把这段代码移到 AppDelegate 的 didFinishLaunching 中,并且增加开关。

  • iPhone7 不需要预加载

在 iPhone7 设备出来后,我们发现 iPhone7 的启动性能反而不如 iPhone6S。分析后发现,在性能更好的 iPhone7 上,由于启动很快,导致子线程的 imageNamed 与 主线程的 imageNamed 相互穿插调用,而 imageNamed 内部的线程安全锁的粒度很小,导致锁的消耗过大。如下图:

因此,在性能更好的 iPhone7 上不再启用预加载。

总结

通过 Background Fetch 和图片预加载这两种方式对启动性能进行优化,给我们提供了另外一种思路,对于优化不要仅限制在条框内,需要适当的创新。但是,对于这种有点“创新”的代码,一定要有“开关”,增强风险意识。当然,性能优化不是一蹴而就的,它是一个持续的课题,值得我们时刻来关注。

由于篇幅限制,很多技术要点我们无法一一展开。而相应的技术内核,我们同样应用在了 mPaaS 并对外输出,欢迎大家上手体验:

tech.antfin.com/docs/2/4954…

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

附注:WWDC2013 Session developer.apple.com/videos/play…

往期阅读

《开篇 | 模块化与解耦式开发在蚂蚁金服 mPaaS 深度实践探讨》

《支付宝移动端动态化方案实践》

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

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

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

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

支付宝客户端架构解析:iOS 客户端启动性能优化初探相关推荐

  1. iOS App 启动性能优化

    为什么80%的码农都做不了架构师?>>>    本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:https://mp.weixin.qq. ...

  2. mysql优化十:从架构角度全局理解mysql性能优化

    从架构角度全局理解mysql性能优化 MySQL性能优化其实是个很大的课题,在优化上存在着一个调优金字塔的说法: 很明显从图上可以看出,越往上走,难度越来越高,收益却是越来越小的.比如硬件和 OS调优 ...

  3. 百度App Android启动性能优化-工具篇

    一.前言 启动性能是APP的极为重要的一环,启动阶段出现卡顿.黑屏问题,会影响用户体验,导致用户流失.百度APP在一些比较低端的机器上也有类似启动性能问题,为保留存,需要对启动流程做深入优化.现有的性 ...

  4. 多迪php,多迪PHP项目经理深度解析:PHP应用性能优化指南!

    原标题:多迪PHP项目经理深度解析:PHP应用性能优化指南! 程序员都喜欢最新的PHP 7,因为它使PHP成为执行最快的脚本语言之一.但是保持最佳性能不仅需要快速执行代码,更需要我们知道影响性能的问题 ...

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

    前言 由本章节开始,我们将从支付宝客户端的架构设计方案入手,细分拆解客户端在"容器化框架设计"."网络优化"."性能启动优化"." ...

  6. 资深架构师手把手教你性能优化

    图片来源:pexels.com 孔庆龙,一线架构师,具有多年的金融架构经验,具备 SOA 服务化.服务治理.系统优化.分布式系统项目经验.目前关注于互联网金融技术架构设计.分布式架构.微服务架构.De ...

  7. iOS最全性能优化(中)

    续 性能优化(上) 9. 重用和延迟加载(lazy load) Views 更多的view意味着更多的渲染,也就是更多的CPU和内存消耗,对于那种嵌套了很多view在UIScrollView里边的ap ...

  8. 云时代架构阅读笔记二——Java性能优化(二)

    承接上文Java性能优化(一)https://www.cnblogs.com/guo-xu/p/11019267.html 4)尽量确定StringBuffer的容量 在说和这个标题相关之前,先说一下 ...

  9. 全景解析SSD IO QoS性能优化

    一.NAND基本原理 目前NAND已经从SLC发展到PLC,但是PLC离大规模上市还有一段距离,我们暂时先略过.市面上主要流通的就是4种NAND类型:SLC.MLC.TLC.QLC.随着每个寿命从高到 ...

  10. 阿里Java架构师精通资料:性能优化+亿级并发架构汇总+架构选型

    分布式并发架构 微服务.Docker容器的基本原理.架构设计,以及应用场景. 缓存:Redis.Memcached.CDN.本地缓存 搜索引擎的选型:Lucene.Solr等选型与比较 应用服务器雪崩 ...

最新文章

  1. 豆瓣图书的推荐与搜索、简易版知识引擎构建(neo4j)
  2. 二叉搜索树相关知识及应用操作
  3. DevExpress AspxGridView数据绑定
  4. grafana计算不同时间的差值_大数据时代!如何基于Spark Streaming构建实时计算平台...
  5. 数据结构基础(3)---C语言实现单链表
  6. 父与子一起学python_父与子的编程之旅:与小卡特一起学Python 中文pdf版[22MB]
  7. python 爬虫代码实例
  8. B站【千锋】Linux云计算基础视频教程_650集完全入门 课堂笔记
  9. 爬虫——selenium模块的基本使用(qq空间的登录)
  10. java speex回声消除_android – Speex回音消除配置
  11. Java 压缩文件夹
  12. cfree5c语言编写贪吃蛇,刚学C语言,想写一个贪吃蛇的代码
  13. 四川大学计算机徐教授,我院举行“BIM实验室开放+徐教授专题讲座”活动
  14. less面试_面试必看:less与sass的区别
  15. ionic 实现广告图片无限滚动标签介绍
  16. Application Loader上传成功了 iTunes Connect不显示构建版本
  17. Linux文本处理三剑客sed详解(正则匹配、命令示例)
  18. Vue ElementUI 表单设计器 代码生成器
  19. Z-Libary最新地址检测,再也不用担心找不到ZLibary了
  20. 简约商务通用PPT模板

热门文章

  1. ipa shell自动打包
  2. 004 数字调制ASK,OOK,FSK,PSK,QAM,CPFSK
  3. linux没有无线wifi密码忘记,新版tplink无线密码(wifi密码)忘记了怎么办?
  4. PHP手册 2009国庆版
  5. macos备份文件服务器地址,mac电脑备份文件的最佳方法
  6. CentOS Steam 9 安装测试
  7. outlook邮件路径更改_如何更改Outlook的新邮件警报声音
  8. java学习篇(一)---从网络下载图片
  9. The Matrix
  10. Google浏览器设置不自动更新:关闭谷歌浏览器自动更新方法(总是自动更新提示失败)