Instrument로 Commit hitch 찾고 제거하기

Lily·2022년 9월 11일
0

안녕하세요~ 릴리에요
오늘은 UIAnimation Hitch와 Render Loop에 대해 알아보자 에 이어서, Commit hitch를 Instrument로 포착, 분석하는 방법과 hitch를 제거하는 방법에 대해 공부해보겠습니다!

TechTalk: Find and fix hitches in the commit phase
를 보고 정리한 글입니다.

가보자고~🚀


먼저 렌더 루프를 간략하게 보고 가겠습니다.

iOS는 뷰를 보여주기 위해 렌더 루프를 사용합니다. 사용자 이벤트가 앱으로 전달되고, 앱은 사용자 이벤트에 따라 뷰를 업데이트합니다. 그리고 업데이트된 뷰는 GPU에서 렌더링되어 최종적으로 화면에 보여지게됩니다.

사용자 이벤트에 따라 UI를 변경하고, 업데이트된 UI layer tree를 GPU에게 제출하는 것을 Commit 이라 하고, Commit이 다음 VSYNC까지 완료되지 못해 발생하는 hitch를 Commit hitch라고 합니다.

Commit Transaction

먼저 Commit transaction에서 발생되는 일에 대해 알아보겠습니다.

여기 이벤트를 기다리는 뷰가 있습니다. 뷰가 터치 이벤트를 받으면 배경색을 바꾸든, 서브 뷰의 frame을 변경하든 UI를 업데이트하게 됩니다.

시스템은 레이아웃이나 디스플레이 변경이 필요한 서브 뷰들을 기록해둡니다.

그리고 다음 커밋 트랜잭션에서 시스템에 의해 draw(), layoutSubviews()가 호출되면서 디스플레이와 레이아웃은 업데이트 됩니다.

커밋 트랜잭션은 디테일하게는 4단계로 구분됩니다.

layout ➡️ Display ➡️ Prepare ➡️Commit

1. Layout phase

레이아웃 단계에서는 레이아웃이 필요한 모든 뷰의 layoutSubviews()가 호출됩니다. 모든 서브뷰들의 레이아웃이 변경되는 것이죠

레이아웃은 다음과 같은 상황에서 발생합니다.

  • 뷰 포지션을 변경할 때 (frame, bounds, transform)
  • 뷰를 추가하거나 삭제할 때
  • setNeedsLayout()을 직접 호출할 때

2. Display phase

디스플레이 단계에서는 컨텐츠 업데이트가 필요한 모든 뷰들의 draw() 가 호출됩니다.

디스플레이는 다음과 같은 상황에서 발생합니다.

  • 오버라이드한 draw에서 뷰를 추가할 때
  • setNeedsDisplay()를 직접 호출 할 때

3. Prepare phase

준비 단계에선 이미지에 대한 작업을 처리합니다.

  • 아직 디코드되지 않은 이미지가 있다면 디코드 됩니다. 만약 큰 이미지라면 많은 시간이 걸릴 수 있습니다.

  • GPU에서 지원하지 않는 Color format의 이미지가 있다면, 이 단계에서 변환됩니다.

이미지의 성능을 최적화하려면 WWDC18: Image and Graphics Best Practices을 참고해주세용

4. Commit phase

뷰 레이어 트리가 상위뷰부터 하위뷰까지 재귀적으로 포장되고, 렌더 서버에게 전송됩니다.


Instrument로 hitch 찾고, 개선하기

Xcode12에는 Instrument에 Animation Hitches라는 템플릿이 추가되었습니다.
이 기능을 사용해서 히치를 찾고 시각적으로 분석해볼겁니다!

Example App

예제 앱을 통해 히치를 찾아보고 고쳐볼 건데요, 앱을 스크롤하며 Insrtrument에서 기록을 해보겠습니다.

그리고 Instrument를 보면 Hitches라는 트랙에서 감지된 히치들을 보여줍니다!

히치 트랙을 펼치면 더 다양한 정보를 볼 수 있습니다

히치의 지속 시간, 커밋 단계, 렌더 단계, 프레임 수명, VSync를 알 수 있네요!

hitch가 발생하기 전까지의 간격을 허용 가능한 지연(Acceptable Latency)라고 합니다.

그 후의 hitch 지속 시간은 hitch duration이라고 부릅니다

detailView에서는 각 히치의 duration, acceptable latency, buffer count 그리고 hitch type을 알 수 있습니다.
hitch type은 hitch가 어떤 단계에서 기인했는지, hitch의 원인을 파악하는데 매우 중요한 정보가 됩니다.

위에서 보여주듯, 선택된 히치는 commit, GPU phase에서 발생했습니다.
Instrument의 UIAnimation hitch 템플릿은 Time profiler template을 포함하고 있기 때문에, 이 시점에 어떤 코드가 실행되었는지 알 수 있습니다.

해당 영역을 선택하고, 커밋이 실행된 프로세스를 필터링합니다. (이 예제에서는 meal planner 앱)

그리고 main thread를 선택하고, call tree를 살펴봅니다.
여기서 가장 비싼 호출을 분석할 수 있습니다. QSTEM CollectionViewCellupdateTags() 가 가장 많은 시간인 10ms를 사용했네요.

