作者:Rickey 王小吉,字节跳动直播中台,一个苹果中毒的程序员。欢迎踩踩主页:github.com/RickeyBoy/R…
审核:Damien, 老司机技能周报编辑,TikTok iOS 工程师
WWDC21 – 了解和消除 App 中的卡死
目录
本文将经过四个部分,让开发者了解并消除 App 中的卡死问题。
一、什么是卡死
当用户触摸了屏幕,但几秒钟之后 App 才有呼应,那么这种状况就被称作卡死,换句话说也便是未呼应、呼应迟缓等。任何 App 都不会想给用户卡顿的体会。
为了了解卡死,咱们需求先知道什么是主线程 runloop。如下图所示,每一个 App 都会依附一个主线程 runloop,它便是一个不会停止的循环,App 底层会在这个循环里不停地对事情进行呼应,处理连绵不断的外部事情,然后呼应最重要的用户操作。
当用户与 App 进行交互时,App 会阅历接收事情、处理事情、(假如有需求的话)更新 UI 这三个阶段,这样三个阶段的事情都发生在主线程 runloop 的一个循环之中,接连呼应到每个用户触发的事情。
而假如处理事情的时间过长,那在接收事情和更新 UI 这两个事情直接就会发生推迟,乃至还会形成主线程使命堆积,堵塞后续事情的呼应。在卡死阶段触发的后续使命将无法被呼应,直到第一个形成卡死的使命结束,与此一起后续使命也将会增加这个卡死的时间,形成恶性循环。
总的来说推迟超过一秒就会让用户感觉到卡死。不过有些状况下一次小推迟相对简略接受,比方关于 0.5 秒的推迟,假如出现在列表滑动阶段那么用户就会觉得反常反感,但假如在页面跳转进程中发生一次,就没那么简略察觉。
二、形成卡死的原因
当主线程上有超出其负荷的使命被履行时,就会发生卡死,此时的卡死或许包含下面两种状况:
- 主线程自身处于繁忙状况,而形成卡死,有或许是处理单个长使命,也有或许是多个接连短使命。
- 主线程被其他线程的使命堵塞,或是被体系资源堵塞。
接下来让咱们分状况来详细评论。
主线程被卡死
主线程自身处于繁忙状况,而形成卡死
履行剩余的前置使命
咱们先来举例看下最常见的第一种状况。一般在前置履行使命时,有或许会履行一些剩余操作,然后导致主线程卡死。
比方在上面这个官方演示的 App(Deserted) 中,页面只平铺展现 4 张食物配料图片,因而只需求加载四张图片即可,而不是加载悉数图片。假如在进入这个页面的时分一次性加载一切图片,每一张图片都会进行消耗许多时间,然后形成主线程卡死,而实践上绝大部分的时间消耗都不会影响页面展现的内容。
履行不相关操作
另一个常见形成卡死的原因,是在主线程履行了其他派发行列中不相关的使命。主线程是串行履行,所以不仅会履行主线程行列里的 block,一起也会履行其他行列里同步履行的 block。所以其他行列中派发到主线程同步履行的 block,就有或许堵塞主线程后续的使命。
而当主线程向存储行列派发了一个同步履行的 block,那么主线程上接下来的操作,就都需求等候存储行列上的使命履行完毕,才干继续履行。这样一来,实践上主线程上大部分的时间都被无谓的浪费了。
如下图所示:App 中一些低优先级的行列,比方一个存储行列(maintenance queue),假如它的使命(maintenance work)被派发到主线程上同步履行,那么这种耗时长却低优先级的维护使命,就会形成主线程卡死。
相似的状况还有:假如存储行列向主线程派发了一个同步履行的 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 都无法呼应。
同步操作堵塞
同步原语(synchronization primitive)会堵塞读写使命履行,因而要削减在主线程进步行同步,即使要用也需求十分小心。进行同步操作的线程,一般会加锁后长时间才会将锁开释,不论是隐式锁仍是显示锁。
下面有一些常见需求留意的同步原语:
特别需求留意信号量的运用,由于信号量不运用优先级战略,因而信号量的抢占有或许导致更长时间的卡死。一个常见的过错如下图所示:想经过信号量之间的等候,使异步办法串行履行。必定要在主线程中防止这种操作。
重复获取不变量
还有一种堵塞主线程的原因,是花费许多价值去不断获取一些不常常改变的数据。
比方官方演示 App Desserted 中的这个代表交际功用的按钮,只要当我具有通信录老友的时分才进行展现。咱们能够每次都获取一下通讯录中的联系人,然后判断是否具有老友。但这样做会增加许多额定开支和推迟,由于主线程调用通讯录的相关结构,会在底层重复许多操作,发生昂扬的开支。更何况需求获取的联系人信息并不常常变动,因而没必要频频的获取联系人,这只会增加体系资源的压力。
体系资源受限
卡死:主线程过度运用,导致体系资源受限。
体系资源包含 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 界面大概会这样显示成这样:
- 符号 1 的地方,system trace 输出的红色细线条代表体系办法的调用
- 符号 2 的紫色条形图代表虚拟内存的分页过错
- 符号 3 的水平蓝条,代表主线程在繁忙状况
- 符号 4 的方位,能够挑选想要检查详细的调用信息
- Instrument 会展现这 4.7s 的卡顿内主线程的调用信息,符号为 5 的这部分内容阐明晰
loadAllImages()
导致了其中 4.6s 的卡顿。这便是前文所说的问题,实践上官方示例 App 中加载了过多剩余的图片,导致了卡死。
MetricKit
进一步了解:What’s new in MetricKit – WWDC20
一旦你的 App 发布之后,就能够经过 MetricKit 来收集卡死时的调用栈了,这样能让你发现哪些卡死的调用栈是用户更简略射中的。咱们现在来看一个比如:
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 中的那些配料图片,由于假如每次用届时都去创立就会有许多开支。
经过运用 NSCache 缓存,每次创立图片的许多开支就能被简化为内存的拜访,这样就能消除咱们之前经过 Instrument 分分出,由于过度加载图片而导致的卡死。
不过重要的是,需求有一个精确的缓存检查机制,在维护缓存的增删,确保缓存内容总量的平衡。这些操作一般会异步地在另一个线程进步行,然后确保主线程能够及时呼应其他事情。
增加观察者
增加观察者来监听告诉的计划也能削减主线程作业量,这种办法能针对数值或许状况进行监听,然后防止贵重的核算量。任何类都能够发出告诉,乃至是自定义的类,检查某些类的 API 阐明文档就能够找到相应的事情告诉。能够经过 Apple developer 官方文档中的找到 NSNotification.Name,也便是可监听的体系告诉名称。
而 Desserted 中的交际按钮,便是一个很好的运用示例:经过注册对 abDatabaseChangedExternally 告诉的监听,主线程不再需求等候获取联系人列表的进程了,只需求等候告诉发出、观察者进行呼应就行了。而在 Desserted 的比如中,收到告诉后将会更新一下缓存的联系人列表。而这些更新操作也需求放在另一个异步线程中,这样确保主线程不被卡死。
转移主线程上的作业
核心:主线程只担任满足重要的使命
将主线程上的作业就行转移,给主线程减负,这也是一个好办法。那终究哪些作业该在主线程上履行呢?
只要满足重要的使命,也便是为 UI 展现供给支持的使命才应该在主线程上履行。而且一切视图和视图控制器,他们的创立、修改和毁掉也都应该在主线程上履行。
而用于更新 UI 元素的核算进程,就能够从主线程转移到其他线程上,只需求在核算结束时在主线程履行真正的 UI 更新操作即可,这种形式十分合适与核算耗时很长的使命。一些不太重要的使命,或是对时间不灵敏的使命,都应该像这样转移到其他线程上异步履行。
异步 API
将使命从主线程转移到异步线程,运用异步 API 是最直接的办法。咱们以网络恳求为例,经过运用 NSURL 类的异步 API,咱们就能让主线程在网络恳求期间保持能够呼应:
一般来讲,异步 API 姓名都会包含 “asynchronously” 单词或许其缩写,而 API 的回调办法名一般包含 “completion”,十分好辨认。
GCD 办法
进一步了解:Modernizing Grand Central Dispatch – WWDC17
GCD 是一个功用强大的多线程架构,能够方便地完成异步操作。GCD 供给了十分简略的接口,能够将任何 block 从主线程移至子线程,既能够同步履行也能够异步履行。因而,运用 GCD 办法能够有用地处理绝大部分的卡死问题。经过运用 GCD 的异步调用办法,将 block 转移到另一个线程履行,就能确保主线程不被卡死;一起在完成后调用 completion 办法,再回到主线程履行必要的办法即可:
GCD 也能轻松完成预加载逻辑。将预加载逻辑绑定在一个线程(比方 prefetchQueue)上,再异步履行预加载 block,就能在确保主线程不卡顿的状况下履行预加载使命。而当主线程需求预加载使命的结果时,运用 sync 办法在同一个线程(prefetchQueue)上串行履行后续逻辑即可。
了解交流的价值
以上说的这些办法,本质上来讲都是一些交流,能处理卡死问题的一起也有或许导致其他问题。
- 运用缓存:空间换时间。运用缓存时需求考虑内存的过度增加,有必要要确保缓存的清理机制是有用的。
- 增加观察者:告诉或许被频频触发,因而在监听告诉的时分最好先加一些过滤条件,防止额定的操作,减轻 CPU 的担负。
- 异步 API:有必要要了解哪些操作能够异步履行,特别是直接触及 UI 更新的使命必定不能放到异步线程,由于异步线程的优先级不高,不会优先履行。
- GCD 办法:运用 GCD 的价值便是你有必要要调整代码履行的次序,因而你需求时间保持清醒,防止程序犯错。运用 sync 办法来确保履行次序也是一个不错的挑选。
不过考虑到这些办法能够有用处理卡死问题,这些交流必定是值得的。
一些其他建议
- 尽量运用 Apple 的结构以及接口,由于它们现已针对一切苹果设备都进行了有用地优化,以及会保持不断地更新与优化。
- 不断迭代优化代码。能够不断制定小的优化目标,看到每次优化的效果,再集腋成裘。
- 合理地运用体系资源。过度运用资源会不仅仅下降 App 的功用体会,也会导致整个体系的卡顿。
五、总结
卡死问题是导致用户体会的最大问题之一,当用户遭遇卡死时很或许就退后台杀进程了,这会导致十分高的用户流失率。所以一旦触及到卡死,必定是需求较高优先级处理的。
经过本文的学习,在咱们知道了解原理、善用东西的状况下,剖析并处理卡死问题就变得简略起来。一起期望本文也能起到抛砖引玉的作用,究竟真实的卡死状况愈加杂乱,还有许多需求进一步探究的地方。最重要的是能了解导致卡死的原理,以及学会处理卡死问题的基本思路,遇到问题才干方便的解决。
重视咱们
咱们是「老司机技能周报」,一个继续追求精品 iOS 内容的技能公众号。欢迎重视。
重视有礼,重视【老司机技能周报】,回复「2021」,收取 2017/2018/2019/2020 内参
支持作者
在这里给大家推荐一下 《WWDC21 内参》 这个专栏,一共有 102 篇关于 WWDC21 的内容,本文的内容也来历于此。假如对其余内容感兴趣,欢迎戳链接阅览更多 ~
WWDC 内参 系列是由老司机牵头组织的精品原创内容系列。 现已做了几年了,口碑一直不错。 主要是针对每年的 WWDC 的内容,做一次精选,并召唤一群一线互联网的 iOS 开发者,结合自己的实践开发经历、苹果文档和视频内容做二次创作。