iOS: Why the UI need to be updated on Main Thread

Dywanedu
6 min readMay 30, 2019

--

During developing, sometimes we may call UIKit components on background threads, maybe in a background network callback we call imageView.image = anImage`, or call UIApplication.sharedApplication` on background threads. When this happened, we will get a runtime error, and we will fix them immediately.

But think it carefully, why do we have to update the UI in the main thread? What happens if the UI is being updated on background threads? Isn’t it better to update the UI on background threads in order to avoid blocking the main thread? This article is based on such questions.

Start From Why UIKit is not Thread-Safe

As we can see, most of the components in UIKit is described as nonatomic`, this means there are not thread safe. And it is unrealistic to design all the properties as thread-safe in UIKit because it is such a huge framework. Designing a thread-safe framework is not just change nonatomic to atmoic or just add an NSLock , it also relates to many problems:

  • Assume we can change view’s properties asynchronously, shall these changes become effective at the same time, or follow each threads own RunLoop?
  • If a UITableView remove a cell on a background thread, then another background thread operate this cell’s index, it may causes crash
  • If a background thread remove a view, and this thread’s RunLoop is not over, at the same time the user tap this “will be removed view”, so should I respond to the touch event? Which thread to respond?

If we think deeper, it seems there has no such many benefits. And if we try to solve the problems above, we can easily get a conclusion: “If I handle all this stuff in a Serial queue, nothing wrong will happen” . And that was how Apple thinks, so UI need to be operated on main thread synchronously.

Thread-Safe Class Design also said the same things:

It’s a conscious design decision from Apple’s side to not have UIKit be thread-safe. Making it thread-safe wouldn’t buy you much in terms of performance;it would in fact make many things slower. And the fact that UIKit is tied to the main thread makes it very easy to write concurrent programs and use UIKit. All you have to do is make sure that calls into UIKit are always made on the main thread.

Now assume we use magic to refactor the UIKit, this magical UIKit can perfectly solve the problems above. Can we update the UI on background threads now?

Sorry, we still can’t.

Runloop and View Drawing Cycle

As we known, UIApplication will init a RunLoop on main thread, which calls Main RunLoop , it will handle most user event during the application life time such as user interactive and so on. It has been in the loop of constantly processing events and hibernation to ensure that user events can be responded as soon as possible. The reason why the screen can be refreshed is because `Main Runloop` is driving.

Also every view’s changes will not change immediately, they will redraw at the end of current RunLoop. This ensure the application can handle all the changes for all the view, and all the changes can become active at the same time. This is called “View Drawing Cycle

Assume we use our Magical UIKit, and update UI on background threads. Problems come when we need to rotate our device and need to refresh our layout, because each thread has it’s own RunLoop, so all the changes can not be apply at the same time, this will lead to after the device rotated, there are some views did not rotate.

Also because the Magical UIKit is not on main thread, so the user events in Main RunLoop is not synchronized with display.

OK, but how about I refactor whole UIApplication user event mechanism, and now it can handle the problem of thread synchronous, can we update UI on background threads now?

Sorry, we still can’t.

Understanding iOS rendering process

Rendering framework

  • UIKit: Contains all kinds of components, handles user events, it does not contain any rendering code.
  • Core Animation: Responsible for drawing, displaying and animating all views.
  • OpenGL ES: Provide 2D and 3D rendering server.
  • Core Graphics: Provide 2D rendering server.
  • Graphics Hardware: GPU

So in iOS, all views are display and animate by Core Animation Framework, not UIKit.

Core Animation Pipeline

Core Animation use Core Animation Pipeline to rendering, which is divided into four steps.

  • Commit Transaction: Layout views, handle image decoding and format conversion operations, pack up view layer and send to Render Server.
  • Render Server: Rendering, analyze the package sent from Commit Transaction and deserialization into a rendering tree. Then it will generate drawing instructions by view layers’ properties, and call OpenGL to render screen when the next VSync Signal comes.
  • GPU: GPU will wait for screen’s VSync Signal, then use OpenGL rendering pipeline to render. After rendering the output will send to buffer.
  • Display: Get data from buffer, and send to screen to display.

So in Core Animation Pipeline, we want it can finishes prepare works in 1/60s and send data to render server, then finishes rendering in 1/60s, in this way, our application will not stuck.

But if we use our Magical UIKit, many background threads update UI, so at the end of the Runloop, when the screen needs to rendering, problems come. Because each thread commits different render information, so we have to handle more Commit Transactions, so the Core Animation Pipeline will commit informations to GPU all the time. However rendering is actually a very expensive operation of the system resources (occupying video memory and CPU), frequent context switching between threads and a large number of transactions causes the GPU to be unprocessable, which in turn affects performance, resulting in the inability to complete the layer tree submission in 1/60s, resulting in severe stalls.

But I really what to update UI on background threads, what can I do?

OK, there still have some way.

Texture or ComponentKit

AsyncDisplayKit(Texture) is a framework developed by Facebook to keep iOS application smooth.

ComponentKit is also a view framework developed by Facebook, it is heavily inspired by React. It takes a functional, declarative approach to building UI.

Now come back to our normal UIKit, return to the world that UI can only be updated on main thread.

These two framework are not actually update UI on background threads, they use a much more clever way to put some time-consuming operations asynchronously, bypassing the limitation that UI can only be updated on the main thread.

Texture create some classes named: Node , Node contains UIView , and Node itself is thread-safe, so we can operate Node on background thread, and when the Node ‘s properties change, instead of change immediately, it will apply them to view on main thread at proper timing, which a little sound like the View Drawing Cycle above.

ComponentKit just create Component classes to “describe” UI, Component is also thread-safe. We can image Component is a stencil plate, view is a paper under the stencil plate, rendering just like ink. So when we create a Component , it means we create a view’s stencil plate, so rendering just need to follow the stencil plate, then it can render the view we need.

Conclusion

“We can not update UI on main thread.” Maybe all iOS developers known it, but have you ever think of why? When we dig deeper, we will find there have many knowledge, and these knowledge is often overlooked by us.

Coding has never been a simple thing.

More funny things on my Github and my blog(Chinese).

Reference

--

--