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

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

前言

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

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

启动时间优化

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

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

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

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

phase

  • 针对 pre-main() :

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

env
premain

这些阶段都是系统进行管控,具体在这些阶段内如何进行优化,可以参照 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,系统就会学习并且适应这个习惯,在用户使用之前,后台进行调度来启动应用并执行数据更新。下图比较清晰的说明了系统是如何学习用户的使用模式的。

pattern

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

Background Fetch 使用

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

fetch

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

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

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

由于 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);
    });
}
AI 代码解读

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; // 复制给成员变量
    //...
AI 代码解读
  1. iOS7 闪退

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

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

if ios 7 : 
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalNever];
else ...
AI 代码解读

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 >= iOS9
        dispatch_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....
    }
}
AI 代码解读

问题与解决

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

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

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

  • iPhone7 不需要预加载

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

imagenamed

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

总结

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

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

https://tech.antfin.com/docs/2/49549

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

附注:WWDC2013 Session
https://developer.apple.com/videos/play/wwdc2013/204/

往期阅读

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

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

目录
打赏
0
0
0
0
38
分享
相关文章
安全监控系统:技术架构与应用解析
该系统采用模块化设计,集成了行为识别、视频监控、人脸识别、危险区域检测、异常事件检测、日志追溯及消息推送等功能,并可选配OCR识别模块。基于深度学习与开源技术栈(如TensorFlow、OpenCV),系统具备高精度、低延迟特点,支持实时分析儿童行为、监测危险区域、识别异常事件,并将结果推送给教师或家长。同时兼容主流硬件,支持本地化推理与分布式处理,确保可靠性与扩展性,为幼儿园安全管理提供全面解决方案。
阿里云SLB深度解析:从流量分发到架构优化的技术实践
本文深入探讨了阿里云负载均衡服务(SLB)的核心技术与应用场景,从流量分配到架构创新全面解析其价值。SLB不仅是简单的流量分发工具,更是支撑高并发、保障系统稳定性的智能中枢。文章涵盖四层与七层负载均衡原理、弹性伸缩引擎、智能DNS解析等核心技术,并结合电商大促、微服务灰度发布等实战场景提供实施指南。同时,针对性能调优与安全防护,分享连接复用优化、DDoS防御及零信任架构集成的实践经验,助力企业构建面向未来的弹性架构。
152 76
销售易CRM:技术架构与安全性能的深度解析
销售易CRM基于云计算与微服务架构,融合高可用性、弹性扩展及模块化开发优势,为企业提供灵活定制化的客户关系管理解决方案。系统采用多层次安全防护机制,包括数据加密、细粒度权限控制和实时监控审计,确保数据安全与隐私保护。某金融机构的成功案例表明,销售易CRM显著提升了数据安全性和系统性能,同时满足行业合规要求。作为数字化转型的利器,销售易CRM助力企业实现可持续发展与市场竞争力提升。
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
157 29
阿里云服务器架构解析:从X86到高性能计算、异构计算等不同架构性能、适用场景及选择参考
当我们准备选购阿里云服务器时,阿里云提供了X86计算、ARM计算、GPU/FPGA/ASIC、弹性裸金属服务器以及高性能计算等多种架构,每种架构都有其独特的特点和适用场景。本文将详细解析这些架构的区别,探讨它们的主要特点和适用场景,并为用户提供选择云服务器架构的全面指南。
249 18
一年撸完百万行代码,企业微信的全新鸿蒙NEXT客户端架构演进之路
本文将要分享的是企业微信的鸿蒙Next客户端架构的演进过程,面对代码移植和API不稳定的挑战,提出了DataList框架解决方案。通过结构化、动态和认知三重熵减机制,将业务逻辑与UI解耦,实现数据驱动开发。采用MVDM分层架构(业务实体层、逻辑层、UI数据层、表示层),屏蔽系统差异,确保业务代码稳定。
67 0
地铁站内导航系统解决方案:技术架构与核心功能设计解析
本文旨在分享一套地铁站内导航系统技术方案,通过蓝牙Beacon技术与AI算法的结合,解决传统导航定位不准确、路径规划不合理等问题,提升乘客出行体验,同时为地铁运营商提供数据支持与增值服务。 如需获取校地铁站内智能导航系统方案文档可前往文章最下方获取,如有项目合作及技术交流欢迎私信我们哦~
100 1
后端服务架构的微服务化转型
本文旨在探讨后端服务从单体架构向微服务架构转型的过程,分析微服务架构的优势和面临的挑战。文章首先介绍单体架构的局限性,然后详细阐述微服务架构的核心概念及其在现代软件开发中的应用。通过对比两种架构,指出微服务化转型的必要性和实施策略。最后,讨论了微服务架构实施过程中可能遇到的问题及解决方案。
云原生时代的应用架构演进:从微服务到 Serverless 的阿里云实践
云原生技术正重塑企业数字化转型路径。阿里云作为亚太领先云服务商,提供完整云原生产品矩阵:容器服务ACK优化启动速度与镜像分发效率;MSE微服务引擎保障高可用性;ASM服务网格降低资源消耗;函数计算FC突破冷启动瓶颈;SAE重新定义PaaS边界;PolarDB数据库实现存储计算分离;DataWorks简化数据湖构建;Flink实时计算助力风控系统。这些技术已在多行业落地,推动效率提升与商业模式创新,助力企业在数字化浪潮中占据先机。
92 12
云计算的未来:云原生架构与微服务的革命####
【10月更文挑战第21天】 随着企业数字化转型的加速,云原生技术正迅速成为IT行业的新宠。本文深入探讨了云原生架构的核心理念、关键技术如容器化和微服务的优势,以及如何通过这些技术实现高效、灵活且可扩展的现代应用开发。我们将揭示云原生如何重塑软件开发流程,提升业务敏捷性,并探索其对企业IT架构的深远影响。 ####
123 3

推荐镜像

更多
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等