이 문서는 앱의 주요 성능 문제를 식별하고 해결하는 데 도움이 됩니다.
주요 성능 문제
앱의 성능 저하에 영향을 줄 수 있는 문제는 다양하지만 앱에서 찾을 수 있는 일반적인 문제는 다음과 같습니다.
- 스크롤 버벅거림
버벅거림은 시스템이 요청된 60hz 이상 주기로 화면에 그려지도록 제시간에 프레임을 빌드하고 제공할 수 없을 때 발생하는 시각적 문제를 설명하는 용어입니다. 버벅거림은 스크롤 시 부드럽게 이어지는 애니메이션된 흐름 대신 끊어지는 느낌이 있을 때 가장 두드러집니다. 앱이 시스템의 프레임 지속 시간보다 콘텐츠를 렌더링하는 데 더 오래 걸리기 때문에 하나 이상의 프레임에서 움직임이 일시중지되면 버벅거림이 나타납니다.
앱은 90Hz 새로고침 빈도를 타겟팅해야 합니다. 기존 렌더링 속도는 60Hz이지만, 대부분의 최신 기기는 스크롤과 같은 사용자 상호작용 중에 90Hz 모드로 작동합니다. 일부 기기는 최대 120Hz의 더 높은 속도를 지원합니다.
특정 시간에 기기에서 사용 중인 새로고침 빈도를 확인하려면 디버깅 섹션에서 개발자 옵션 > 새로고침 빈도 보기를 사용하여 오버레이를 사용 설정합니다.
- 시작 지연 시간
시작 지연 시간은 앱 아이콘, 알림 또는 기타 진입점을 탭한 후 사용자의 데이터가 화면에 표시되기까지 걸리는 시간입니다.
앱에서 다음과 같은 시작 목표를 세우세요.
500ms 미만의 콜드 스타트. 콜드 스타트는 실행되는 앱이 시스템의 메모리에 없을 때 발생합니다. 재부팅하거나 사용자 또는 시스템이 앱 프로세스를 중지한 후 앱을 처음 시작할 때 발생합니다.
반면에 웜 스타트는 앱이 이미 백그라운드에서 실행 중일 때 발생합니다. 콜드 스타트는 저장소에서 모든 항목을 로드하고 앱을 초기화해야 하므로 시스템에서 가장 많은 작업을 해야 합니다. 콜드 스타트가 500ms 이하가 되도록 해 보세요.
P95 및 P99 지연 시간은 지연 시간 중앙값에 매우 가깝습니다. 앱을 시작하는 데 시간이 오래 걸리면 사용자 환경이 저하됩니다. 앱 시작의 중요한 경로에서 프로세스 간 통신(IPC) 및 불필요한 I/O로 인해 잠금 경합이 발생하고 불일치가 초래될 수 있습니다.
- 전환이 원활하지 않음
이는 탭 간 전환이나 새 활동 로드와 같은 상호작용 중에 두드러집니다. 이러한 유형의 전환은 매끄러운 애니메이션이어야 하며 지연이나 시각적 깜박임이 없어야 합니다.
- 전력 비효율
작업을 하면 배터리 충전량이 줄고 불필요한 작업을 하면 배터리 수명이 줄어듭니다.
코드에서 새 객체를 생성할 때 발생하는 메모리 할당으로 인해 시스템에서 상당한 작업이 발생할 수 있습니다. 할당 자체에 Android 런타임(ART)의 작업이 필요할 뿐만 아니라 나중에 이러한 객체를 해제(가비지 컬렉션)하는 데도 시간과 노력이 필요하기 때문입니다. 할당과 수집은 특히 임시 객체의 경우 훨씬 빠르고 효율적입니다. 전에는 가능하면 객체를 할당하지 않는 것이 좋았지만, 이제는 앱과 아키텍처에 가장 적합한 방법을 사용하는 것이 좋습니다. ART의 기능을 고려할 때 유지보수할 수 없는 코드의 위험을 감수하며 할당을 줄이는 방법은 좋지 않습니다.
그러나 노력이 필요하므로 내부 루프에서 많은 객체를 할당할 경우 성능 문제가 발생할 수 있다는 점에 유의하세요.
문제 파악
성능 문제를 식별하고 해결하려면 다음 워크플로를 따르는 것이 좋습니다.
- 다음과 같은 중요한 사용자 여정을 식별하고 검사합니다.
- 런처, 알림 등의 일반적인 시작 흐름
- 사용자가 데이터를 스크롤하는 화면
- 화면 간 전환
- 탐색 또는 음악 재생과 같은 장기 실행 흐름
- 다음 디버깅 도구를 사용하여 이전 흐름에서 발생하는 상황을 검사합니다.
- Perfetto: 정확한 타이밍 데이터로 전체 기기에서 발생하는 상황을 확인할 수 있습니다.
- 메모리 프로파일러: 힙에서 발생하는 메모리 할당을 확인할 수 있습니다.
- Simpleperf: 특정 기간 동안 CPU를 가장 많이 사용하는 함수 호출에 관한 flamegraph를 표시합니다. Systrace에서 시간이 오래 걸리는 항목은 파악하지만 이유는 알 수 없는 경우 Simpleperf를 통해 추가 정보를 확인할 수 있습니다.
이러한 성능 문제를 이해하고 디버그하려면 개별 테스트 실행을 수동으로 디버그하는 것이 중요합니다. 집계된 데이터를 분석하여 이전 단계를 대체할 수는 없습니다. 하지만 사용자에게 실제로 표시되는 내용을 이해하고 회귀가 발생할 수 있는 시점을 식별하려면 자동 테스트 및 필드에서 측정항목 수집을 설정하는 것이 중요합니다.
- 시작 흐름
- 필드 측정항목: Play Console 시작 시간
- 실험실 테스트: Macrobenchmark를 사용하여 테스트 시작
- 버벅거림
- 필드 측정항목
- Play Console 프레임 vitals: Play Console 내에서는 측정항목을 특정 사용자 여정으로 좁힐 수 없습니다. 앱 전체에 걸친 전반적인 버벅거림만 보고합니다.
FrameMetricsAggregator
를 사용한 맞춤 측정: 특정 워크플로 중에FrameMetricsAggregator
를 사용하여 버벅거림 측정항목을 기록할 수 있습니다.
- 실험실 테스트
- Macrobenchmark로 스크롤
- Macrobenchmark에서는 단일 사용자 여정을 괄호로 묶는
dumpsys gfxinfo
명령어를 사용하여 프레임 시간을 수집합니다. 이는 특정 사용자 여정에서 발생하는 버벅거림의 편차를 이해하는 방법입니다. 회귀 또는 개선사항을 식별하기 위해서는 프레임을 그리는 데 걸리는 시간을 강조하는RenderTime
측정항목이 버벅거리는 프레임 수보다 더 중요합니다.
- 필드 측정항목
성능 분석을 위한 앱 설정
앱에서 정확하고 반복 가능하며 실행 가능한 벤치마크를 가져오려면 적절한 설정이 중요합니다. 노이즈 소스를 억제하면서 최대한 프로덕션에 가까운 시스템에서 테스트하세요. 다음 섹션에는 테스트 설정을 준비하기 위해 따를 수 있는 여러 가지 APK 및 시스템별 단계가 나오며, 이 중 일부는 사용 사례별로 설명됩니다.
Tracepoints
앱은 맞춤 트레이스 이벤트로 코드를 계측할 수 있습니다.
트레이스를 캡처하는 동안 섹션마다 약간의 오버헤드(약 5μs)가 발생하므로 모든 메서드에 트레이스를 포함하지는 않아야 합니다. 0.1ms를 초과하는 큰 작업 청크를 추적하면 병목 현상에 관한 중요한 정보를 얻을 수 있습니다.
APK 고려사항
디버그 변형은 스택 샘플의 문제 해결 및 기호화에 유용할 수 있지만 성능에 심각한 비선형적 영향을 미칩니다. Android 10(API 수준 29) 이상을 실행하는 기기는 매니페스트에서 profileable
android:shell="true"
를 사용하여 출시 빌드에서 프로파일링을 사용 설정할 수 있습니다.
프로덕션 수준의 코드 축소 구성을 사용합니다. 이러한 구성은 앱에서 사용하는 리소스에 따라 성능에 상당한 영향을 줄 수 있습니다. 일부 ProGuard 구성은 tracepoint를 삭제하므로 테스트를 실행 중인 ProGuard 구성에서 이러한 규칙을 삭제하는 것이 좋습니다.
컴파일
기기 내에서 앱을 알려진 상태(일반적으로 speed
또는 speed-profile
)로 컴파일합니다.
백그라운드 just-in-time(JIT) 활동은 상당한 성능 오버헤드를 일으킬 수 있으며, 테스트 실행 사이에 APK를 재설치하는 경우 종종 성능 오버헤드에 도달합니다. 이를 위한 명령어는 다음과 같습니다.
adb shell cmd package compile -m speed -f com.google.packagename
speed
컴파일 모드는 앱을 완전히 컴파일합니다. speed-profile
모드는 앱 사용 중에 수집되는 활용된 코드 경로의 프로필에 따라 앱을 컴파일합니다. 일관되고 올바른 프로필 수집은 어려울 수 있으므로 프로필을 사용하기로 한 경우 프로필이 제대로 수집되는지 확인합니다. 프로필은 다음 위치에 있습니다.
/data/misc/profiles/ref/[package-name]/primary.prof
Macrobenchmark를 사용하면 직접 컴파일 모드를 지정할 수 있습니다.
시스템 고려사항
수준은 낮고 충실도는 높은 측정의 경우에는 기기를 보정하세요. 동일한 기기 및 동일한 OS 버전에서 A/B 비교를 실행합니다. 같은 기기 유형에서도 상당한 성능 편차가 있을 수 있습니다.
루팅된 기기에서는 Microbenchmark에 lockClocks
스크립트를 사용하는 것이 좋습니다. 특히 lockClocks 스크립트는 다음을 실행합니다.
- CPU의 빈도를 고정합니다.
- 작은 코어를 사용 중지하고 GPU를 구성합니다.
- 열 제한을 사용 중지합니다.
앱 실행, DoU 테스트, 버벅거림 테스트와 같은 사용자 환경 중심 테스트에는 lockClocks
스크립트를 사용하지 않는 것이 좋습니다. 하지만 Microbenchmark 테스트에서 노이즈를 줄이는 데는 필수적일 수 있습니다.
가능하면 측정 데이터의 노이즈를 줄이고 부정확한 측정을 방지할 수 있는 Macrobenchmark 같은 테스트 프레임워크를 사용해 보세요.
느린 앱 시작: 불필요한 트램펄린 활동
트램펄린 활동은 앱 시작 시간을 불필요하게 연장할 수 있으므로 앱이 트램펄린 중인지 인식하는 것이 중요합니다. 다음 트레이스 예에서와 같이 첫 번째 활동에서 프레임을 그리지 않고 한 activityStart
다음에 다른 activityStart
가 바로 나옵니다.
그림 1. 트램펄린 활동을 보여주는 트레이스
이는 알림 진입점과 일반 앱 시작 진입점에서 모두 발생할 수 있으며 보통 리팩터링을 통해 해결할 수 있습니다. 예를 들어 다른 활동을 실행하기 전에 이 활동을 사용해 설정을 실행하는 경우 activityStart 코드를 재사용 가능한 구성요소 또는 라이브러리에 팩터링합니다.
GC를 자주 트리거하는 불필요한 할당
Systrace에서 예상보다 더 자주 가비지 컬렉션(GC)이 발생할 수도 있습니다.
다음 예에서 장기 실행 작업 중 10초는 앱이 불필요하지만 시간이 지남에 따라 일관되게 할당할 수 있음을 나타냅니다.
그림 2. GC 이벤트 사이의 공간을 보여주는 트레이스
메모리 프로파일러를 사용하면 특정 호출 스택이 대부분의 할당을 실행하는 것을 확인할 수 있습니다. 코드를 유지 관리하기가 더 어려워질 수 있으므로 모든 할당을 적극적으로 삭제할 필요는 없습니다. 대신 할당의 핫스팟에서 작업을 시작하세요.
버벅거리는 프레임
그래픽 파이프라인은 비교적 복잡하며 사용자에게 최종적으로 누락된 프레임이 표시될지 판단하는 데는 약간의 미묘한 차이가 있을 수 있습니다. 경우에 따라 플랫폼은 버퍼링을 사용하여 프레임을 '구조'할 수 있습니다. 하지만 앱의 관점에서 볼 때 문제가 있는 프레임을 식별하기 위해 이러한 미묘한 차이는 대부분 무시해도 됩니다.
앱에서 필요한 작업을 거의 진행하지 않고 프레임을 그릴 경우 Choreographer.doFrame()
tracepoint가 60FPS 기기에서 16.7ms 케이던스로 발생합니다.
그림 3. 빠르고 빈번한 프레임을 보여주는 트레이스
트레이스를 축소하고 탐색하면 프레임이 완료되는 데 좀 더 오래 걸릴 수 있습니다. 하지만 할당된 16.7ms를 초과하지 않으므로 괜찮습니다.
그림 4. 주기적인 작업 버스트가 발생하며 빠르고 빈번한 프레임을 보여주는 트레이스
이 정기적인 케이던스에 중단이 있으면 이는 그림 5와 같이 버벅거리는 프레임인 것입니다.
그림 5. 버벅거리는 프레임을 보여주는 트레이스
이를 식별하는 연습을 할 수 있습니다.
그림 6. 많이 버벅거리는 프레임을 보여주는 트레이스
경우에 따라 확장되는 뷰 또는 RecyclerView
의 기능에 관한 자세한 정보를 확인하기 위해 tracepoint를 확대해야 할 수 있습니다. 다른 경우에는 추가 검사가 필요하기도 합니다.
버벅거리는 프레임을 식별하고 원인을 디버깅하는 방법에 관한 자세한 내용은 느린 렌더링을 참고하세요.
흔히 발생하는 RecyclerView 실수
불필요하게 RecyclerView
의 전체 지원 데이터를 무효화하면 프레임 렌더링 시간이 길어지고 버벅거림이 발생할 수 있습니다. 대신 업데이트해야 하는 뷰의 수를 최소화하려면 변경되는 데이터만 무효화합니다.
콘텐츠를 완전히 대체하는 대신 업데이트하게 하는, 비용이 많이 드는 notifyDatasetChanged()
호출을 방지하는 방법은 동적 데이터 표시를 참고하세요.
모든 중첩된 RecyclerView
를 제대로 지원하지 않으면 내부 RecyclerView
가 매번 완전히 다시 만들어질 수 있습니다. 중첩된 모든 내부 RecyclerView
에는 모든 내부 RecyclerView
간에 뷰를 재활용할 수 있도록 RecycledViewPool
이 설정되어 있어야 합니다.
데이터를 충분히 미리 가져오지 않거나 적시에 미리 가져오지 않으면 사용자가 서버에서 더 많은 데이터를 기다려야 할 때 스크롤 목록 하단에 도달하기가 불편할 수 있습니다. 이는 프레임 기한이 누락되지는 않으므로 엄밀히 말하면 버벅거림은 아니지만 사용자가 데이터를 기다릴 필요가 없도록 미리 가져오기의 시기와 양을 수정하여 UX를 크게 개선할 수 있습니다.
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 앱 시작 분석 및 최적화{:#app-startup-analysis-optimization}
- 정지된 프레임
- Macrobenchmark 작성