이 함수를 분석하면 commit hitch를 유발한 원인이 무엇인지 알 수 있겠죠?

먼저 QSTEM CollectionViewCell의 component들을 살펴보겠습니다.
UIImageViewUILabel, MKTMealPlannerTagLabel로 구성되어있습니다.

이제 QSTEM CollectionViewCell의 코드를 살펴볼게요.
menuItem은 property observer를 사용하고, 두가지 상황에서 가 호출되고 있습니다. menuItem이 존재하는 경우와 menuItemnil인 경우, 2가지 상황에서 updateTags() 가 호출됩니다.

updateTags메서드 구현을 살펴보겠습니다.

태그가 없고, 태그를 담는mealTagStackView를 슈퍼뷰에서 제거합니다.
태그가 있고, mealTagStackView가 없다면 mealTagStackView를 만들어서 rootStackView의 하위뷰에 추가합니다.

그리고 재사용가능한 tagLabel이 있다면 사용하고, 아니라면MKTMealPlannerTagLabel를 생성해서 태그를 보여줍니다.

Cell의 prepareForReuse를 살펴보겠습니다.

menuItem에 nil을 할당해주고 있습니다. nil을 할당하는 것은 updateTags를 호출하는 두번째 시나리오에 해당됩니다. 따라서 cell이 디큐될 때마다, 기존의 mealTagStackView를 제거하고, 위에서 구현한 TagLabel의 재사용 로직을 사용하지 못합니다. 매번 새로운 TagLabel을 생성하고 이는 최적의 퍼포먼스를 보장하지 못하겠죠.

또한 하위뷰를 추가하거나 삭제하면 커밋 트랜잭션에서 살펴본 것과 같이, commit phase에서 레이아웃이 업데이트 됩니다. 따라서 Commit phase에 할 일이 증가하게됩니다.

해결 방법은 간단합니다.
prepareForReuse의 아무런 일도 해주지 않으면 됩니다.

그럼 재사용 로직을 활용할 수 있고, 뷰의 계층구조를 변경하고, Label을 생성하는 비용을 줄일 수 있습니다!

해결 한 후의 hitch를 비교해보면, 전보다 아주 많이 hitch수가 줄어든 모습입니다!


이 처럼 Time Profiler를 함께 사용해서, hitch가 발생할 때 어떤 코드가 실행되었는지 파악할 수 있습니다. hitch의 원인을 파악하고 고치는데 매우 중요한 역할을 할 수 있습니다👍


Instrument를 통해 앱에서 hitch를 파악하는 방법에 대해 알아봤다면, Commit phase에서 hitch를 줄 일 수 있는 추천 방법, 꿀팁들을 살펴볼게요!

Commit hitch를 줄이는 방법

1. Keep views lightweight

1) CALayer의 property를 최대한 사용하기

draw는 CPU에서 연산을 하지만, CALayer는 GPU를 가속해서 사용합니다. CPU에서 연산을 하게되면 mainthread의 부하를 증가시키겠죠?

그러니 CALayer의 프로퍼티로 구현이 가능하면 draw 대신 쓰세요!

만에 하나 draw를 사용하게 되면 성능을 측정하세요

2) 필요 없다면 draw override 하지 않기

draw를 override해서 빈 구현 하지 마세요.
단지 draw를 override하는 것 만으로 커밋 트랜잭션동안 더 많은 메모리와 시간을 사용하게 합니다.

3) view 재사용하기

뷰를 재사용 하세요!

뷰 계층구조를 변경하는 추가하고 삭제하는 작업은 비싼 비용입니다.

4) isHidden 사용하기

애니메이션동안 특정 뷰를 안보이게 하고 싶다면 isHidden프로퍼티를 사용하세요!

isHiddenremove보다 훨씬 저렴한 작업이니까요.

2. Reduce expensive or redundant layout

1) layoutIfNeeded 보다 setNeedsLayout() 사용하기

레이아웃을 업데이트하려면 setNeedsLayout() 사용하세요!

layoutIfNeeded는 커밋 트랜잭션 시간을 늘리고, 히치를 유발합니다. 대부분 경우는 레이아웃을 업데이트하기위해 다음 런루프를 기다릴 수 있을겁니다.

2) 필요한 최소한의 constraint만 사용하세요

복잡한 제약 계산을 피하기 위해, 필요한 최소한의 제약만 사용하세요!

3) 자신 뷰의 레이아웃만 업데이트 하세요

자신 또는 자식 뷰의 레이아웃만 invalidate하세요!
부모 뷰의 레이아웃을 invalidate하면 재귀적인 레이아웃을 유발하고 비용이 비쌉니다.


정리

커밋 히치를 찾고, 원인을 파악하는 방법과 commit hitch를 사전에 예방할 수 있는 꿀팁들에 대해 알아보았습니다.

뷰의 frame rate가 떨어질 때마다, 적용시켜볼 수 있는 실질적 방법들을 알게된것 같아 든든하네요!🙂
Instrument 애용해야겠습니다ㅎㅎㅎ

profile
i🍎S 개발을 합니다

0개의 댓글