Why does it take such a long time for UI to be updated from background thread?
Asked Answered
H

1

3

I understand that all UI updates must be done from Main thread.

But purely for the sake of deeper understanding how GCD and dispatch main work:

I have a button that runs a network call and in its completionHandler I eventually do:

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

For the color change to happen it takes 6-7 seconds. Obviously if run the above code from main thread it would change the border color immediately.

Question1 even though I don't have ANY other code to run, why doesn't the UI changes happen immediately from the background thread? What is waiting for?

Interesting though is that if I click the button to make the network call and then tap on the textField itself (before the 6-7 seconds), the border color would change immediately.

Is that happening because of:

From the background thread I've updated the model ie change the textField color which queues the UI/view to be updated...but since we're on a background queue, that UI updated could take a few seconds to happen

But then I tapped on the textField right away and forced a super quick read of the textField and all its properties which includes the border—from main thread (actual user touches are always handled through main thread)...which even though are not yet red on the screen, but since it's red on the model it will read from it and change color to red immediately.

Question2: Is that observation correct?

If I don't tap and just wait:

enter image description here

If I tap:

enter image description here


My full code is as below:

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()
    }

}
Helpmeet answered 5/7, 2017 at 16:40 Comment(11)
The first sentence of your question is wrong: "I understand that all UI updates must be done from background thread." should read "I understand that all UI updates must be done from the main thread". (btw, I'm not the one who down voted you)Phototherapy
@DuncanC oops. Thanks. Edited...Helpmeet
Your observation sounds reasonable and could be correct. But UIKit is not open source. Apple requires that UI updates are done on the main thread, and we can only guess what happens otherwise.Hamilton
@MartinR I'm really trying to align my own brain and get a deeper understanding the decision making of GCD + iOS. Why is waiting for 6-7 seconds? Can you make an educated guess?Helpmeet
Even if I could – it would be just a guess. Unless an Apple engineer happens to come along this thread and answers your question, we can do nothing but guess. – (And guessing is not what SO is for. I am actually tempted to vote to close, but I am unsure what to choose as the reason :)Hamilton
@MartinR OK, no guesses. Let's just keep open. Perhaps an Apple engineer may show up or someone who has knowledge from elsewhere or simply put knows more than usHelpmeet
Why are you spending time messing around with behavior Apple specifically tells you to NOT do? Regardless of what issue you might be trying to get around, you should simply NEVER do this. No exceptions.Rapeseed
@PEEJWEEJ Isn't it obvious that I'm not going to do this. It's just how I get deeper understanding of stuff and get better at debugging. Everybody's different.Helpmeet
To each his own...but the "answer" for this will be the same as with anything you do that the APIs tell you not to do. You should never do it, no one without source-code level access can give a real answer, and the results can change from version to version.Rapeseed
@MartinR Do you have any thoughts or know of any Apple documentation about this comment?Helpmeet
I suspect that even an Apple engineer can't tell you what happens. When you try to update the UI from a background thread, you introduce concurrency bugs. Code operating at the same time on different cores tries to access the same hardware resources at the same time. (Memory, display hardware, etc.)What happens in that case is likely actually not predictable. it's dependent on conditions that vary from run to run, and thus unless you have a hardware multi-core processor emulator, you can't predict what will happen, or tell what happened, exactly. See my answer.Phototherapy
P
6

If you attempt to do UI updates from a background thread, "the results are undefined." The most common effect I've seen is what you describe - very long delays before the update shows up. The second-most common effect I've seen is a crash. The third-most common effect is some sort of drawing artifact.

The results of doing UI updates from a background thread are truly nondeterministic. You've got multiple processor cores accessing the same hardware resources at the same time, and with the exact timing between those accesses being unknowable and infinitely variable. It would be like having a computer with no display but 2 keyboards and 2 mice, and 2 operators editing the same document at the same time. Each person's actions with the keyboard would change the state of the document, and screw up the changes the other person was trying to apply. The cursor would be in the wrong place. The amount of text in the document would be different than expected. The scroll position would be off. etc, etc.

Similarly, if 2 cores are each trying to access hardware resources to do screen refreshes, those accesses will cross and conflict with each other.

As Martin says in his comment, the UIKit code is proprietary, so we can't know the details of what goes wrong. All we know is that bad things happen, so DON'T DO THAT.

Phototherapy answered 5/7, 2017 at 17:1 Comment(7)
hmmm. You opened up the problem nicely. So could it be that the background thread is like: " Why are you doing UI updates using me?!! I'm not the one responsible for managing UI changes...but now since you asked me to let me wait to see if there any more idiotic UI changes done on background and then all I'll group them all together and do it...it may not be perfect...I may just crash or create a drawing artifact. Please don't do this to me again" + I'm guessing Apple may also enforces this delay so developers would catch bugs earlier.Helpmeet
As stated before we can't know for sure since UIKit is proprietary, but I believe is basically due do the priority queues and how they work. Since background queue has low priority it will be executed some time in the future and we cannot know. To understand this better you must have knowledge of how a scheduler works so it will all make sense. I'd recommend Silberschatz or Tanenbaum books about operating systems.Glynis
When you make UI changes from background, does the change still eventually go through the main thread But the time is unknown? Or you could technically have 20 background threads...all making UI changes at unknown times?! From what you said it's the 2nd one, but do you have any reason for that?Helpmeet
@valcanaia "Since background queue has low priority it will be executed some time in the future and we cannot know" <-- I think you got it wrong. If you place a breakpoint which only has a sound at: self.textField.layer.borderColor you will hear it beeps 6-7 seconds before the actual UI change. Clearly the line is executed. But it's misleading...It's not like setting self.textField.layer.borderColor would immediately change the UI, it only changed the model. There are possibly multiple steps that are invisible in between the actual UI change and us simply changing a value.Helpmeet
@valcanaia what DispatchQueue.main.async{...} does is it somehow forces an immediate UI change. Again 'UI change' is different from 'model/code change'Helpmeet
Yes, this is the part where "UIKit is proprietary" appear. Simply imagine that just executing self.textField.layer.borderColor = someColor involves getting each of the variables and then finally sets it. If you do a step by step over this line you'll see Xcode "stuck" in this line some times, one for getting each variable(self, textField, layer, borderColor). The execution of this line is not atomic and, even worst, is on a background thread.Glynis
No, a DispatchQueue.main.async { code } doesn't force anything. The architecture itself "forces" putting the code on the main execution queue, which has higher(the highest) priority than others. For instance if you use .sync instead of .async you will see a difference, you will see that even in the main thread, your UI update won't be executed immediately.Glynis

© 2022 - 2024 — McMap. All rights reserved.