관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. UI 성능 분석 - Instrument의 Animation Hitches 사용하여 UI 성능 개선하기 본문

iOS 응용 (swift)

[iOS - swift] 2. UI 성능 분석 - Instrument의 Animation Hitches 사용하여 UI 성능 개선하기

jake-kim 2023. 1. 28. 23:15

1. UI 성능 분석 - Render Loop 이해하기 (Commit, Layout, Display, Prepare, Commit)

2. UI 성능 분석 - Instrument의 Animation Hitches 사용하여 UI 성능 개선하기 <

3. UI 성능 분석 - Commit Hitch를 줄이는 UI 성능 최적화 프로그래밍

 

* 참고한 애플 영상

1번 글에서 알아본 2가지의 hitch

1) Commit hitch

  • commit 단계: UI를 변경하고 이 변경을 랜더 서버에게 알리는 역할
  • 주어진 시간내에 commit하지 못하는 경우
  • commit하지 못한 경우 이전 프레임을 그대로 보여줌
  • “hitch time”: 위와같이 지연되는 시간을 의미하며 ms로 측정

2) Render hitch

  • Render 단계: commit단계에서 변경된 UI를 바탕으로 GPU를 사용하여 렌더링하는 역할
  • 주어진 시간내에 랜더 서버에서 랜더링하지 못하는 경우

-> 이번 글에서는 Commit hitch를 잡는 내용

Commit 개념 (복습)

  • commit 한다는 의미?
    • 사용자가 버튼을 탭 > 뷰 UI 내부적으로 계산하여 업데이트 > 업데이트된 layer tree를 GPU에게 제출하는 행위
  • Commit Hitch란?
    • Commit 단계가 지연되어 이전 프레임을 보여주어 끊기는 듯한 현상 발생

Commit Transaction의 4가지 단계

  • Layout
    • 레이아웃 변경 단계
    • 변경이 필요한 subview들의 레이아웃이 layoutSubviews()가 호출되며 변경되는 단계
    • 언제? frame, bounds, transfrom, 뷰 추가 삭제, setNeedsLayout() 호출되는 경우
  • Display
    • content를 업데이트 되는 경우 호출
    • 언제? 오버라이딩한 draw()를 호출할 때, setNeedsDisplay() 호출할 때
  • Prepare
    • 이미지에 대한 작업을 처리 (디코딩 되지 않은 이미지를 디코드)
    • GPU에서 지원하지 않는 포멧의 이미인 경우 이 단계에서 처리
  • Commit
    • layer tree를 pacakge하여 render 서버에 전송하는 역할

Instruments로 commit hitch 찾고 제거하기

* 테스트 할 코드

  • cmd + I 로 instrument 실행 (프로파일)
    • Animations Hitches 선택

  • record 버튼 클릭

  • Record 버튼을 누르고 UI 관련 작업을 한 후 Stop 버튼 클릭 > 자동으로 분석 > 아래 화면 확인

  • 아래 로깅에서 Severity, Hitch Type확인
    • Severity - Moderate (보통의), High 등
    • Hitch Type - hitch의 유형

(Severity high인 것들을 대략적으로 확인하고 Hitch Duration이 큰 값을 확인 - 7번 Hitch ID가 duration이 33.33ms보다 높으므로 frame이 지연되어 버벅거리게 되는 버그 발생)

  • Main 프로세스를 찾고, 어떤 코드에서 발생했는지 유추
    • filter 검색에 자신의 앱 이름을 검색 (예제에서 사용한 앱 이름은 ExUIDebugger)
    • 검색 후 아래처럼 Main Thread 선택

  • Profile 선택 > 오른쪽 Havial Stack Trace에서 UI관련코드 선택

* Havial Stack Trace는 duration이 많이 걸리는 부분을 추천해주어서 디버깅 할 때 매우 편리

  • 실제로 30ms가 발생하므로 이 부분 체크

  • 밑에 layoutSubviews인 곳도 12ms나 차지하고 있으므로 이 부분도 체크

  • 위에서 얻은 단서를 토대로 MyCell이라는 클래스의 prepare(titleText:)메소드와 그 안의 layoutSubviews 관련 코드 확인
    • MyCell코드를 보니 prepare에서 계속 titleLabel 인스턴스를 지우고 생성하므로 재사용성에 안좋은 코드이며, layoutIfneeded()를 매번 호출하므로 UI성능에 안좋은 효과
    • draw(_:) 메소드를 오버라이딩 했는데, 아무런 구현이 없고 오버라이딩 하기만 하더라도 UI 성능에 안좋으므로 필요하지 않다면 삭제 필요 (구체적인 내용은 다음 챕터에서 계속)
import UIKit

final class MyCell: UITableViewCell {
    private var titleLabel: UILabel?
    
    override func prepareForReuse() {
        super.prepareForReuse()
        prepare(titleText: nil)
    }
    
    func prepare(titleText: String?) {
        guard let titleText else {
            titleLabel?.removeFromSuperview()
            return
        }
        
        let titleLabel = UILabel()
        titleLabel.text = titleText
        titleLabel.textColor = .black
        titleLabel.font = .systemFont(ofSize: 24)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(titleLabel)
        NSLayoutConstraint.activate([
            titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor),
            titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor),
            titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
        ])
        
        self.titleLabel = titleLabel
        
        layoutIfNeeded()
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
    }
}
  • 아래처럼 개선하면 좋은 코드 
    • titleLabel 매번 인스턴스 생성 안하도록 수정 (재사용)
    • layoutIfNeed() 삭제
    • draw(_:) 오버라이딩 삭제
import UIKit

final class MyCell: UITableViewCell {
    private var titleLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        titleLabel.textColor = .black
        titleLabel.font = .systemFont(ofSize: 24)
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        
        contentView.addSubview(titleLabel)
        NSLayoutConstraint.activate([
            titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor),
            titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor),
            titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor),
        ])
    }
    
    required init?(coder: NSCoder) {
        fatalError()
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        
        prepare(titleText: nil)
    }
    
    func prepare(titleText: String?) {
        titleLabel.text = titleText
    }
}

* (Hitches commit이 있는) 전체 코드: https://github.com/JK0369/ExUIDebugger

 

* 참고

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

Comments