앱 시작 분석 및 최적화

앱이 시작되는 과정이 앱의 첫인상을 결정합니다. 앱 시작 시 사용자가 앱을 사용하는 데 필요한 정보를 빠르게 로드하고 표시해야 합니다. 앱을 시작하는 데 시간이 너무 오래 걸리면 대기 시간 때문에 사용자가 앱을 종료할 수도 있습니다.

Macrobenchmark 라이브러리를 사용하여 시작 시간을 측정하는 것이 좋습니다. 이 라이브러리는 개요와 자세한 시스템 트레이스를 제공하여 시작 시 발생하는 상황을 정확히 확인할 수 있습니다.

시스템 트레이스는 기기에서 발생하는 활동에 관한 유용한 정보를 제공하므로 앱이 시작되는 동안 어떤 작업을 하는지 파악하고 최적화할 수 있는 영역을 식별하는 데 도움이 됩니다.

앱 시작을 분석하려면 다음을 실행하세요.

시작 분석 및 최적화 단계

앱 시작 시 최종 사용자에게 반드시 중요한 특정 리소스를 로드해야 하는 경우가 많습니다. 필수가 아닌 리소스는 시작이 완료될 때까지 로드를 기다릴 수 있습니다.

성능을 절충하려면 다음을 고려하세요.

  • Macrobenchmark 라이브러리를 사용하여 각 작업에 걸리는 시간을 측정하고 완료하는 데 오래 걸리는 블록을 식별합니다.

  • 리소스를 많이 사용하는 작업이 앱 시작에 필수적인지 확인합니다. 앱이 완전히 그려질 때까지 대기할 수 있는 작업이면 시작 시 리소스 제약 조건을 최소화할 수 있습니다.

  • 이 작업을 앱 시작 시 실행해야 하는지 확인합니다. 종종 기존 코드나 서드 파티 라이브러리에서 불필요한 작업을 호출할 수 있습니다.

  • 가능하면 장기 실행 작업을 백그라운드로 이동합니다. 백그라운드 프로세스도 시작 시 CPU 사용량에 영향을 줄 수 있습니다.

작업을 완전히 조사한 후 로드하는 데 걸리는 시간과 앱 시작 시 포함할 필요성 사이에서 절충점을 결정할 수 있습니다. 앱의 워크플로를 변경할 때 회귀 또는 브레이킹 체인지가 발생할 가능성을 포함해야 합니다.

앱의 시작 시간에 만족할 때까지 최적화하고 다시 측정합니다. 자세한 내용은 측정항목을 사용하여 문제 감지 및 진단을 참고하세요.

주요 작업에 소요된 시간 측정 및 분석

전체 앱 시작 트레이스가 있으면 트레이스를 확인하고 bindApplication 또는 activityStart와 같은 주요 작업에 걸린 시간을 측정합니다. Perfetto 또는 Android 스튜디오 프로파일러를 사용하여 이러한 트레이스를 분석하는 것이 좋습니다.

앱 시작 중에 소비된 전체 시간을 확인하여 다음과 같은 작업을 식별하세요.

  • 오랜 시간을 차지하며 최적화할 수 있는 작업. 모든 밀리초가 성능에는 중요합니다. 예를 들어 Choreographer 그리기 시간이나 레이아웃 확장 시간, 라이브러리 로드 시간, Binder 트랜잭션, 리소스 로드 시간을 찾습니다. 일반적으로 20밀리초보다 오래 걸리는 모든 작업을 살펴봅니다.
  • 기본 스레드를 차단하는 작업. 자세한 내용은 Systrace 보고서 탐색을 참고하세요.
  • 시작 시 실행하지 않아도 되는 작업
  • 첫 번째 프레임이 그려질 때까지 기다릴 수 있는 작업

이러한 각 트레이스를 자세히 조사하여 성능 차이를 찾습니다.

기본 스레드에서 비용이 많이 드는 작업 식별

파일 I/O, 네트워크 액세스와 같이 비용이 많이 드는 작업은 기본 스레드 외부에 유지하는 것이 좋습니다. 이는 앱 시작 중에도 똑같이 중요합니다. 기본 스레드에서 비용이 많이 드는 작업을 실행하면 앱이 응답하지 않게 되고 다른 중요한 작업이 지연될 수 있기 때문입니다. StrictMode.ThreadPolicy를 사용하면 기본 스레드에서 비용이 많이 드는 작업이 발생하는 사례를 식별할 수 있습니다. 디버그 빌드에서 StrictMode를 사용 설정하여 문제를 최대한 빨리 식별하는 것이 좋습니다(다음 예 참고).

