【老司机精选】理解和消除 App 中的卡死

作者:Rickey 王小吉,字节跳动直播中台,一个苹果中毒的程序员。欢迎踩踩主页:github.com/RickeyBoy/R…

审核:Damien, 老司机技能周报编辑,TikTok iOS 工程师

WWDC21 – 了解和消除 App 中的卡死

目录

本文将经过四个部分,让开发者了解并消除 App 中的卡死问题。

【老司机精选】理解和消除 App 中的卡死

一、什么是卡死

【老司机精选】理解和消除 App 中的卡死

当用户触摸了屏幕,但几秒钟之后 App 才有呼应,那么这种状况就被称作卡死,换句话说也便是未呼应、呼应迟缓等。任何 App 都不会想给用户卡顿的体会。

为了了解卡死,咱们需求先知道什么是主线程 runloop。如下图所示,每一个 App 都会依附一个主线程 runloop,它便是一个不会停止的循环,App 底层会在这个循环里不停地对事情进行呼应,处理连绵不断的外部事情,然后呼应最重要的用户操作。

【老司机精选】理解和消除 App 中的卡死

当用户与 App 进行交互时,App 会阅历接收事情、处理事情、(假如有需求的话)更新 UI 这三个阶段,这样三个阶段的事情都发生在主线程 runloop 的一个循环之中,接连呼应到每个用户触发的事情。

而假如处理事情的时间过长,那在接收事情和更新 UI 这两个事情直接就会发生推迟,乃至还会形成主线程使命堆积,堵塞后续事情的呼应。在卡死阶段触发的后续使命将无法被呼应,直到第一个形成卡死的使命结束,与此一起后续使命也将会增加这个卡死的时间,形成恶性循环。

【老司机精选】理解和消除 App 中的卡死

总的来说推迟超过一秒就会让用户感觉到卡死。不过有些状况下一次小推迟相对简略接受,比方关于 0.5 秒的推迟,假如出现在列表滑动阶段那么用户就会觉得反常反感,但假如在页面跳转进程中发生一次,就没那么简略察觉。

二、形成卡死的原因

当主线程上有超出其负荷的使命被履行时,就会发生卡死,此时的卡死或许包含下面两种状况:

  1. 主线程自身处于繁忙状况,而形成卡死,有或许是处理单个长使命,也有或许是多个接连短使命。
  2. 主线程被其他线程的使命堵塞,或是被体系资源堵塞。

接下来让咱们分状况来详细评论。

主线程被卡死

主线程自身处于繁忙状况,而形成卡死

履行剩余的前置使命

咱们先来举例看下最常见的第一种状况。一般在前置履行使命时,有或许会履行一些剩余操作,然后导致主线程卡死。

【老司机精选】理解和消除 App 中的卡死

比方在上面这个官方演示的 App(Deserted) 中,页面只平铺展现 4 张食物配料图片,因而只需求加载四张图片即可,而不是加载悉数图片。假如在进入这个页面的时分一次性加载一切图片,每一张图片都会进行消耗许多时间,然后形成主线程卡死,而实践上绝大部分的时间消耗都不会影响页面展现的内容。

履行不相关操作

另一个常见形成卡死的原因,是在主线程履行了其他派发行列中不相关的使命。主线程是串行履行,所以不仅会履行主线程行列里的 block,一起也会履行其他行列里同步履行的 block。所以其他行列中派发到主线程同步履行的 block,就有或许堵塞主线程后续的使命。

而当主线程向存储行列派发了一个同步履行的 block,那么主线程上接下来的操作,就都需求等候存储行列上的使命履行完毕,才干继续履行。这样一来,实践上主线程上大部分的时间都被无谓的浪费了。

如下图所示:App 中一些低优先级的行列,比方一个存储行列(maintenance queue),假如它的使命(maintenance work)被派发到主线程上同步履行,那么这种耗时长却低优先级的维护使命,就会形成主线程卡死。

【老司机精选】理解和消除 App 中的卡死

相似的状况还有:假如存储行列向主线程派发了一个同步履行的 block,那么主线程也有必要要等候这个 block 履行完成才干继续。

没有运用适宜 API

没有运用适宜 API 也是常见形成卡死的原因之一,完成同样的效果有多种不同的路径,所以必定要熟读 API 文档,确保运用适宜的 API。

以演示 App Desserted 为例,它给图片加圆角的办法,是经过根据 bitmap 的 UIGraphics 办法,将图片转换为 bitmap 之后,再用圆角矩形的贝塞尔曲线进行裁剪,最后将裁剪后的 bitmap 转换回图片格式。

