UI를 업데이트 하는 코드를 메인스레드 없이 작성할 때 우리는 `--- must be used from main thread only` 라는 보라색 경고 문구를 볼 수 있다.
아래와 같이 왜 UI는 메인스레드에서 업데이트 될까? 라는 주제에 대해 생각을 정리해본다.
✓ 1. UIKit이 Thead-Safe하지 않은 이유?
먼저 Tread-Safe 라는 용어를 되짚어보자.
Tread-Safe
스레드 세이프는 멀티스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다.
만약 UIKit의 모든 속성이 Thread-Safe하게 설계된다면, 느려짐과 같은 성능저하가 발생할 수 있기 때문에, 대부분의 구성요소는 nonatomic(서로 연결되어있음)으로 기술되어 있다. 즉 Thread-Safe 하지 않다. UIKit이 만약 atomic으로 기술되어 있었다면, Thread-Safe를 보장해주는 block 메커니즘이 필요하다. 이는 자연스럽게 성능저하로 이어진다.
✅ 정리: UIKit의 모든 속성을 Thread-safe하게 설계하면, 느려짐과 같은 성능저하가 발생할 수 있기 때문에 그렇게 설계할 수 없다. (Thread-safe하지 않게 설계한 것은 애플의 의도다.)
✓ 2. 만약 UIKit이 Thead-Safe하여 비동기적으로 UI를 업데이트 할 수 있다면?
만약 UI 업데이트가 비동기적으로 가능하다는 가정을 해보자. 아래의 2가지 방법 중 어떤 방법이 효율적일까?
1) 스레드 자체의 RunLoop를 따르는 방법
2) 비동기적으로 BackGroundThread에서 UI 업데이트가 이루어지는 방법
먼저 RunLoop 라는 용어를 되짚어보자.
RunLoop
작업을 스케줄링하거나, 전달되는 이벤트를 조정하기 위해 사용되는 이벤트 처리 루프를 말한다.
RunLoop의 목적은 스레드가 일할 때는 일하고, 일이 없을 때는 쉬도록 하는 목적으로 고안되었다.
1) UIApplication은 메인 스레드에서 Main RunLoop를 호출해, RunLoop를 초기화하며 앱 내에서 발생하는 이벤트를 담당한다. 이러한 과정을 통해 스크린의 내용이 Refresh 된다. 이때, view의 변경은 즉시 일어나지 않는다. RunLoop 마지막 쯤 Redraw하여 UI가 업데이트 되는데, 이러한 변경을 View Drawing Cycle이라 부른다.
2) 만약 BackGroundThread에서 UI의 변경이 가능하다면, UI가 동시에 변해야하는 상황(화면의 회전 등등 ..)에서 UI들이 동시에 변하지 않는 문제가 발생할 것이다.
✅ 정리: 메인 런루프(Runloop)가 뷰의 업데이트를 관리하는 View Drawing Cycle을 통해 뷰를 동시에 업데이트 하는 그런 설계를 통해 동작하고 있는데, (메인쓰레드가 아닌)백그라운드 쓰레드가 각자의 런루프로 그런 동작을 하게되었을때, 뷰가 제멋대로 동작할 수있다. (예를 들어, 기기를 회전 했을때, 동시에 뷰의 레이아웃이 재배치되는 그런 동작을 못하게 될 수도 있다.)
✓ 3. iOS 렌더링 프로세스 이해하기
UIKit: 모든 종류의 구성 요소를 포함하고 사용자 이벤트를 처리하며 렌더링 코드를 포함하지 않는다.
Core Animation: 모든 뷰를 그리거나 디스플레이하고 애니메이션을 담당한다.
OpenGL ES: 2D 3D 및 렌더링 서버를 제공한다.
Core Graphics: 2D 렌더링 서버를 제공한다.
그래픽 하드웨어: GPU
UIKit은 모든 종류의 구성 요소를 포함하고, 사용자 이벤트를 처리하는 프레임워크라면, Core Animation은 모든 뷰를 그리거나, 디스플레이하고 애니메이션을 담당한다. 따라서 iOS에선 모든 뷰가 UIKit이 아닌 Core Animation Framework에 의해 보여질 수 있다.
Core Animation은 Core Animation Pipe line을 사용하여서 렌더링을 한다.
코어 애니메이션은 위의 파이프라인 방식을 통해 랜더링을 진행하며 이는 4단계로 나뉜다. 위의 파이프라인 과정이 1초당 60번 (60Hz 주사율 기준) 이루어지게 된다.
Commit Transaction: view를 레이아웃하고 이미지를 디코딩하여 Render Server에 이를 전달한다.
Render Server: Commit Transaction으로부터 받은 package를 분석하고 deserialize하여 rendering tree에 보낸다. 이후에 drawing instruction 을 생성하고 VSync Signal을 기다렸다가 화면을 랜더링하기 위해 OpenGL을 호출한다.
GPU: VSync Signal이 떨어지면 OpenGL을 사용하여 랜더링을 시작한다. 랜더링이 끝난뒤에는 buffer로 내용을 전달한다.
Display: Buffer로부터 데이터를 받아서 화면에 띄워준다.
만약 백그라운드 스레드를 활용해서 view를 변경한다면 여러 스레드에서 위의 파이프라인을 시작하는 trigger를 당기게 된다. 위의 파이프라인은 굉장히 비싼 작업이기 때문에 (GPU의 메모리 낭비 극심) 빈번한 context switching은 막는 것이 좋다.
✅ 정리: iOS가 그림의 그리는 렌더링 프로세스(코어애니메이션 -> 렌더서버 -> GPU -> 표시)가 있는데, 여러 쓰레드에서 각자의 뷰의 변경사항 GPU로 보내면 GPU는 각각의 정보를 다 해석해야하니 느려지거나, 비효율적이 될 수 있다.
출처: https://medium.com/@duwei199714/ios-why-the-ui-need-to-be-updated-on-main-thread-fd0fef070e7f
'iOS' 카테고리의 다른 글
[iOS] 네트워크 연결은 코드로 어떻게 확인할까? (1) | 2022.09.29 |
---|---|
[iOS] AppDelegate, SceneDelegate ? (1) | 2022.09.20 |