Kotlin

class MyApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        ...
        if (BuildConfig.DEBUG)
            StrictMode.setThreadPolicy(
                StrictMode.ThreadPolicy.Builder()
                    .detectAll()
                    .penaltyDeath()
                    .build()
            )
        ...
    }
}

Java

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        ...
        if(BuildConfig.DEBUG) {
            StrictMode.setThreadPolicy(
                    new StrictMode.ThreadPolicy.Builder()
                            .detectAll()
                            .penaltyDeath()
                            .build()
            );
        }
        ...
    }
}

StrictMode.ThreadPolicy를 사용하면 모든 디버그 빌드에서 스레드 정책이 사용 설정되고 스레드 정책 위반이 감지될 때마다 앱을 비정상 종료하므로 스레드 정책 위반을 놓치기 어렵습니다.

TTID 및 TTFD

앱이 첫 프레임을 생성하는 데 걸리는 시간을 보려면 처음 표시하는 데 걸린 시간(TTID)을 측정하세요. 그러나 이 측정항목에는 사용자가 앱과 상호작용을 시작할 수 있는 시간이 꼭 반영되는 것은 아닙니다. 완전히 표시하는 데 걸린 시간(TTFD) 측정항목이 완전히 사용할 수 있는 앱 상태를 유지하는 데 필요한 코드 경로를 측정하고 최적화하는 데 더 유용합니다.

앱 UI가 완전히 그려진 시점을 보고하는 전략은 시작 시간 정확성 개선을 참고하세요.

TTID와 TTFD는 둘 다 자체 영역에서 중요하므로 모두 최적화합니다. TTID가 짧으면 사용자가 앱이 실제로 실행되고 있는지 직접 확인할 수 있습니다. 사용자가 앱과 빠르게 상호작용할 수 있도록 하려면 TTFD를 짧게 유지하는 것이 중요합니다.

전반적인 스레드 상태 분석

앱 시작 시간을 선택하고 전체 스레드 슬라이스를 확인합니다. 기본 스레드는 항상 응답해야 합니다.

Android 스튜디오 프로파일러Perfetto와 같은 도구는 기본 스레드에 관한 자세한 개요와 각 단계에서 소요되는 시간을 제공합니다. perfetto 트레이스 시각화에 관한 자세한 내용은 Perfetto UI 문서를 참고하세요.

기본 스레드 절전 모드 상태의 주요 청크 식별

절전 모드에 많은 시간이 소비되는 경우 앱의 기본 스레드가 작업 완료를 기다리고 있기 때문일 가능성이 높습니다. 멀티스레드 앱이 있다면 기본 스레드가 대기 중인 스레드를 식별하고 해당 작업을 최적화하는 것을 고려해 보세요. 중요한 경로에서 지연을 초래하는 불필요한 잠금 경합이 없는지 확인하는 것도 유용할 수 있습니다.

기본 스레드 차단 및 무중단 절전 모드 줄이기

차단된 상태로 전환되는 기본 스레드의 모든 인스턴스를 찾습니다. Perfetto와 스튜디오 프로파일러는 스레드 상태 타임라인에 주황색 표시기로 이를 나타냅니다. 작업을 식별하고 예상된 작업인지 피할 수 있는 작업인지 확인한 후 필요하다면 최적화합니다.

IO 관련 중단 절전 모드는 개선할 수 있는 좋은 부분입니다. IO를 실행하는 다른 프로세스는 관련 없는 앱이더라도 최상위 앱이 실행하는 IO와 경합할 수 있습니다.

시작 시간 단축

최적화 기회를 확인한 후 시작 시간을 개선하는 데 도움이 되는 가능한 솔루션을 살펴봅니다.

  • 콘텐츠를 비동기식으로 지연 로드하여 TTID 속도를 높입니다.
  • 바인더를 호출하는 함수 호출을 최소화합니다. 불가피한 경우 호출을 반복하거나 비차단 작업을 백그라운드 스레드로 이동하는 대신 값을 캐시하여 이러한 호출을 최적화해야 합니다.
  • 앱 시작을 더 빠르게 표시하려면 화면의 나머지 부분이 로드될 때까지 최소한의 렌더링이 필요한 항목을 최대한 빠르게 사용자에게 표시할 수 있습니다.
  • 시작 프로필을 만들고 앱에 추가합니다.
  • Jetpack 앱 시작 라이브러리를 사용하여 앱 시작 시 구성요소 초기화를 간소화합니다.

UI 성능 분석