这一系列的操作会导致 CPU 负载过重,耗时长且消耗许多内存。这是由于运用了过错的体系硬件,这里应该利用 GPU 而不是 CPU:经过运用 CoreAnimation 中 layer.cornerRadius 属性和 masksToBounds 属性,就能轻松且高效地给图片增加圆角。

主线程被堵塞

主线程被其他线程的使命堵塞,或是被体系资源堵塞

过错运用同步 API

运用同步 API 会堵塞线程上后续使命的履行,从调用开始堵塞直到 API 回来结果。假如这类办法内部进行了许多的作业,或许有或许延时回来,那么它们就不应该在主线程上被运用,由于它们或许导致推迟,以及会增加失利的几率。

一个典型的事例是在主线程进行同步的网络恳求。关于那些运用 5G 网络的用户,这样或许不会有任何推迟,但是网络条件差一些的话,恳求就或许会花更长的时间。关于那些信号很差的用户,有或许就会一直卡死了。谁也不能确保网络恳求的耗时长短,所以这类的同步操作都应该防止在主线程履行。

File I/O 受限

另一种主线程堵塞的状况是被体系资源限制了,由于体系资源常常会不够用。File I/O 文件接口又是最常用且最简略资源缺乏的体系资源,由于文件接口有许多不确定要素,比方体系硬件功用,以及或许其他 App 也一起在进行读写操作。因而 App 需求尽量防止这些影响要素,比方防止在主线程运用 I/O 接口。

不支持并发的数据存储特别简略发生问题。当主线程读取一个正在进行写操作的数据,由于不支持并发,那么这个读取操作就会被推迟到写操作结束,在写操作以及读操作完成之前,App 都无法呼应。

【老司机精选】理解和消除 App 中的卡死

同步操作堵塞

同步原语(synchronization primitive)会堵塞读写使命履行,因而要削减在主线程进步行同步,即使要用也需求十分小心。进行同步操作的线程,一般会加锁后长时间才会将锁开释,不论是隐式锁仍是显示锁。

下面有一些常见需求留意的同步原语:

【老司机精选】理解和消除 App 中的卡死

特别需求留意信号量的运用,由于信号量不运用优先级战略,因而信号量的抢占有或许导致更长时间的卡死。一个常见的过错如下图所示:想经过信号量之间的等候,使异步办法串行履行。必定要在主线程中防止这种操作。

【老司机精选】理解和消除 App 中的卡死

重复获取不变量

还有一种堵塞主线程的原因,是花费许多价值去不断获取一些不常常改变的数据。

【老司机精选】理解和消除 App 中的卡死

比方官方演示 App Desserted 中的这个代表交际功用的按钮,只要当我具有通信录老友的时分才进行展现。咱们能够每次都获取一下通讯录中的联系人,然后判断是否具有老友。但这样做会增加许多额定开支和推迟,由于主线程调用通讯录的相关结构,会在底层重复许多操作,发生昂扬的开支。更何况需求获取的联系人信息并不常常变动,因而没必要频频的获取联系人,这只会增加体系资源的压力。

体系资源受限

卡死:主线程过度运用,导致体系资源受限。

【老司机精选】理解和消除 App 中的卡死

体系资源包含 CPU、内存、存储空间等的状况都对卡死有较大的影响,实践状况下千差万别的设备硬件状况,和开发时本地测验遇到的零散状况大不相同。所以咱们应该尽量去防备这样的景象,比方运用自动化测验,或许以最陈旧的设备进行压力测验。

总的来说,形成卡死的原因便是主线程被过度运用了。因而为了确保良好的功用体会,咱们需求让主线程尽量只去做一些 UI 更新有必要的使命。

三、如何剖析卡死原因

了解了常见的卡死原因之后,让咱们来看看有哪些有用的东西能够对 App 中的卡死来监控和分类。

System Trace

进一步了解:System Trace in depth – WWDC16

为了对卡死进行分类,一般首先要了解 App 正在做的使命。而在 Instrument 中的 Time Profile 这个东西能够清楚地展现当时 App 的调用栈信息,能精准地分分出正在履行的使命信息。而 Instrument 中的 System Trace 东西能够展现更多信息,包含体系调用、分页过错、I/O 接口信息等,乃至包含进程内以及进程间的协作状况。

