출처: https://medium.com/@duwei199714/ios-why-the-ui-need-to-be-updated-on-main-thread-fd0fef070e7f
iOS: Why the UI need to be updated on Main Thread
Do you ever think about why UI really MUST to be updated on main thread? What will happened if we turn UIKit into thread-safe design?
medium.com
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 |
출처: https://medium.com/@duwei199714/ios-why-the-ui-need-to-be-updated-on-main-thread-fd0fef070e7f
iOS: Why the UI need to be updated on Main Thread
Do you ever think about why UI really MUST to be updated on main thread? What will happened if we turn UIKit into thread-safe design?
medium.com
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 |