为什么从后台线程更新UI需要这么长时间?

3
我明白所有的UI更新都必须在主线程上完成。
但是,为了更深入地了解GCD和dispatch main的工作原理:
我有一个按钮,运行一个网络调用,在它的completionHandler中最终执行以下操作:
self.layer.borderColor = UIColor(red: 255/255.0, green: 59/255.0, blue: 48/255.0, alpha: 1.0).cgColor
self.layer.borderWidth = 3.0

为了使颜色更改发生,需要6-7秒钟。显然,如果从主线程运行上面的代码,它将立即更改边框颜色。
问题1: 即使我没有任何其他要运行的代码,为什么UI更改不会立即在后台线程中进行?等待了什么?
有趣的是,如果我点击按钮进行网络调用,然后在6-7秒内轻按文本输入框本身,边框颜色将立即更改。
这是因为:
我已经从后台线程更新了模型,即更改了文本字段的颜色,该操作排队更新UI/view...但由于我们处于后台队列中,UI更新可能需要几秒钟才能完成。
但是,我马上轻按了文本输入框,并迫使其快速读取文本输入框及其属性,包括边框——从主线程(实际用户触摸始终通过主线程处理)......即使尚未在屏幕上变为红色,但因为它在模型中是红色的,所以会从中读取并立即更改颜色为红色。
问题2: 这种观察是正确的吗?
如果我不轻按而只是等待: enter image description here 如果我轻按: enter image description here 完整代码如下:
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    @IBAction func isValid(_ sender: Any) {

        let userEmail = textField.text

        let requestURL = NSURL(string: "https://jsonplaceholder.typicode.com")

        var request = URLRequest(url: requestURL as! URL)

        request.httpMethod = "POST"

        let postString = "Anything"

        request.httpBody = postString.data(using: .utf8)

        let task = URLSession.shared.dataTask(with: request) { data, response, error in

            guard let data = data, error == nil else {
                print("error=\(error)")
                return
            }

            if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
            }

            do {

                let json = try? JSONSerialization.jsonObject(with: data, options: [])

                if let _ = json as? [String: Any] {

                    self.textField.layer.borderColor = UIColor(red: 255/255.0, green: 59/255.0, blue: 48/255.0, alpha: 1.0).cgColor
                    self.textField.layer.borderWidth = 3.0

                }

            } catch let error as NSError {
                print(error)
            }

        }
        task.resume()

    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

3
你的问题的第一句话是错误的:“我了解所有UI更新必须从后台线程完成。” 应更改为“我了解所有UI更新必须从线程完成”。(顺便说一句,我不是那个给你点了反对票的人) - Duncan C
@DuncanC 哎呀,谢谢提醒。已经编辑好了... - mfaani
2
你的观察听起来很有道理,可能是正确的。但是UIKit不是开源的。苹果要求UI更新在主线程上完成,否则我们只能猜测会发生什么。 - Martin R
3
你为什么要浪费时间去做苹果明确告诉你不要做的行为?无论你试图绕过什么问题,你都应该永远不要这样做。没有例外。 - GetSwifty
1
@PEEJWEEJ 难道不明显我不会这么做吗?这只是我的学习方式,让我更深入地理解事物并在调试方面变得更好。每个人都不同。 - mfaani
显示剩余6条评论
1个回答

6
如果你在后台线程中尝试进行 UI 更新,“结果是未定义的。” 我看到最常见的影响就是你描述的 - 更新显示之前非常长的延迟。我看到的第二个最常见的影响是崩溃。第三个最常见的影响是一些绘图瑕疵。
从后台线程进行 UI 更新的结果是真正的不确定性。你有多个处理器核心同时访问相同的硬件资源,并且这些访问之间的确切时间是不可知的且无限变化的。这就像拥有没有显示器但有 2 个键盘和 2 个鼠标的计算机,以及 2 个操作员同时编辑同一文档。每个人使用键盘的操作都会改变文档的状态,并破坏另一个人试图应用的更改。光标会处在错误的位置。文档中的文本量与预期不同。滚动位置会偏离。等等。
同样,如果 2 个核心都尝试访问硬件资源来进行屏幕刷新,这些访问将交叉并冲突。
正如 Martin 在他的评论中所说,UIKit 代码是专有的,因此我们无法了解出错的详细情况。我们只知道会发生糟糕的事情,所以不要这样做。

嗯,你成功地打开了这个问题。那么,背景线程是否像这样:“你为什么要使用我来更新UI?!! 我不是负责管理UI更改的人...但是现在既然你叫我等待,看看是否后台有更多愚蠢的UI更改,然后我会将它们全部分组并执行...这可能不完美...我可能会崩溃或创建绘图伪影。请不要再这样对我”,而且我猜Apple也会强制实施这种延迟,以便开发人员能更早地捕捉到错误。 - mfaani
如前所述,由于UIKit是专有的,我们无法确定,但我认为这基本上是由于优先级队列及其工作方式。由于后台队列具有低优先级,因此它将在未来的某个时间执行,我们无法知道确切时间。要更好地理解这一点,您必须了解调度程序的工作原理,这样一切就都会有意义了。我建议阅读Silberschatz或Tanenbaum关于操作系统的书籍。 - henrique
1
当您从后台进行UI更改时,更改是否仍最终通过主线程进行,但时间未知?或者您可以在技术上拥有20个后台线程...所有这些线程都在未知的时间进行UI更改?根据您所说的是第二种情况,但您有任何理由吗? - mfaani
@valcanaia,DispatchQueue.main.async{...} 的作用是立即强制进行 UI 更改。再次强调,“UI 更改”与“模型/代码更改”是不同的。 - mfaani
是的,这就是"UIKit是专有的"出现的部分。只需想象执行self.textField.layer.borderColor = someColor涉及获取每个变量,最后才设置它。如果您逐步执行此行,您会看到Xcode有时会在此行“卡住”,其中一个原因是获取每个变量(self、textField、layer、borderColor)。该行的执行不是原子性的,更糟糕的是,它在后台线程上执行。 - henrique
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接