接下来我将演示运用这两个东西,来剖析官方示例 App 中的卡死原因。在运用 System Trace 对 App 进行了剖析之后,Instrument 界面大概会这样显示成这样:

【老司机精选】理解和消除 App 中的卡死

  • 符号 1 的地方,system trace 输出的红色细线条代表体系办法的调用
  • 符号 2 的紫色条形图代表虚拟内存的分页过错
  • 符号 3 的水平蓝条,代表主线程在繁忙状况
  • 符号 4 的方位,能够挑选想要检查详细的调用信息
  • Instrument 会展现这 4.7s 的卡顿内主线程的调用信息,符号为 5 的这部分内容阐明晰 loadAllImages() 导致了其中 4.6s 的卡顿。这便是前文所说的问题,实践上官方示例 App 中加载了过多剩余的图片,导致了卡死。

MetricKit

进一步了解:What’s new in MetricKit – WWDC20

一旦你的 App 发布之后,就能够经过 MetricKit 来收集卡死时的调用栈了,这样能让你发现哪些卡死的调用栈是用户更简略射中的。咱们现在来看一个比如:

【老司机精选】理解和消除 App 中的卡死

MetricKit 同样也会统计卡死时的调用栈,呈现出的调用栈和 Time Profile 中的很像。经过剖析调用栈,咱们发现这个卡死和咱们刚才剖析的卡死不一样,这个卡死是新加的交际功用导致的,由于一次性获取了一切联系人而堵塞了当时行列。而假如不是运用 MetricKit,我或许永远也不会发现这个卡死问题。

Xcode Organizer

能够经过下面两个 session 来了解 Xcode Organizer 的更多信息:

Diagnose power and performance regressions in your App – WWDC21

Improving battery life and performance – WWDC19

在修复卡死问题时,能够量化 App 的全体功用状况是十分重要的一点。而 Xcode Organizer 东西能够展现功用表现相关的数据,包含能展现各个版本运用的卡死率的图表,这关于咱们剖析 App 用户流失的原因很有帮助。

四、如何消除卡死

那么现在咱们来了解一些常见处理 App 卡死问题的战略。一起需求记住,每一种战略都能处理一些卡死问题,为了找到最适宜的处理计划,你有必要要了解这些计划的副作用并进行挑选。

削减主线程上的作业

削减主线程进步行的作业总量,能够有用地消除并防备卡死问题。为了到达这个目的,咱们一般有两个办法。第一个是优化主线程上使命的效率,然后削减总时间。第二个办法是将一些使命从主线程移除,改用一些不会堵塞主线程的办法进行,然后确保主线程能及时呼应。

运用缓存

关于常常运用的资源,采用缓存战略是一种很好的办法。缓存一般是一种存在于内存中的,假如需求多 App 同步的话也能够耐久化到硬盘空间。有或许被用到的固定资源很合适运用缓存,比方 Desserted 中的那些配料图片,由于假如每次用届时都去创立就会有许多开支。

【老司机精选】理解和消除 App 中的卡死

经过运用 NSCache 缓存,每次创立图片的许多开支就能被简化为内存的拜访,这样就能消除咱们之前经过 Instrument 分分出,由于过度加载图片而导致的卡死。

不过重要的是,需求有一个精确的缓存检查机制,在维护缓存的增删,确保缓存内容总量的平衡。这些操作一般会异步地在另一个线程进步行,然后确保主线程能够及时呼应其他事情。

增加观察者

增加观察者来监听告诉的计划也能削减主线程作业量,这种办法能针对数值或许状况进行监听,然后防止贵重的核算量。任何类都能够发出告诉,乃至是自定义的类,检查某些类的 API 阐明文档就能够找到相应的事情告诉。能够经过 Apple developer 官方文档中的找到 NSNotification.Name,也便是可监听的体系告诉名称。

【老司机精选】理解和消除 App 中的卡死

而 Desserted 中的交际按钮,便是一个很好的运用示例:经过注册对 abDatabaseChangedExternally 告诉的监听,主线程不再需求等候获取联系人列表的进程了,只需求等候告诉发出、观察者进行呼应就行了。而在 Desserted 的比如中,收到告诉后将会更新一下缓存的联系人列表。而这些更新操作也需求放在另一个异步线程中,这样确保主线程不被卡死。

转移主线程上的作业

核心:主线程只担任满足重要的使命

将主线程上的作业就行转移,给主线程减负,这也是一个好办法。那终究哪些作业该在主线程上履行呢?

