출처: https://medium.com/@duwei199714/ios-why-the-ui-need-to-be-updated-on-main-thread-fd0fef070e7f
UIKit 의 모든 속성을 Thread-Safe 하게 설계하면, 느려짐 뿐만 아니라 다양한 문제를 야기한다.
대부분의 UIKit 컴포넌트들은 Not Thread-Safe 이라는 의미인 nonatomic 이라고 표현된다.
그리고 UIKit 은 너무나 큰 프레임워크이기 떄문에 UIKit 의 모든 속성을 Thread-Safe 하게 설계하는 것은 비현실적이다. Thread-Safe 한 프레임워크를 설계하는 것은 단순히 nonatomic → atomatic 의 변형, NSLock 의 추가 뿐만이 아니라 많은 문제를 야기한다.
여러 가설을 세워봤을 때 다양한 문제가 발생한다.
가설 ① : View 의 프로퍼티들을 비동기적으로 변경할 경우, 이러한 변경 사항들이 동시에 적용되는지, 각 스레드의 RunLoop 를 따를 것인지 어떻게 정할 것인가?
가설 ② : 만약 UITableView 의 Cell 을 Background Thread 에서 삭제할 때, 다른 Background Thread 에서 해당 Cell 을 호출할 때 발생하는 Crash 는 어떻게 처리할 것인가?
등등...
이렇게 발생하는 여러 문제를 해결하는 방법은 "Serial Queue 에서 이를 담당하는 것"
즉, 하나의 Thread 에서 View 를 그리는 모든 Task 를 담당하는 것이며, 이로써 UI 를 그리는 작업은 Main Thread 에서 담당하도록 하였다.
각 Background Thread 마다 독자적인 RunLoop 로 View 를 업데이트 할 경우 에러 발생
메인 런루프(Main Runloop)가 뷰의 업데이트를 관리하는 View Drawing Cycle 을 통해 View를 동시에 업데이트 하는 방식으로 동작하고 있는데,
(Main Thread 가 아닌) Background Thread 가 각자의 런루프로 View 를 업데이트 할 경우, View가 제멋대로 동작할 수있다.
(예를 들어, 기기를 회전 했을때, 동시에 뷰의 레이아웃이 재배치되는 그런 동작을 못하게 될 수도 있다.)
※ UIApplication 은 Main Thread 의 RunLoop 인 Main RunLoop 를 초기화하는데, 이를 통해 App 의 생명주기 동안 발생하는 대부분의 User Event 를 처리할 수 있으며, User Event 가 가능한 한 빨리 응답될 수 있도록 이벤트를 지속적으로 처리한다.
즉, 화면을 새로 고칠 수 있는 이유는 Main RunLoop 가 동작하기 때문이다.
또한 모든 뷰의 변경 내용은 즉시 변경되지 않고 현재의 RunLoop 의 마지막에 다시 그려지는데(redraw), 이를 통해 Application 은 모든 View 에 대한 모든 변경 내용을 동시에 처리할 수 있다. 이러한 과정을 "View Drawing Cycle" 이라고 한다.
그런데 여러 Background Thread 에서 UI 업데이트를 담당한다고 가정해보고, 다음과 같은 과정을 수행한다고 생각해보자.
→ 디바이스를 회전한 후, 레이아웃을 새로고침 해주세요
각 Background Thread 는 각자의 RunLoop 를 가지기 때문에, 모든 변경 내용을 동시에 처리할 수 없을 것이다. 결과적으로 일부 View 들은 회전되지 못한채로 남을 수도 있다.
그러므로 하나의 Thread 에서 UI 의 업데이트를 담당한다면, 독자적인 RunLoop 의 실행을 통해 마지막에 Redraw 하는 과정으로 모든 View 에 업데이트가 동시에 처리될 수 있으므로, Main Thread 에서 UI 업데이트를 담당해야 한다.
iOS가 View 를 디스플레이하는 렌더링 프로세스 중, 여러 Background Thread 에서 View 의 변경 사항을 GPU로 보내게 되면, GPU는 각각의 정보를 다 해석해야하니 느려지거나, 비효율적이 될 수 있다.
Rendering Framework 를 확인해보면 모든 View 들은 UIKit 이 아니라 Core Animation Framework 에서 Display 되고 Animate 된다.
Core Animation 은 Core Animation Pipeline 을 통해 렌더링을 수행하는데, 크게 4가지로 분류할 수 있다.
① Commit Transaction : View Layout, 이미지 디코딩 처리, View Layer Packing 등 수행 후 Rendering Server 로 전달
어떤 자료구조로 Commit Transaction 을 수행할까?
② Rendering : Commit Transaction 을 통해 받은 Package 를 렌더링하고, 분석하고 deserialization 하여 Rendering Tree 로 전달한다. 이후 View Layer 의 프로퍼티 별로 Drawing instruction 을 생성하고, 다음 Vsync 신호가 오면 OpenGL 을 호출하여 화면을 렌더링한다.
③ GPU : GPU는 화면의 Vsync 신호를 기다렸다가, OpenGL 렌더링 파이프라인을 통해 렌더링한다. 렌더링 후 Output 을 Buffer 로 전송한다.
④ Display : Buffer 에서 데이터를 가져온 후, 스크린에 Display 한다.
Core Animation Pipeline 에서, 1/60 초 동안 이러한 작업 준비를 마치고, 다음 1/60초 안에 Rendering Server 에 데이터를 전달한다. 이러한 방식으로 Application 은 Stuck되지 않는다.
그러나 Background Thread 에서 UI 업데이트를 담당한다고 가정해보았을 때, Background Thread 에서 RunLoop 가 끝나고, 화면이 렌더링 할 경우 문제가 발생한다.
각 Thread 는 서로 다른 Rendering Information 을 Commit 하기 때문에 더 많은 Commit Transaction 을 처리해야 하고, Core Animation Pipeline 은 항상 GPU 에게 정보를 Commit 해야 한다.
하지만 Thread 들 간의 빈번한 컨텍스트 전환은 GPU 를 처리할 수 없게 만들고, 이는 결국 Layer Tree Submission 을 1/60 초 이내에 전달하지 못하는 성능 저하를 야기한다.
이를 방지하기 위해 UI 작업을 메인 스레드에서 업데이트 한다.
' iOS > Swift' 카테고리의 다른 글
AppDelegate.swift 와 @main (0) | 2022.10.13 |
---|---|
App Thinning 이란? (2) | 2022.10.06 |
iOS 4계층 (0) | 2022.09.18 |
Breakpoint 과 디버깅 버튼 / 단축키 (0) | 2022.09.18 |