性能
主线程只负责提交这一帧
卡顿先分清是单帧超时还是主线程被占住几秒。两种问题用的工具不一样。
60Hz 下一帧大约 16.7ms,ProMotion 下更短。主线程要在截止时间前完成事件、布局、绘制和提交。超时一帧,滚动会顿一下,这是 hitch。主线程连续几百毫秒不返回,界面像死了,这是 hang。先分清你在修哪一种。
什么不该在主线程
JSON 解码、图片解码、数据库查询、文件读写、压缩、正则扫一大段文本,都不该占着主线程。判断标准不是“这段代码短”,而是它的输入会不会变大。今天 20KB 的配置明天可能是一份完整目录。
常见的假异步:后台线程算完,主线程又把结果再处理一遍,比如把模型数组 map 成另一种模型、算高度、拼富文本。真正贵的那一步还在主线程上。
struct FeedItem: Sendable {
var id: String
var title: String
}
func loadFeed(from data: Data) async throws -> [FeedItem] {
try await Task.detached(priority: .userInitiated) {
let decoded = try JSONDecoder().decode([FeedItem].self, from: data)
return decoded
}.value
}
回到主线程只做赋值。UIImageView.image = 已经解码好的图,table 的 reload 或 snapshot apply,这些才是主线程的活。从主线程调用 DispatchQueue.main.sync 会直接死锁,这种代码不要留在重试逻辑里。
怎么看
- 滚动发涩:用 Animation Hitches,看哪一帧超时,以及超时是发生在 layout 还是 render。
- 点按后卡住:用 Hangs,或者 Time Profiler 看主线程最深的那条栈。Invert Call Tree,Hide System Libraries,先看你自己的符号。
- 怀疑被系统检测:打开 Main Thread Checker。它抓的是 UIKit 在后台被碰,不是性能,两件事不要混。
QoS 也要配对。用户正在等的跳转用 userInitiated。统计、清理用 utility。把重活丢到 default 再在里面抢主线程,队列会反过来堵。
修的时候给嫌疑区域打 os_signpost,区间要包住完整工作,而不是只包一行赋值。下一次录制能直接看到这段占了多少毫秒,比在 Time Profiler 里猜方法名快。