只要满足重要的使命,也便是为 UI 展现供给支持的使命才应该在主线程上履行。而且一切视图和视图控制器,他们的创立、修改和毁掉也都应该在主线程上履行。

而用于更新 UI 元素的核算进程,就能够从主线程转移到其他线程上,只需求在核算结束时在主线程履行真正的 UI 更新操作即可,这种形式十分合适与核算耗时很长的使命。一些不太重要的使命,或是对时间不灵敏的使命,都应该像这样转移到其他线程上异步履行。

异步 API

将使命从主线程转移到异步线程,运用异步 API 是最直接的办法。咱们以网络恳求为例,经过运用 NSURL 类的异步 API,咱们就能让主线程在网络恳求期间保持能够呼应:

【老司机精选】理解和消除 App 中的卡死

一般来讲,异步 API 姓名都会包含 “asynchronously” 单词或许其缩写,而 API 的回调办法名一般包含 “completion”,十分好辨认。

GCD 办法

进一步了解:Modernizing Grand Central Dispatch – WWDC17

GCD 是一个功用强大的多线程架构,能够方便地完成异步操作。GCD 供给了十分简略的接口,能够将任何 block 从主线程移至子线程,既能够同步履行也能够异步履行。因而,运用 GCD 办法能够有用地处理绝大部分的卡死问题。经过运用 GCD 的异步调用办法,将 block 转移到另一个线程履行,就能确保主线程不被卡死;一起在完成后调用 completion 办法,再回到主线程履行必要的办法即可:

【老司机精选】理解和消除 App 中的卡死

GCD 也能轻松完成预加载逻辑。将预加载逻辑绑定在一个线程(比方 prefetchQueue)上,再异步履行预加载 block,就能在确保主线程不卡顿的状况下履行预加载使命。而当主线程需求预加载使命的结果时,运用 sync 办法在同一个线程(prefetchQueue)上串行履行后续逻辑即可。

了解交流的价值

以上说的这些办法,本质上来讲都是一些交流,能处理卡死问题的一起也有或许导致其他问题。

  • 运用缓存:空间换时间。运用缓存时需求考虑内存的过度增加,有必要要确保缓存的清理机制是有用的。
  • 增加观察者:告诉或许被频频触发,因而在监听告诉的时分最好先加一些过滤条件,防止额定的操作,减轻 CPU 的担负。
  • 异步 API:有必要要了解哪些操作能够异步履行,特别是直接触及 UI 更新的使命必定不能放到异步线程,由于异步线程的优先级不高,不会优先履行。
  • GCD 办法:运用 GCD 的价值便是你有必要要调整代码履行的次序,因而你需求时间保持清醒,防止程序犯错。运用 sync 办法来确保履行次序也是一个不错的挑选。

不过考虑到这些办法能够有用处理卡死问题,这些交流必定是值得的。

一些其他建议

  1. 尽量运用 Apple 的结构以及接口,由于它们现已针对一切苹果设备都进行了有用地优化,以及会保持不断地更新与优化。
  2. 不断迭代优化代码。能够不断制定小的优化目标,看到每次优化的效果,再集腋成裘。
  3. 合理地运用体系资源。过度运用资源会不仅仅下降 App 的功用体会,也会导致整个体系的卡顿。

五、总结

卡死问题是导致用户体会的最大问题之一,当用户遭遇卡死时很或许就退后台杀进程了,这会导致十分高的用户流失率。所以一旦触及到卡死,必定是需求较高优先级处理的。

经过本文的学习,在咱们知道了解原理、善用东西的状况下,剖析并处理卡死问题就变得简略起来。一起期望本文也能起到抛砖引玉的作用,究竟真实的卡死状况愈加杂乱,还有许多需求进一步探究的地方。最重要的是能了解导致卡死的原理,以及学会处理卡死问题的基本思路,遇到问题才干方便的解决。

重视咱们

咱们是「老司机技能周报」,一个继续追求精品 iOS 内容的技能公众号。欢迎重视。

重视有礼,重视【老司机技能周报】,回复「2021」,收取 2017/2018/2019/2020 内参

支持作者

在这里给大家推荐一下 《WWDC21 内参》 这个专栏,一共有 102 篇关于 WWDC21 的内容,本文的内容也来历于此。假如对其余内容感兴趣,欢迎戳链接阅览更多 ~

WWDC 内参 系列是由老司机牵头组织的精品原创内容系列。 现已做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并召唤一群一线互联网的 iOS 开发者,结合自己的实践开发经历、苹果文档和视频内容做二次创作。