DispatchQueue.main.asyncAfter 的魔法:Swift 3 中的延迟机制

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:为什么“等待”并不像看起来那么简单

\n\n

想象一下:您正在用 Swift 3 编写一个 iOS 应用。需要显示一个动画,几秒钟后将其隐藏。或者向服务器发送请求,但不是立即发送,而是延迟发送。看起来,这有什么难的呢?直接写 sleep(2) 就搞定了。但如果您这样做,您的应用就会变得像冰块一样:界面卡死,用户开始猛戳屏幕,最终导致差评和评分下降。

\n\n

“How to program a delay in Swift 3” 这个问题在 Stack Overflow 上获得了数千次浏览。这并非偶然:新手们总是踩到同样的坑。在本文中,我们不仅会提供现成的解决方案。我们将深入 底层 了解延迟机制,弄清楚 DispatchQueue.main.asyncAfter 的工作原理,并理解为什么 sleep 是邪恶的。此外,我还会向您展示一个能节省数小时调试时间的技巧。

\n\n

为什么 sleep() 是 Swift 中的反模式

\n\n

让我们先来澄清一个最常见的误解。许多从其他语言(例如,使用 time.sleep() 的 Python)转来的开发者会尝试这样做:

\n\n
sleep(2) // 阻塞当前线程 2 秒\nprint("2 秒过去了")
\n\n

这段代码 会阻塞当前线程。如果您在主线程上调用它——应用就会“休眠”2 秒。用户界面不会更新,按钮无法点击,动画无法运行。如果主线程被阻塞时间过长(通常超过 10 秒),iOS 会直接杀掉您的应用。

\n\n

此外,sleep() 不考虑 RunLoop 的状态。这是一个粗暴的系统调用,阻止系统正常处理事件。在 Swift 3(及更高版本)中,正确的方法是使用 Grand Central Dispatch (GCD)。

\n\n

DispatchQueue.main.asyncAfter:实现延迟的正确方式

\n\n

在 Swift 3 中,最常用且推荐用于延迟执行代码的方法是 GCD 的 asyncAfter 方法。它的用法如下:

\n\n
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {\n    print("2 秒过去了(非阻塞模式)")\n}
\n\n

这里我们告诉系统:“在当前时间之后的 2 秒,在主线程上执行这段代码块”。主线程在此期间保持空闲——动画正常运行,按钮可以点击,用户很开心。

\n\n

但让我们深入挖掘一下。什么是 deadline?为什么是 .now() + 2.0,而不是简单的 2.0?底层又发生了什么?

\n\n

asyncAfter 的剖析:deadline、DispatchTime 和 DispatchQoS

\n\n

asyncAfter 接受一个类型为 DispatchTime 的参数 deadline。这不仅仅是一个毫秒数,而是一个 绝对时间点,与系统时钟绑定。当您写 .now() + 2.0 时,GCD 会计算:“当前时间 + 2 秒”,并将任务以此时间戳放入队列。

\n\n

在底层,GCD 使用 mach_absolute_time()——一个硬件定时器,不受系统时间更改的影响。也就是说,即使用户将时钟拨回,您的延迟也不会出错(这与 Timer 不同,后者可能会表现出不可预测的行为)。

\n\n

一个重要细节:deadline 是 执行前的最短时间。GCD 不保证代码会在恰好 2 秒后执行。如果系统繁忙(例如,正在进行复杂的数据处理),执行可能会延迟。对于大多数 UI 任务来说,这并不关键,但对于高精度定时器(例如,在游戏中),最好使用 CADisplayLink。

\n\n

另一个参数是 qos(服务质量)。您可以指定任务的优先级:

\n\n
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1.0) {\n    // 高优先级任务\n}
\n\n

博客

书籍推荐