article thumbnail image
Published 2024. 2. 24. 18:31

Hitch는 Render Loop에서 제시간에 Frame을 준비하지 못했을 때 발생한다.

 

이러한 Hitch는 어떤 Stage에서 발생했냐에 따라, 

Commit Hitch, Render Hitch로 구분된다. 

 

Commit Hitches

"Commit Phase"는 UI를 변경하고 업데이트된 UI Layer Tree를 Render server로 제출한다. 

이때 Render Server로 제출되는 결과물을 "Commit"이라 부른다. 

 

다음과 같이 "Event Phase"에서 touch이벤트를 통해 backgroundColor와 frame을 변경했다고 가정하자.

다음과 같이 시스템은 display 혹은 layout이 필요하다고 마킹을 하게 된다. 

 

이후 "Commit Phase"에서 시스템에 의해 각 draw(rect:) 혹은 layoutSubviews 메서드를 호출한다.

이를 통해 Layout Tree를 구성하고, Drawing 작업을 진행한다.

 

4 Step in Commit Phase

이런 Commit Phase는 디테일하게 4개의 단계로 구성된다. 

  • Layout
  • Display 
  • Prepare 
  • Commit

 

Layout Step

Layout이 필요한 View의 layoutSubviews()를 호출해, Layout Tree를 생성하는 단계이다. 

layoutSubviews()는 view와 subView들의 위치와 크기를 재조정한다.
이는 재귀적으로 모든 Subview의 layoutSubviews()를 호출하기 때문에, 직접 호출하면 안 된다. 

 

Layout 단계는 다음과 같은 상황일 때 수행된다. 

  • View Position 변경 (bounds, frame, transform..)
  • View 추가 혹은 삭제
  • setNeedsLayout()을 직접 호출

 

Display Step

Content Update가 필요한 View들의 draw(rect:)메서드가 호출한다.

 

Display 단계는 다음과 같은 상황에서 수행된다. 

  • draw(rect:)을 override 한 View를 추가 
  • setNeedsDisplay()를 명시적으로 호출

 

Prepare Step

Prepare단계에서는 이미지에 대한 처리 작업을 진행된다. 

만약 디코딩되지 않은 이미지가 있다면 디코딩이 되고, 

GPU에서 지원하지 않는 Color Format의 이미지가 있다면, 이 단계에서 변환된다.

 

Commit Step

마지막으로 View Layer Tree가 상위뷰부터 하위뷰까지 재귀적으로 패키징해, render서버로 전송한다.

 

Commit Hitch 줄이기 

Keep Views LightWeight

커스텀 draw(rect:)메서드보단, CALayer 프로퍼티 사용하기

draw(rect:)은 앞서 살펴보았듯이, Commit Phase의 "Display 단계"에서 호출이 된다. 

또한, CALayerGPU에서 동작하는 반면, draw(rect:)CPU에서 동작한다.

CALayer는 Core Animation의 주된 Object이며 말 그대로 Animation을 담당한다. 
Core Graphics의 draw(rect:)메서드는 view가 초기에 나타날 때와 같이 빠르게 다시 그릴 필요가 있을 때 
호출된다. 
빠르게 그릴 필요가 있기 때문에 Core Graphics는 CPU에서 동작한다.
하지만, Animation은 1초에 60개 혹은 120개를 그려야 하기 때문에 GPU에서 동작한다. 

 

CPU에서의 연산 처리가 자주 일어나게 된다면, draw(rect:) 메서드의 연산 시간은 길어지게 되고, 

이는 최종적으로 Commit Hitch가 발생할 가능성이 생긴다. 

따라서, 가능하다면 CALayer의 프로퍼티를 사용하는 것이 좋다. 

// layer의 type이 CALayer
view.layer.cornerRadius = 20

 

불필요한 draw(rect:) 메서드 override 하지 않기

draw(rect:)메서드를 override하는 것만으로도 Commit Phase에서 더 많은 메모리와 시간이 필요하게 된다. 

 

View 재사용 & isHidden 사용하기

View를 remove 하고 add 하는 작업은 굉장히 비싼 작업이다. 

따라서, 가능하다면 View를 재사용하고, remove보단 isHidden을 사용하는 것이 더 저렴하다.

 

Reduce Expensive or Redundant Layout

layoutIfNeeded보단 setNeedLayout사용하기 

layoutIfNeeded는 Commit Phase의 시간을 늘리게 되는데, 이는 결국 Commit Hitch를 유발한다. 

대부분의 경우에는 Run Loop을 기다려도 된다. 

 

layoutIfNeeded는 기존 Update가 예약되어 있던 View들을 뒤로 미루고 현재 업데이트를 가장 먼저 실행하게 된다. 

즉, 다음 Loop에선 Layout이 필요한 view들이 많아지게 되고 Commit Phase가 늘어나게 된다. 

 

최소한의 Constraints만 사용하기 

Constraints의 연산시간을 줄이기 위해, 필요한 만큼만 Constraints를 사용하자. 

 

layout업데이트가 필요한 곳만 업데이트하기

만약 부모 View의 Layout을 invalidate 하게 되면 재귀적으로 계산이 일어나 비용이 비싸지게 된다. 

따라서, 필요한 View에서만 invalidate를 하는 것이 좋다.

 

 

Render Hitches

"Commit Phase"에선 App의 UI를 Modify 하고, 업데이트된 UI Layer Tree를 Renser Server로 제출한다.

RenderServer는 UI Layer Tree을 랜더링 한다. 

 

이전 포스팅에서 보았듯이 Render Server Stage는 다음과 같이 구분된다. 

  • Render Prepare Phase : Layer Tree를 GPU에서 수행할 수 있도록 Linear Pipeline으로 컴파일한다.  
  • Render Execute Phase : GPU에서 final Image로 그린다. 

 