앱 시작에는 스플래시 화면과 홈페이지의 로드 시간이 포함됩니다. 앱 시작을 최적화하려면 트레이스를 검사하여 UI를 그리는 데 걸린 시간을 파악하세요.

초기화 시 작업 제한

특정 프레임은 다른 프레임보다 로드하는 데 시간이 더 오래 걸릴 수 있습니다. 이는 앱에서 비용이 많이 드는 그리기로 간주됩니다.

초기화를 최적화하려면 다음을 실행하세요.

  • 느린 레이아웃 패스 개선을 먼저 선택합니다.
  • 맞춤 트레이스 이벤트를 추가하여 비용이 많이 드는 그리기와 지연을 줄여 Perfetto의 각 경고와 Systrace의 알림을 조사합니다.

프레임 데이터 측정

프레임 데이터를 측정하는 방법에는 여러 가지가 있습니다. 5가지 기본 수집 방법은 다음과 같습니다.

  • dumpsys gfxinfo를 사용한 로컬 수집: dumpsys 데이터에서 관찰된 모든 프레임이 앱의 느린 렌더링을 초래하거나 최종 사용자에게 영향을 미치는 것은 아닙니다. 하지만 이는 여러 출시 주기에서 전반적인 성능 동향을 파악하기 위한 좋은 방법입니다. gfxinfoframestats를 사용하여 UI 성능 측정값을 테스트에 통합하는 방법에 관한 자세한 내용은 Android 앱 테스트 기본사항을 참고하세요.
  • JankStats를 사용한 필드 수집: JankStats 라이브러리를 사용하여 앱의 특정 부분에서 프레임 렌더링 시간을 수집하고 데이터를 기록 및 분석합니다.
  • Macrobenchmark를 사용하는 테스트에서(내부적으로 Perfetto)
  • Perfetto FrameTimeline: Android 12 (API 수준 31)에서는 Perfetto 트레이스에서 프레임 드롭을 유발하는 작업에 이르기까지 프레임 타임라인 측정항목을 수집할 수 있습니다. 이는 프레임 드롭 이유를 진단하기 위한 첫 번째 단계가 될 수 있습니다.
  • 버벅거림 감지를 위한 Android 스튜디오 프로파일러

기본 활동 로드 시간 확인

앱의 기본 활동에는 여러 소스에서 로드되는 대량의 정보가 포함될 수 있습니다. 홈 Activity 레이아웃을 확인하고 특히 홈 활동의 Choreographer.onDraw 메서드를 살펴보세요.

  • reportFullyDrawn을 사용하여 앱이 이제 최적화를 위해 완전히 그려졌다고 시스템에 알립니다.
  • Macrobenchmark 라이브러리와 함께 StartupTimingMetric을 사용하여 활동 및 앱 실행을 측정합니다.
  • 프레임 드롭을 확인합니다.
  • 렌더링하거나 측정하는 데 시간이 오래 걸리는 레이아웃을 식별합니다.
  • 로드하는 데 시간이 오래 걸리는 애셋을 식별합니다.
  • 시작 중에 확장되는 불필요한 레이아웃을 식별합니다.

기본 활동 로드 시간을 최적화하는 다음과 같은 솔루션을 고려해 보세요.

  • 초기 레이아웃을 최대한 단순하게 만듭니다. 자세한 내용은 레이아웃 계층 구조 최적화를 참고하세요.
  • 맞춤 tracepoint를 추가하여 드롭된 프레임과 복잡한 레이아웃에 관한 추가 정보를 제공합니다.
  • 시작 시 로드되는 비트맵 리소스의 수와 크기를 최소화합니다.
  • 레이아웃이 즉시 VISIBLE되지 않는 경우 ViewStub을 사용합니다. ViewStub은 런타임 시 레이아웃 리소스를 느리게 팽창시키는 데 사용할 수 있는 보이지 않는 제로 크기의 뷰입니다. 자세한 내용은 ViewStub을 참고하세요.

    Jetpack Compose를 사용하는 경우 상태를 사용하여 일부 구성요소 로드를 지연하는 ViewStub과 유사한 동작을 얻을 수 있습니다.

    var shouldLoad by remember {mutableStateOf(false)}
    
    if (shouldLoad) {
     MyComposable()
    }
    

    shouldLoad를 수정하여 조건부 블록 내에 컴포저블을 로드합니다.

    LaunchedEffect(Unit) {
     shouldLoad = true
    }
    

    그러면 첫 번째 스니펫의 조건부 블록 안에 코드가 포함된 리컴포지션이 트리거됩니다.