[번역] 메인쓰레드에서만 왜 UI를 업데이트해야될까?

도윤·2022년 11월 1일
0

UIkit

목록 보기
1/1
post-thumbnail

https://medium.com/@duwei199714/ios-why-the-ui-need-to-be-updated-on-main-thread-fd0fef070e7f

해당 글을 번역한 것입니다.


개발하면서 imageView.image = asImage 혹은 UIApplication.sharedApplication와 같은 UIKit Components를 Background Thread에서 호출할지도 모른다.
이렇게 코드를 치면 Runtime error가 발생하고 즉각 고쳐야 한다.

신중하게 생각해보면 왜 메인 쓰레드에서 UI를 업데이트 해야될까?
UI를 background 쓰레드에서 업데이트된다면 무슨일이 발생할까? 메인 쓰레드내에서 blocking을 막기 위해 background 쓰레드에서 UI를 업데이트하는게 더 좋지 않을까?

Start From Why UIKit is not Thread-safe

UIKit내의 대부분의 Components들은 nonatomic하며 이것은 thread safe하지 않는다. 모든 프로퍼티가 thread-safe하도록 설계되는 것은 매우 비현실적이다. 그렇게 되면 framwork가 매우 비대해진다.

thread-safe한 framework으로 설계하는 것은 nonatomic에서 atomic하게 만들고 NSLock또한 처리해야 한다.

  • 변화들이 동시에 효율적으로 변하고 각 쓰레드는 각자의 RunLoop을 소유하고 있으며 뷰의 프로퍼티들을 비동기적으로 변화한다고 가정하자.
  • 만약 UITableView가 Background Thread에서 제거된 후 다른 쓰레드가 cell의 index에 접근한다면 crash가 발생할 것이다.
  • 만약 Background Thread에서 View를 제거하고 동일한 쓰레드의 RunLoop가 끝나지 않는 동시에 will be removed view가 tap된다면 어떠한 이벤트에 반응해야될까? 어느 쓰레드가 반응해야될까?

위 사항을 봤을 때 background thread에서 실행했을 때 큰 이점이 없어보인다. 우리가 이 문제를 해결하려고 접근을 해보면, "Serial Queue로 이것들을 다루면 문제가 발생하지 않을 것이다"라고 쉽게 결론을 내릴 수 있다. 그래서 UI는 main thread에서 동기적으로 동작될 필요가 있다.

Apple측에서 thread-safe하지 않도록 하는 것은 의도된 설계이다. thread-safe하게 만드는 것은 성능적인 면에서 이점이 없다. 이것은 사실 더 느리게 만든다. UIKit이 메인스레드에서만 작동한다는 사실은 프로그램을 동시적으로 만들기 더 쉽다. 당신이 해야할 것은 UIKit이 메인스레드에서만 작동하도록 하면 된다.

지금 UIKit을 리팩토링하고 위에서 발생한 문제를 해결했다고 가정을 하자. 그렇게 되면 UI를 Background thread에서 업데이트할 수 있을까?

Runloop and View Drawing Cycle

UIApplication은 메인스레드에서 Runloop(Main RunLoop)를 시작할 것이다. 이것은 어플리케이션 주기(Application lift time)동안에 대부분의 유저 이벤트를 다룬다. 사용자 이벤트에 가능한 빨리 응답할 수 있도록 이벤트를 지속적으로 처리하고 최대 절전 모드를 사용하는 루프에 있다. 스크린이 refresh할 수 있는 이유는 'Main Runloop'이 실행중이기 때문이다.(driving)

또한 모든 뷰의 변화가 즉각적으로 이뤄지지 않고 뷰들은 현재 Runloop의 끝에 다시 그린다(redraw). application은 모든 뷰의 변화를 다룰 수 있고 모든 변화는 동시에 active된다. 이것을 "View Drawing Cycle"이라고 부른다.

Magical UIKit을 우리가 사용하고 Background threads에서 UI가 업데이트 된다고 가정해보자. 문제는 우리가 device를 회전시키고 layout을 업데이트할 때 발생한다. 각 쓰레드의 고유한 Runloop이 있기 때문이다. 그래서 모든 변화는 동시에 적용될 수 없고 디바이스가 회전된 이후에 몇몇 뷰의 loyout은 업데이트되지 않는다.

또한 Magical UIKit이 메인스레드에서 있지 않기 때문에 Main Runloop내에 있는 유저 이벤트가 동기적으로 보여지지 않는다.

만약 문제를 동기적으로 처리를 한다면 Background Thread에서 처리할 수 있을까?

Understanding iOS rendering progress

Rendering framework

  • UIKit: 모든 종류의 components를 포함하고 있고, user events를 다루고 모든 rendering code를 다루고 있지 않는다.
  • Core Animation: 모든 뷰를 animating하고 displaying, drawing을 담당
  • OpenGL ES: 2d, 3d 렌더링 서버 제공
  • Core Graphics: 2d sendering 서버 제공
  • Graphics Hardware: GPU

Core Animation은 4가지 스텝으로 나눠진 렌더링을 하기 위해 Core Animation Pipeline을 사용한다.

  • Commit Transaction: views를 배치하고 image를 decoding 및 형식 변환 작업 처리, 뷰 레이어 포항(pack up) 그리고 Render Server에 전송
  • Render Server: Rendering, 트랜잭션 커밋 및 역직렬화에서 렌더링 트리로 보내진 패키지를 분석한다. 그 다음 뷰 레이어의 속성별로 그리기 명령 실행하고 Vsync 신호가 오면 OpenGL을 호출하여 화면을 렌더링.
  • GPU: GPU는 스크린의 Vsync신호를 기다렸다가 Vsync신호를 기다렸다가 OpenGL Rending 파이프라인을 사용하려 렌더링한다. 렌더링 후 출력이 버퍼로 전송됩니다.
  • Display: buffer에서 데이터를 얻고 screen에 display하기 위해 보낸다.

Core Animation Pipeline내에서, 우리는 1/60s에서 작업 준비를 마치고 render서버로 보내서 1/60s내에 렌더링을 끝낸다. 이 방식으로 앱은 멈추지 않는다(not stuck)

그러나 Magical UIKit을 사용한다면, 많은 Background Thread가 UI를 업데이트 하고 Runloop의 막바지에 screen에 rendering해야될 때 문제가 발생한다.

각 스레드의 커밋은 다른 render 정보를 가지고 있어서 우리는 더 많은 Commit Transactions 다뤄야 하며 Core Animation Pipeline은 정보를 항상 GPU에 커밋합니다. 그러나 렌더링은 사실 매우 비싼 시스템 자원(video memory, CPU)의 매우 비싼 작업이며, 스레드와 많은 수의 Transaction 간의 빈번한 Context Switching은 GPU를 처리할 수 없게 만들고, 이는 성능에 영향을 미쳐 레이어 트리 제출을 1/60초 내에 완료할 수 없게 만든다.

0개의 댓글