在 DispatchQueue.main.async 中完成某些处理时,更新 UI 控件会出现延迟
Posted
技术标签:
【中文标题】在 DispatchQueue.main.async 中完成某些处理时,更新 UI 控件会出现延迟【英文标题】:Getting a lag in updating UI controls when some processing done in DispatchQueue.main.async 【发布时间】:2018-06-15 12:35:06 【问题描述】:尝试从 DispatchQueue.main.async 闭包更新 UI 控件,该闭包执行一些处理并需要几百毫秒或更长时间,UI 标签的更新会有几秒到几秒的延迟。如果没有延迟或延迟很短,则 UI 中的标签会随着代码的运行而更新,并且似乎是瞬时的。
我有这个小例子来说明我添加了一个“毫秒等待”函数来模拟处理时间并显示 UI 更新延迟发生的问题。
在示例中,waitForMilliSecs 设置为 300 或更少,标签会立即更新。任何大于 300 的数字,都会出现更新标签几秒到几秒的延迟。日志消息表明代码已经运行,理想情况下应该在打印出来时更新 UI。
class ViewController: UIViewController
@IBOutlet weak var label1: UILabel!
@IBOutlet weak var label2: UILabel!
override func viewDidLoad()
super.viewDidLoad()
DispatchQueue.main.async
os_log("before")
self.label1.text = "updated label 1 1111"
self.label2.text = "updated label 2 2222"
self.waitForMilliSecs(MilliSecs: 300)
os_log("after")
func waitForMilliSecs(MilliSecs millisecs: Int) -> Void
var date = NSDate()
let firstTime = Int64(date.timeIntervalSince1970 * 1000)
var currentTime = firstTime
while currentTime - firstTime < millisecs
date = NSDate()
currentTime = Int64(date.timeIntervalSince1970 * 1000)
真正的用例是我正在抓取 html 页面的数据,然后使用页面的一些内容更新 UI。完成处理程序是从后台线程上的 URLSession.shared.dataTask 调用的,因此 DispatchQueue.main.async 闭包用于更新主线程上的 UI。
有没有更好的方法来更新 UI?有没有办法强制更新主线程上的事件?
【问题讨论】:
UI 更新在主线程上完成。任何消耗足够时间来减慢 UI 更新的事情都可以而且应该在备用线程上完成。在后台线程上处理您的 HTML 页面,然后在主线程上更新 UI 元素。 感谢您的意见。我将 html 页面检索代码放入 DispatchQueue.global(qos: .userInitiated).async 线程,现在我得到了更好的结果。 如果这是一个“新延迟”并且您运行的是最新的 ios 版本,那么您可能会看到 Apple 由于电池问题而导致速度变慢。我们已经看到运行最新操作系统的几个问题,因此您可能也会遇到。 我正在运行最新的操作系统版本。 【参考方案1】:没有比在主线程上更新 UI 更好的方法了。
但你所做的并不完全正确。您也在主线程上进行处理(函数 waitForMilliSecs)。这可能不是你想要的。 您需要在后台线程上进行处理,并在处理完成后,在主线程上更新 UI。
override func viewDidLoad()
super.viewDidLoad()
DispatchQueue.global().async
print("before")
//this function is doing some real work and produces some results.
self.waitForMilliSecs(MilliSecs: 3000)
print("after")
DispatchQueue.main.async
self.label1.text = "updated label 1 1111"
self.label2.text = "updated label 2 2222"
github repo 展示了整个例子: https://github.com/jurajantas/TestOfBackgroundProcessing.git
【讨论】:
我将此代码放入我的测试应用程序中,但它在更新 UI 中的标签时仍然会有几秒到几秒的随机延迟。我希望在 X 毫秒的等待延迟之后,UI 会更新。但事实并非如此。我对何时应该更新的假设是否不正确? 在日志控制台中看到“之后”后会更新 UI。 如果您有最新的 xcode (9.2),则有一个名为 Thread sanitizer 的新功能。当您从后台线程更新 UI 时,它会警告您。如果您确实从后台线程更新 UI,您会看到更新速度较慢(而且它不是线程安全的)。更多信息在这里:developer.apple.com/documentation/code_diagnostics/… 标签更新发生在“after”打印后几秒到几秒之间。我希望标签会更新,并且与打印“之后”发生的时间相同。我使用 xcode 9.2 并且在错误的线程中看到了有关 ui 更新的警告。尽管随着后台线程的更改,这不再发生。顺便说一句,谢谢你的cmets 我创建了 github repo 来展示它是如何工作的。 UI 立即更新。 github.com/jurajantas/TestOfBackgroundProcessing.git【参考方案2】:主线程是同步的。在您的示例中,您正在使用循环加载主线程,阻止 UI 更新。
至于您的实际用例 - 所有 UI 都必须在主线程中更新(否则可能会发生一些不可预测的伪影,包括部分更新和意外的颜色更改)。 GCD (DispatchAsyn) 是最自然的方式,但是许多第三方可用于简化异步操作。点赞https://cocoapods.org/pods/ResultPromises
【讨论】:
感谢有关帮助此类操作的第 3 方工具的信息。我会调查这个【参考方案3】:当您在waitForMilliSecs
函数中使用值 300 时,它看起来是瞬时的,但不是。这只是一小段时间,您不会注意到当您的代码在主线程上旋转时 UI 已锁定。
调用self.label1.text = "updated label 1 1111"
后文本没有立即更新的原因是UI 更改不会立即发生。 UI 以特定速率(60hz 或 120hz)更新。您所做的每项更改都将在下一个渲染周期中显示在屏幕上。
在等待时检查 300 毫秒切片期间的 CPU 使用率。你会看到它飙升到接近 100%,这是一件非常糟糕的事情。
你最终想要做什么?
【讨论】:
我要做的是检索网页,从页面中提取数据,然后将数据设置为一些标签并更新表格。在 viewDidLoad() 方法中,我将 URLSession.dataTask(with: request, completionHandler:...) 放入 DispatchQueue.global(qos:...).async() 闭包中,以确保请求在后台.完成处理程序在 UILabel 和 table.reloadData() 调用周围有 DispatchQueue.main.async。 UI 更新虽然有延迟,但更新 UI 需要几秒到几秒钟。我观察了调试器中的线程和调用,调用似乎在正确的背景和主线程上。为什么我尝试等待循环是为了测试何时发生此问题我的页面检索和 HTML 处理大约需要 0.5-.7 毫秒,我试图查看这是否是时间问题。 更正:应该是“.5-.7 秒”,而不是 0.5-.7 毫秒【参考方案4】:您尝试过 CADisplayLink 吗?默认情况下,它每 60 秒调用一次屏幕刷新(这解决了我的一些问题):https://developer.apple.com/documentation/quartzcore/cadisplaylink
let displayLink = CADisplayLink(target: self, selector: #selector(updateUI))
displayLink.add(to: .current, forMode: .common)
然后:
@objc func updateUI()
print("Updating UI!")
【讨论】:
以上是关于在 DispatchQueue.main.async 中完成某些处理时,更新 UI 控件会出现延迟的主要内容,如果未能解决你的问题,请参考以下文章
在 React 应用程序中在哪里转换数据 - 在 Express 中还是在前端使用 React?