[iOS] 메인 큐에서 UI 업데이트를 하는 이유는?

Youngwoo Lee·2021년 12월 25일
2

iOS

목록 보기
40/46
post-thumbnail

포트폴리오나 과제전형에서 DispatchQueue를 사용하게 되면 항상 "왜 항상 main thread에서 UI 업데이트를 하는지 설명해보세요" 라는 질문을 받는다. 알고있지만, 항상 입에서 안떨어진다...

왜 백그라운드 스레드에서 UI를 업데이트하면 안되는지 알아가보도록 하자!


참고자료

해당 글은 iOS: Why the UI need to be updated on Main Thread 를 참고했습니다.

UIKit이 Thread-safe 하지 않은 이유

우리는 메인 스레드에서 무조건 UI를 업데이트 해야되는 것을 알기 이전에, 먼저 UIKit이 Thread-safe하지 않은 것에 대한 이유를 알아야 한다. 만약 스레드 세이프 했다면? main Thread에서 UI를 serial하게 처리하지 않았겠죠!


UIKit의 대부분 구성요소들은 nonatomic(서로 연결된)으로 구현되어 있다. 이는 Thread-safe 하지 않다는 것을 의미한다. 또한, UIKit에서 모든 속성들을 Thread-safe 하게 디자인하는 것은 UIKit이 너무 방대하기 때문에 사실 불가능하다고 한다.

왜냐하면 Thread-safe 프레임 워크를 설계하는 것은 nonatomic(서로 연결된)을 atomic(서로 독립적)으로 변경하거나 NSLock을 추가하는 것뿐만 아니라 많은 문제와 관련이 있다고 한다.


만약 Thread-safe해서 비동기적으로 UI를 적용할 수 있다면?

뷰의 속성을 비동기적으로 변경할 수 있다고 가정하고, 이러한 변경 사항이 동시에 적용되는 것(비동기적 변경)이 과연 효율적일까? 아니면 각 스레드 자체의 RunLoop(메인에서 한꺼번에 변경)를 따르는 것이 효율적일까?

  • 만약 UITableView가 백그라운드 스레드에서 셀을 제거하고, 다른 백그라운드 스레드가 이 셀의 인덱스를 작동하면 크래쉬가 발생할 수 있다.

이러한 문제들을 가장 쉽게 해결할 수 있는 방법은 현재 UIKit이 설계되어 있는 것처럼 이 모든 것들을 시리얼 큐에서 처리하는 것이다. UIKit이 Thread-safe 하지 않게 설계되었다고 해도 사용할 때는 우리가 의도한 것처럼 작동할 것이다.


RunLoop 및 Drawing Cycle

앱의 User Interaction을 처리하고 UI 업데이트를 하는 Run LooP와 Drawing cycle에 대해서도 알아보자!

앱의 Entry Point라고 할 수 있는 main 함수가 실행되게 되면, UIApplicationMain 함수가 실행되게 되고, UIApplication 객체가 생성된다. UIApplication은 메인 스레드에서 Main Run Loop를 호출해, Run Loop를 초기화하고, 그것은 앱의 Life Cycle 동안 대부분의 사용자 이벤트를 처리합니다. 이런 Run Loop는 사용자 이벤트를 가능한 빨리 응답할 수 있도록 지속적으로 이벤트를 처리를 하고 절전 시키는 일 등을 처리하는 지속적 반복 과정에 있습니다. 화면을 새로 고칠 수 있는 이유는 바로 Main Run Loop가 동작 중이기 때문이죠

또한 모든 뷰의 변경 사항은 즉시 변경되지 않으며, 현재의 Run Loop 끝에서 다시 그려집니다. 이를 통해서 앱이 모든 뷰에 대한 모든 변경 사항을 처리할 수 있다는 것을 보장할 수 있으며 모든 변경 사항이 동시에 활성화될 수 있습니다. 이것을 우리는 "View Drawing Cycle" 이라고 불러왔죠!

백그라운드 스레드에서 UI를 업데이트한다고 가정해보자! 각 스레드마다 자체 Run Loop가 있기 때문에 스마트폰을 회전했을 때, 레이아웃을 새로 고쳐야 할 때 문제가 발생할 수 밖에 없습니다. 모든 변경 사항을 동시에 적용할 수 없으므로 스마트폰을 회전시키고 나서 몇 개의 뷰는 회전하지 않은 것을 보게 될 수도 있습니다.


렌더링 과정에서의 성능상 차이점


UIView의 프로퍼티인 layer에 대해서 학습을 하다보면 다들 CALayer 타입에 대해서 공부해봤을 테고, 그리고 CA(Core Animation)의 역할에 대해서 알아봤을 것이다. UIKit이 "모든 종류의 구성 요소를 포함하고 사용자 이벤트를 처리하는 프레임워크" 라면, Core Animation은 "모든 뷰를 그리거나, 디스플레이(표시) 하고 애니메이션하는데 책임이 있습니다. 따라서 iOS에서는 모든 뷰가 UIKit이 아닌 Core Animation Framework에 의해서 표시되고 애니메이션될 수 있습니다.


Core AnimationCore Animation Pipe line을 사용하여서 렌더링을 하며, 해당 Pipeline에서는 1/60초 후에 작업을 준비하고 렌더링 서버로 데이터를 보낸 다음, 1/60 초 후에 렌더링을 완료할 수 있기를 원합니다. 이러한 방식으로 앱이 멈추지는 않습니다.

그러나 백그라운드 스레드에서 UI를 업데이트하게 되면, Run Loop가 끝나고 화면을 렌더링할 때 문제가 발생합니다. 각 스레드는 서로 다른 렌더링 정보를 통해서 파이프라인을 진행해야 됩니다. 각 스레드는 서로 다른 렌더링 정보를 커밋하므로 더 많은 커밋 트랜잭션을 처리해야하므로 Core Animation Pipeline은 항상 GPU에 정보를 커밋합니다. 그러나 렌더링은 실제로 시스템 리소스 (비디오 메모리 및 CPU 점유)의 비용이 많이 드는 작업이므로 스레드와 많은 트랜잭션간에 빈번한 컨텍스트 전환으로 인해 GPU를 처리할 수 없어 성능에 영향을 미치므로 완료할 수 없습니다. 1/60 초의 레이어 트리 제출로 심각한 중단이 발생하게 됩니다.


정확히 무슨 소리인지는 모르겠지만, 모든 스레드가 각각의 렌더링을 진행할 시에 GPU 렌더링 작업 비용이 높으므로 성능상 좋지 않다는 말 같다...

결론

  • UIKit은 서로 연결되어 있는 요소들이 너무 많아서 Apple은 UIKit을 스레드 세이프하지 않게 설계하였다.
  • 이런 프레임워크에서 일어나는 일련의 문제들을 해결하기 위해서는 Main Queue(Main Run Loop)를 두고 UI 업데이트를 하나의 큐에서 처리하는 것이 가장 효율적이다.
profile
iOS Developer Student

0개의 댓글