Rednering Example

다음과 같은 View를 랜더링 한다고 가정해 보자!

유의할 점은 막대와 원을 중심으로 그림자가 있다는 것이다. 

  • Commit Phase에선 다음과 같이 Layer 트리를 Render Server로 제출한다. 
  • Render Prepare Phase에선 GPU에서 그릴 수 있도록 선형의 Pipeline을 형성한다. 
    • 이때 PipeLine은 부모 View부터 순차적으로 구성된다.
  • Render Execute Phase에서 최종적으로 GPU에서 Image를 생성한다. 

 

이제 Render Execute Phase에서 GPU가 어떻게 view를 그리는지 자세히 알아보자.

GPU는 다음과 같이 부모 View서부터 Texture에 랜더링을 시작한다. 

 

Shadow Rendering

문제는 빨간색으로 하이라이트 된 영역 뒤에 그림자이다. 

 

 

GPU입장에선 현재 단계에선 어떤 영역을 그림자로 처리해야 할지 모르는 상태이다. 

그렇다고 원과 직사각형 모양의 View를 먼저 그리고, 그림자를 그리는 것은 더 어려운 작업이다.  

이를 위해선 경계선 파악이 필요한데, 이는 어려운 작업일뿐더러 비용도 많이 들어간다. 

 

따라서, 그림자를 파악하기 위해 다음과 같이 새로운 Texture를 생성해 원과 직사각형을 그린다.

 

이후, 이를 검은색으로 처리하고,

 

최종적으로 Blur처리를 통해 그림자를 생성한다.

 

이후 해당 Texture를 원래 Texture에 복사를 하고, 

랜더링 작업을 마무리한다.

 

 

OffScreen Rendering

앞서 살펴보았던 그림자와 같이, 

GPU가 다른 texture에 Rendering을 하고, 

이를 원래 texture로 복사하는 작업을 "OffScreen Rendering"이라 한다.

 

OffScreen Rendering은 위에서 처럼 추가적인 메모리 공간과 시간이 필요하기에,

Render Server에서 Hitch의 주된 요인이 된다. 

 

iOS에선 다음과 같은 랜더링이 offscreen Rendering을 진행한다.

  • Shadow
  • Masking
  • Rounded Rectangle
  • Visual Effects

 

Shadow 

그림자의 경우 앞서 예시를 살펴보았듯이,

그림자의 영역을 사전에 알 수 없어서 OffScreen으로 랜더링을 했다. 

 

Masking

Masking의 경우, 어디까지 content가 잘리는지 알 수 없기 때문에, OffScreen으로 랜더링을 한다. 

 

Rounded Rectangle

이것 역시 Masking이란 관련이 있는데, 

Rounding으로 인해 잘리는 Content영역을 사전에 모르는 경우 OffScreen으로 랜더링을 한다. 

 

Visual Effects

UIKit에선 2가지 Visual Effect를 제공하는데, 

이들은 모두 OffScreen Rendering을 진행한다.

 

Render Hitch 줄이기 

OffScreen Rendering이 Render Phase에서 Hitch를 발생시킨다.

앞서 살펴본 4가지 타입의 OffScreen Rendering은

모두 사전에 충분한 정보가 주어지지 않아 OffScreen Rendering이 발생했다.

 

따라서, 제공된 API를 사용하고, Masking을 최적화를 통해

사전에 정보를 제공함으로써 Render Hitch를 줄일 수 있다.

 

Use Provided APIs

 

Core Animation의 메인 객체인 CALayer는 GPU에서 동작하며, 적절한 랜더링 정보를 전달해 줄 수 있다.

 

그림자의 경우 사전에 CALayershadowPath 프로퍼티를 통해

그림자의 모양을 사전에 알려주면 불필요한 OffScreen Rendering을 피할 수 있다. 

 

Rounded Rectangle의 경우에는 CALayercorderRadiuscornerCurve 프로퍼티를 사용하여, 

GPU에 코너 관련 정보를 전달한다. 

하지만 Rounded Rectangle에서는 코너 부분에 Content가 있다면 불필요한 OffScreen Rendering이 발생하기에, 

Masking를 지정해 줄 필요가 있다.

 

Optimize Masking

Rounded Rectangle과 같이 내부 Content가 잘리는 경우,

CALayer를 통해 maskToBounds를 통해 내부 Content가 잘리지 않게 한다면, 

추가적인 OffScreen Rendering 역시 필요 없게 된다. 

 

 

References

https://developer.apple.com/videos/play/tech-talks/10855

https://developer.apple.com/videos/play/tech-talks/10857

https://developer.apple.com/videos/play/tech-talks/10856

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html

https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html

 

https://stackoverflow.com/questions/14659563/to-drawrect-or-not-to-drawrect-when-should-one-use-drawrect-core-graphics-vs-su

https://stackoverflow.com/questions/17715530/the-relationship-between-coregraphics-uiviews-and-calayers?rq=3

https://stackoverflow.com/questions/11909394/why-is-drawrect-faster-than-using-calayers-uiviews-for-uitableviews

https://stackoverflow.com/questions/18748276/why-an-empty-implementation-of-drawrect-will-adversely-affect-performance-durin?rq=3

'iOS > iOS' 카테고리의 다른 글

[iOS] Core Animation(2) - CAAnimation  (0) 2024.03.07
[iOS] Core Animation(1) - Concept  (0) 2024.03.05
[iOS] Render Loop & Hitch(1)  (0) 2024.02.22
[iOS] UIResponder(1)  (0) 2023.11.10
[iOS] Custom Drop Down  (1) 2023.10.31
복사했습니다!