도구 및 실적

유형 안전 유지 규칙 및 DEX 최적화

스피커

정보

강연 소개

Android 앱은 서드 파티 라이브러리에서 비롯되는 지나치게 보수적인 유지 규칙으로 인해 최적화되지 않은 상태로 출시되는 경우가 많으며, 이로 인해 APK가 비대해지고 콜드 스타트가 느려지며 런타임 성능이 저하됩니다.

Google Play의 새로운 DEX 최적화 성능 기준에 따라 이제 앱 가시성은 최적화 곡선에서 앞서 나가는 것에 직접적으로 좌우됩니다.

이 강연에서는 규칙 비대화를 해결하기 위한 최신 R8 워크플로를 소개합니다. 학습할 내용

  • R8 구성 분석기 및 스킬을 사용하여 기존 규칙 오버헤드를 식별합니다.
  • 광범위한 와일드 카드 유지 규칙에서 정확하고 유형 안전한 생성된 주석으로 전환합니다.
  • 새로운 Android CLI 도구로 최적화 회귀를 방지합니다.

요약

  • Google Play에서는 25% 이상의 코드 최적화를 요구하는 앱 품질 기준을 적용합니다. 이러한 기준을 충족하면 메모리 사용량이 줄어들고 시작 속도와 ANR 발생률이 개선됩니다.
  • R8 구성 분석기는 어떤 유지 규칙이 코드를 유지하는지 보여주는 HTML 보고서를 생성합니다. 포함된 규칙과 사용되지 않는 규칙을 강조 표시하고 컴파일 전에 최적화 가능성을 측정합니다.
  • 리플렉션 비정상 종료를 방지하기 위해 도입되거나 라이브러리에서 상속되는 지나치게 광범위한 유지 규칙은 앱 최적화를 저해합니다. 단일 패키지 전체 와일드 카드 규칙은 사용되지 않는 수십 개의 클래스를 유지할 수 있습니다.
  • Jetpack Keep은 린트를 사용하여 리플렉션을 감지하고 코드에서 직접 정확한 주석과 조건부 유지 규칙을 생성합니다. 이전 R8 버전과 호환되는 표준 소비자 규칙을 내보냅니다.
  • Android CLI는 업로드 없이 Google Play의 정확한 로직을 사용하여 App Bundle 및 AAR을 검사합니다. 문제가 있는 규칙에 플래그를 지정하고 자동화된 회귀 테스트를 위해 JSON 보고서를 내보냅니다.

리소스

Gemini Notebook

주제

  • dex-optimization
  • r8
  • keep-rules
  • jetpack-keep
  • android-cli
  • app-performance

스크립트

DEX 최적화 소개

  • Hi, everyone!

  • 저는 Android DevRel팀의 브리아나입니다.

  • 저는 Android 성능팀의 크리스 크레이크입니다.

  • 유형 안전 유지 규칙 및 DEX 최적화에 오신 것을 환영합니다.

  • 오늘은 DEX 최적화가 무엇인지, 왜 관심을 가져야 하는지, 그리고 최근의 흥미로운 업데이트에 대해 이야기해 보겠습니다.

  • 오랫동안 DEX 최적화는 있으면 좋은 정도로만 여겨져 왔습니다.

  • 따라서 한 번 구성한 후 그대로 두거나, 전용 시간이 있을 때 구성할 수 있습니다.

  • 하지만 모바일 업계가 기기 메모리 가용성을 변화시키는 심각한 하드웨어 공급 제약을 겪고 있는 상황에서, 앱이 메모리를 적게 사용하고, 더 빠르게 시작하고, ANR을 줄이고, 전반적인 렌더링 및 런타임 성능을 개선하려면 잘 최적화된 DEX가 필수적입니다.

  • 건전한 Android 생태계를 유지하고 앱이 프리미엄 경험을 계속 제공할 수 있도록 Google Play에서는 2월에 시행되는 성능 기준을 포함하는 새로운 앱 품질 요구사항을 발표했습니다.

  • 여기에는 DEX 코드 최적화 기준이 포함됩니다.

  • 앱은 축소, 최적화, 난독화에서 25% 이상을 달성해야 합니다.

  • 그렇다면 Android 개발자에게는 어떤 의미가 있을까요?

  • 최적화되지 않은 DEX 코드는 사용자 경험에 부정적인 영향을 미칠 뿐만 아니라 Play 스토어에서 앱의 공개 상태와 게시 기능에도 영향을 미칠 수 있습니다.

  • 하지만 여러분은 제대로 찾아오셨습니다. 이 강연이 끝날 때쯤이면 이러한 요구사항을 충족하는 것은 물론, 그 이상을 달성할 수 있는 도구를 갖추게 될 것입니다.

  • DEX 코드 최적화는 Android의 앱 최적화 도구인 R8을 사용하여 달성할 수 있습니다.

  • 다른 앱 최적화 도구를 사용하여 앱이 이러한 기준을 충족할 수 있다면 R8이 반드시 필요한 것은 아니지만, R8은 Android 도구 모음에 직접 내장되어 있어 권장되는 방법입니다.

  • 잘 최적화된 DEX, App Bundle은 메모리를 적게 사용하지만, 주요 이점은 여전히 성능과 관련이 있습니다. ANR 감소, 빠른 시작, 런타임 성능 개선 등이 있습니다.

  • 앱을 최적화하면 앱과 사용자에게 실질적이고 입증된 긍정적인 결과를 가져다줍니다.

  • 예를 들어 Grab은 R8 전체 모드를 채택하고 유지 규칙을 미세 조정하여 앱의 최적화를 78%, 축소를 79%, 난독화를 82% 달성할 수 있었습니다.

  • 이 슬라이드를 보면서 '수치가 꽤 높지만 실제로 어떤 의미가 있을까?'라고 생각하실 수도 있습니다.

  • 이러한 개선사항과 기준 프로필을 함께 적용한 결과 ANR이 32% 감소하고 시작 시간이 35% 개선되었습니다.

  • 상위 앱이 R8을 처음 채택하면 DEX 크기가 평균 38% 감소하고 사용자가 인식하는 ANR이 16% 감소합니다.

  • 이미 R8을 사용하고 있다면 좋은 소식이 있습니다.

  • 이미 R8을 사용 설정한 앱이 최적화, 축소, 난독화 수준을 50% 이상으로 개선하는 조치를 취하면 DEX 크기가 평균 25% 더 감소하고, 시작 시간이 4% 감소하며, 사용자 인식 ANR이 추가로 17% 감소합니다. 이는 이전 슬라이드에서 보여드린 16% 에 더해지는 수치입니다.

  • 따라서 오늘은 이러한 수치를 달성하는 방법을 안내해 드리고자 합니다.

  • DEX 코드를 최적화하고 지나치게 광범위하고 잘못 구성된 유지 규칙에서 구체적이고 좁은 규칙으로 전환하는 데 도움이 되는 도구를 다룹니다.

  • 먼저 R8은 수년 동안 사용되어 왔습니다.

  • 또한 이 기능이 큰 영향을 미치는 최적화를 가능하게 했지만, 구성하는 데 여전히 많은 주의가 필요하다는 점을 알고 있습니다.

  • 리플렉션 호출은 입력을 예측할 수 없으며 런타임 비정상 종료로 이어질 수 있습니다.

  • 이로 인해 과거에는 앱팀과 라이브러리팀 모두 안전을 위해 지나치게 광범위한 유지 규칙을 추가하기도 했습니다.

  • 상위 앱의 41% 가 Google Play에서 정의한 25% 최적화 수준에 미치지 못하지만, 상위 앱의 13% 는 이미 이 기준의 3배를 달성하고 있습니다.

  • 이 수치는 다양한 업계와 사용자 여정의 앱에서 달성할 수 있습니다.

  • 자체 유지 규칙과 라이브러리에서 상속된 유지 규칙을 모두 관리할 수 있는 적절한 도구와 가시성이 있다면 앱의 최적화 점수를 높일 수 있습니다.

  • 이를 지원하는 새로운 도구와 개선사항을 살펴보기 전에, 최적화를 더 쉽게 만들어 주는 지난 한 해 동안의 몇 가지 다른 R8 개선사항도 강조하고 싶습니다.

  • 첫째, 새 앱 템플릿에는 패키지 범위가 포함되어 있어 처음부터 최소 수준의 최적화를 보장하는 데 유용합니다.

  • 잠시 후에 설명할 R8 구성 분석기는 이제 이전 버전의 AGP에서 지원됩니다.

  • Bazel에서도 비공식적으로 지원됩니다.

  • AGP 9.2에서는 코루틴 프리미티브가 2배 빨라졌습니다.

  • Tinder는 AGP 9.2에서 이루어진 R8 개선 덕분에 버전 9.0.1에서 업그레이드한 후 ANR이 13% 감소했습니다.

  • 또한 AGP 9.3에는 더 명확한 이름 지정, 기본 규칙 및 리소스 축소를 위한 더 간단한 기본값, 유지 규칙을 위한 AGP 소스 디렉터리 지원을 갖춘 재설계된 R8 API가 도입되었습니다.

    android {
        buildTypes {
            release {
                optimization {
                    // no need to specify default
                    // rules and resource shrinking
                    enabled = true
                }
            }
        }
        ...
    }
    android {
        buildTypes {
            release {
                // Enables code optimizations.
                isMinifyEnabled = true
    
                // Enables resource shrinking.
                isShrinkResources = true
    
                proguardFiles(
                    // Default rules.
                    getDefaultProguardFile(
                        "proguard-android-optimize.txt"
                    ),
                    ... // list out each file with keep rules
                )
                ...
            }
        }
        ...
    }
  • 오늘 다룰 업데이트는 개발자 환경 개선에 관한 것입니다.

  • 먼저 R8 구성 분석기와 R8 스킬을 활용하여 기존 규칙 오버헤드를 식별하고 개선 경로를 결정하는 방법을 알아보겠습니다.

  • 그런 다음 크리스가 광범위한 와일드 카드 유지 규칙에서 새롭고 정확하며 유형 안전하고 자동 생성되는 주석으로 전환하는 방법을 다룹니다.

  • 마지막으로 새로운 CLI 검증 도구를 사용하여 비정상 종료를 유발하지 않고 적극적으로 최적화하는 방법을 살펴보겠습니다.

R8 구성 분석기 개요

  • R8 구성 분석기는 DEX 최적화 조사를 시작하기에 가장 좋은 도구입니다.

  • 이 도구를 사용해 본 적이 없다면 다음과 같이 표시됩니다.

  • R8 구성에 관한 정보를 시각화하는 HTML 보고서입니다.

  • 이 보고서는 Analytics Bloat라는 라이브러리가 포함된 내 앱에 관한 보고서입니다.

  • 짐작하시겠지만, 이는 실제 앱이나 실제 라이브러리가 아니지만 상단에 파란색으로 강조 표시되어 있습니다.

  • 구성 분석기는 다음을 측정합니다. 축소: R8이 유지 규칙에 의해 축소되지 않는 클래스, 메서드, 필드의 비율입니다.

  • 이는 R8이 첫 번째 축소 패스를 실행한 후에 계산됩니다.

  • 또한 메서드 인라인 처리 및 클래스 병합과 같은 전체 프로그램 최적화에 열려 있는 코드의 비율인 최적화를 측정합니다.

  • 난독화는 더 짧은 식별자로 난독화하거나 이름을 바꿀 수 있는 멤버의 비율입니다.

  • 또한 R8 Config Analyzer는 최종 최적화 및 DEX 컴파일 전에 이러한 점수를 계산하므로 이 보고서에 표시되는 최적화, 축소, 난독화 값은 Play Console에 표시되는 값과 정확히 일치하지 않습니다.

  • 이는 Play에서 최종 DEX 출력을 측정하기 때문입니다.

  • 이러한 차이가 있는 이유는 개발 시 빠른 로컬 피드백 루프에 액세스할 수 있도록 하기 위함입니다.

  • 하지만 R8 구성 분석기에 표시되는 점수는 Play 스토어에 업로드하기 전에 DEX 최적화 가능성을 검증하는 데 여전히 매우 유용합니다.

  • 보고서에는 보고서에 들어갈 때 표시되는 기본 뷰인 보관 규칙 분석 표도 포함되어 있습니다.

  • 여기에서 규칙을 클릭하면 특정 규칙이 유지하는 클래스, 메서드, 필드의 정확한 수를 확인할 수 있으므로 어떤 규칙이 필요 이상으로 유지되고 있는지 확인할 수 있습니다.

  • 또한 여기에서 확대된 부분인 포함된 규칙도 있습니다.

  • 이러한 규칙은 더 광범위한 규칙과 중복되는 규칙입니다.

  • 이러한 키워드는 강조 표시되므로 광범위한 키워드를 삭제하고 더 구체적인 키워드만 유지할 수 있습니다.

  • 또한 앱의 안정성에 영향을 주지 않고 삭제할 수 있는 규칙인 사용되지 않는 규칙도 표시됩니다.

  • 따라서 구성 분석기는 기존 규칙 누적을 시작으로 몇 가지 문제를 해결하도록 설계되었습니다.

  • 이전에는 규칙별 공개 상태가 없었기 때문에 이전 규칙을 삭제하는 것이 다소 위험했습니다.

  • 따라서 팀은 이를 그대로 두거나 더 광범위한 와일드 카드를 추가하곤 했습니다.

  • 둘째, 라이브러리 규칙의 사각지대를 발견하는 데 도움이 됩니다.

  • 따라서 SDK를 추가할 때 해당 라이브러리와 함께 번들로 제공되는 규칙이 지나치게 광범위한지, 아니면 10개 정도의 버전이 지난 후에도 필요한지 판단하기가 어려웠습니다.

  • 라이브러리에 대해서도 잠시 후에 자세히 다루겠습니다.

  • 셋째, 최적화의 영향을 평가하는 데 도움이 됩니다.

  • 이전에는 규칙이 클래스 2개를 유지하는지 2,000개를 유지하는지 쉽게 알 수 있는 방법이 없었습니다.

  • 내 앱과 앞서 살펴본 라이브러리의 컨텍스트에서 R8이 어떻게 작동하는지 살펴보겠습니다.

  • 여기에는 앱 모듈과 라이브러리 3개가 있습니다.

  • 오른쪽의 라이브러리 3에 주목하세요.

  • 바로 패키지 전체 보존 규칙이 포함된 Analytics Bloat 라이브러리입니다.

  • 먼저 R8은 병합된 모든 보관 규칙에서 루트 진입점을 식별합니다. 여기에서는 노란색으로 강조표시되어 있습니다.

  • 일반적으로 이러한 클래스는 매니페스트의 클래스와 같이 앱의 기본 진입점이어야 합니다.

  • 또한 Analytics Bloat 라이브러리의 와일드 카드 유지 규칙으로 인해 라이브러리의 모든 단일 클래스가 진입점으로 취급됩니다.

  • 그런 다음 R8은 이러한 루트에서 호출 그래프 전반의 연결 가능성을 추적합니다.

  • 루트에서 연결할 수 있는 모든 클래스는 보존을 위해 녹색으로 표시되고, 참조되지 않은 클래스는 비활성 코드로 분류되어 빨간색으로 표시됩니다.

  • 그런 다음 R8은 트리 쉐이킹을 실행하고 참조되지 않는 불량 코드를 제거합니다.

  • 따라서 이 트리 셰이킹 또는 첫 번째 축소 패스 후에는 다음과 같은 항목이 남습니다.

  • 앱의 축소 점수는 42.5%로 Google Play의 기준인 25% 를 상회하지만, 아직 원하는 수준에는 미치지 못합니다.

  • 이는 주로 애널리틱스 라이브러리의 패키지 전체 유지 규칙 때문입니다.

  • 따라서 남은 40개 클래스 중 17개만 더 축소할 수 있습니다.

  • 여기서 강조하고 싶은 점은 잘못된 유지 규칙 하나가 앱 최적화에 큰 문제를 일으킬 수 있다는 것입니다.

  • 이 점을 염두에 두고 라이브러리에 집중해 보겠습니다.

  • 지난 1년 동안 Google팀은 라이브러리 소비자 유지 규칙이 생태계 최적화에 미치는 영향을 파악하는 데 많은 투자를 해왔습니다.

  • 많은 라이브러리가 안정성을 위해 지나치게 광범위한 규칙을 포함하여 제공됩니다.

  • 또는 앱 개발자가 앱에 직접 추가할 수 있도록 문서에 광범위한 규칙을 지정하기도 합니다.

  • 하지만 광범위한 규칙을 적용하는 것이 안정성에 필수적인 것은 아니며, 성능에 부정적인 영향을 미치고, 그 비용은 주로 앱 개발자가 부담해야 합니다.

  • 분석 비대화 라이브러리와 같은 패키지 전체 유지 규칙에 의해 전체 라이브러리가 보존되는 경우가 있었으며, 이 라이브러리의 경우 앱의 최적화 가능성에 광범위한 영향을 미쳤습니다.

  • Google의 자체 SDK도 이 문제에서 자유롭지 못했으며, 지난 1년 동안 12개가 넘는 Google 라이브러리에서 지나치게 광범위한 유지 규칙을 수정했습니다.

  • 구성 분석기를 사용하려는 경우 AGP 9.3에 직접 통합되어 있지만 R8 버전을 업그레이드하면 이전 버전에서도 사용할 수 있습니다.

  • 이 명령어를 실행하면 앞서 보여드린 HTML 보고서가 생성되어 개별 규칙과 앱 전체의 축소, 최적화, 난독화에 미치는 영향과 유지 규칙을 보여줍니다.

  • 전체 R8 출시 빌드를 실행하는 경우 보고서가 자동으로 생성되며 이 파일 경로에서 찾을 수 있습니다.

  • 이 독립형 작업의 이점은 전체 빌드보다 생성하는 데 시간이 덜 걸린다는 것입니다.

우수사례: WHOOP 최적화

  • WHOOP팀은 구성 분석기를 실행하여 축소 74%, 난독화 74%, 최적화 0%를 얻었습니다.

  • 예상하시겠지만, 팀은 이 최적화 값을 보고 정말 놀랐습니다.

  • 구성 분석기를 통해 빌드에서 최적화를 사용 중지하는 기존 ProGuard 파일을 사용하고 있다는 사실을 발견했고, 업데이트된 버전으로 전환하여 앱의 최적화를 사용 설정할 수 있었습니다.

  • 그 결과 55% 의 최적화가 이루어졌고 APK 크기가 4% 줄었습니다.

  • 기준 프로필과 함께 사용한 결과 콜드 시작 시간이 38% 개선되고 과도한 정지된 프레임이 28% 감소했습니다.

  • 왼쪽 화면은 기준 프로필이 없는 경우이고 오른쪽 화면은 기준 프로필이 있는 경우입니다. 보시다시피 기준 프로필을 추가하면 앱 시작이 더 빠릅니다.

  • 한 번 더 재생해 보겠습니다.

  • 됐습니다

  • 이 링크를 클릭하면 구성 분석기 가이드로 이동합니다. 보고서를 수동으로 검토하는 것도 좋지만, 앱의 R8 구성 상태에 따라 시간이 많이 걸릴 수 있습니다.

  • 따라서 속도를 높이기 위해 R8 분석기 기술이 있습니다.

    $ android skills list
    $ android skills add r8-analyzer
  • 에이전트에게 R8 구성을 분석해 달라고 요청하면 이 스킬은 R8 구성 분석기 보고서를 생성할 뿐만 아니라 가장 영향력 있는 규칙을 알려줍니다.

  • 다음은 출력 예시입니다. 몇 가지 세부정보를 살펴보면 최적화 점수가 42%로 표시되어 있습니다. 즉, 코드베이스의 58%는 R8로 최적화할 수 없습니다.

  • keep 규칙 섹션을 구체적으로 살펴보면, 분석 bloat 라이브러리의 패키지 전체 keep 규칙이 코드베이스의 22% 를 최적화하지 못하게 하고 있습니다. 또한 이 스킬은 이미 알고 있듯이 이 keep 규칙이 너무 광범위하므로 이를 세분화할 것을 권장합니다.

  • 이제 R8 구성 분석기와 스킬을 사용하여 앱 비대화를 유발하고 DEX 최적화를 차단하는 원인을 진단할 수 있습니다.

DEX 최적화에서 리플렉션의 문제

  • 하지만 지나치게 광범위한 유지 규칙을 찾은 후에는 예기치 않은 비정상 종료를 일으키지 않고 안전하게 교체하려면 어떻게 해야 할까요?

  • 이제 크리스에게 넘겨서 기존 유지 규칙에서 새로운 유형 안전 자동 생성 유지 주석으로 전환하는 방법을 보여드리겠습니다.

  • 브리아나님, 감사합니다.

  • 그럼 어떻게 여기까지 왔는지 이야기해 볼까요?

  • 이 비대한 분석 라이브러리에는 기본적으로 모든 것을 유지하는 유지 규칙이 있습니다. 이로 인해 발생하는 핵심 문제는 리플렉션입니다.

  • 그럼 리플렉션 코드를 잠시 살펴보겠습니다.

    fun createWithReflection(className: String): MyObject {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as MyObject
    }
  • 이 함수는 특정 객체를 확장하고 이를 반영적으로 수행하는 객체 인스턴스화 함수입니다.

  • DEX 최적화 측면에서 이 방식의 단점은 이러한 모든 정적 호출을 런타임까지 지연시킨다는 것입니다.

  • 코드를 살펴보면 이 클래스 이름이 어디에서 오는지에 따라 R8 최적화 도구는 여기서 무슨 일이 일어나는지 알 수 없습니다.

  • 이러한 상황은 기본적으로 Dex 최적화를 중단하므로 일반적으로 Keep 규칙을 추가해야 합니다.

  • 이상적으로는 이러한 상황을 피하는 것이 좋습니다.

  • 이상적으로는 리플렉션을 완전히 피하는 것이 좋습니다. 특히 2026년에는 직렬화와 종속 항목 삽입을 위한 놀라운 코드 생성 도구가 있어 이러한 범주의 문제를 완전히 제거할 수 있습니다.

  • 하지만 그게 전부는 아니죠?

  • 특정 경우에는 여전히 리플렉션이 필요합니다.

  • 그럼 이 비대해진 라이브러리에서 이 문제가 어떻게 발생했는지 자세히 살펴보겠습니다.

  • 여기 한 가지 구체적인 예가 있습니다. 이는 일반적인 예로, 선택적 종속 항목입니다.

    // Problem 1 - optional dependency
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
    
    // Problem 2 - dynamically instantiate subclass
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
    
    // Problem 3 - serialization
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • 여기서는 라이브러리 클래스 경로에서 액세스할 수 없는 특정 유형을 확장하므로 문자열로 처리해야 합니다.

  • 라이브러리가 빌드될 때 존재할 수도 있고 존재하지 않을 수도 있는 특정 클래스를 참조하고 있으므로 이 라이브러리가 리플렉션을 사용하여 관련 라이브러리 내의 멤버를 찾는 것은 매우 합리적입니다.

  • 또 다른 사례는 서브클래스를 동적으로 인스턴스화하는 것입니다.

  • 따라서 특히 매니페스트 파일에서 가져오거나 서버 측에서 전송되는 경우 로컬 앱에 '이 항목을 확장하고 나중에 생성해 줘'라고 알립니다.

  • 그리고 여기에서 볼 수 있는 세 번째 카테고리는 직렬화입니다.

  • 모든 멤버를 반복하여 JSON 직렬화가 진행되고 있습니다.

  • 정말 역겨운 일이지만, 이런 일은 가끔 발생합니다.

  • 필요한 순간에 항상 해결되는 것은 아닙니다.

  • 하지만 이 결과가 무엇인지 이야기해 보겠습니다.

  • 이러한 리플렉션 호출이 쌓이면 사용자 환경보다 개발자 환경을 최적화하는 쉬운 방법을 선택하고 싶은 유혹이 커집니다.

  • 기분이 좋지 않으니 할 일을 추가하면 괜찮아질 겁니다.

    # TODO: clean this up
    -keep com.example.analytics.bloat.** { * };
  • 언젠가는 내가 직접 고칠 수 있겠지?

  • 그럼, 우리가 해야 할 일, 적어도 오늘 이전에 해야 했던 일, 그리고 여기에서 이상적인 보관 규칙을 작성하는 방법에 대해 이야기해 보겠습니다.

    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • 자, 준비되셨나요?

  • 첫 번째 부분은 이 리플렉션 함수에서 반환하는 특정 클래스를 유지하는 것입니다.

    ### Keep the EventFilter
    -keep class com.example.analytics.bloat.EventFilter {
        *
    }
  • 아니, 잠깐만요.

  • 있잖아요

  • 이는 사실이 아닙니다.

  • 이건 Kotlin 클래스니까 문자열 유형인지 확인해야 해. KT를 넣어 보자.

    ### Keep the actual EventFilter
    -keep class com.example.analytics.bloat.EventFilterKt {
        *
    }
  • 사실 여기서 중요한 점은 실제로 EventFilter를 반환하는 것이 아니라 EventFilter의 서브클래스를 반환한다는 것입니다. 따라서 이를 올바르게 확장하려면 EventFilter를 확장하는 클래스를 유지해야 합니다.

    ### Keep the EventFilter subclasses
    -keep class ** extends com.example.analytics.bloat.EventFilterKt {
        *
    }
  • 이 작업을 더 효율적으로 하려면 어떻게 해야 할까요? 방금 방법도 효과가 있었겠지만, 더 효율적으로 하려면 훌륭한 라이브러리 개발자로서 무엇을 더 해야 할까요?

  • -keepclasseswithmembers를 지정하고 객체를 확장하는 데 사용하는 이 특정 <init> 생성자가 있는 클래스만 유지해야 합니다.

    ### Keep the EventFilter subclasses, if they have this constructor
    -keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • 그리고 정말로 친절하게 하려면 allowaccessmodification를 추가하여 이 Keep 규칙의 타겟이 되는 모든 API에서 더 많은 최적화가 이루어지도록 허용할 것입니다. R8의 추가 최적화를 막고 싶지 않기 때문입니다. 좋은 시민이 되려고 노력하고 싶습니다.

    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
       void <init>();
    }
  • 마지막 단계는 추가 작업이 필요하기 때문에 거의 아무도 하지 않지만 라이브러리에 적합한 조건부로 만드는 것입니다.

    ### -if makes this conditional, critical for libraries
    -if class com.example.analytics.bloat.EventToolsKt {
        class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
    }
    ### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
    -keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
        void <init>();
    }
  • 개념적으로는 이 함수를 실제로 호출하지 않는 한 이러한 서브클래스를 모두 유지할 필요는 없으며, 이것이 Keep 규칙 상단에 있는 이 if 문이 하는 일입니다.

  • 손을 들어 보라고 할 수도 있지만, 이렇게 하는 사람은 거의 없다는 것을 알고 있습니다.

  • 실제로 작성하고 조건부로 만드는 것 말입니다.

  • 그렇다면 이 과정을 조금 더 개선할 수 있다면 어떨까요?

  • 먼저, 좋아요.

  • 다른 방법이 있을지도 모릅니다.

  • 그래서 수동으로 작성된 좁은 범위의 Keep 규칙을 작성하면 어떻게 될지 논의했습니다.

  • 기술적으로는 가능하지만 작성하기 어렵고 읽기 어려우며 유지관리하기가 매우 어렵습니다. 그 이유는 그 아래에서 무언가 발생할 때 쉽게 중단될 수 있는 작은 문자열 유형 비트가 많이 있기 때문입니다.

  • 그 아래에 있는 무언가가 바뀝니다.

  • 대안도 있습니다.

  • @Keep과 같은 주석이 있습니다.

  • @Keep은 오랫동안 AndroidX 주석 패키지에 포함되어 있었지만, 요즘에는 사용이 권장되지 않습니다.

    @androidx.annotation.Keep
    class EventFilter { … }
    
    @androidx.annotation.Keep
    class SizeBasedEventFilter { … }
    
    @androidx.annotation.Keep
    class DropLogsEventFilter { … }
  • 라이브러리 개발자에게는 좋은 솔루션이 아니며, 실제로 내부적으로는 금지되어 있습니다. 유지관리는 더 쉽지만 매우 유연하지 않고, 앞서 언급한 최적화 기능 중 일부가 포함되어 있지 않기 때문입니다.

  • 그럴 수 없습니다.

  • 호출 사이트 분석을 허용하지 않습니다.

  • 이제 이전에 살펴본 특정 사례를 살펴보겠습니다. "EventFilter에 @Keep을 추가하면 되니까 괜찮겠죠?"

    @Keep
    class EventFilter { … }
  • 아니요.

  • @Keep은 실제로 서브클래스를 타겟팅하지 않고 해당 클래스만 타겟팅합니다.

    @Keep
    class EventFilter { … }
    
    @Keep
    class SizeBasedEventFilter { … }
    
    @Keep
    class DropLogsEventFilter { … }
  • 따라서 실제로 모든 서브클래스를 타겟팅해야 하며, 이제 작성하기 어려웠던 단일 Keep 규칙보다 더 많은 @Keep 주석을 작성하고 있습니다.

  • 두 가지 모두 문제가 있죠?

  • 잊어버리기 쉽고, 늦게 알게 되며, 기본적으로 호출자에 따라 조건부로 설정되지 않습니다.

  • 특히 라이브러리의 경우 더 나은 방법이 필요합니다.

  • Google은 바로 사용할 수 있는 더 쉬운 방법을 제공하고자 합니다.

  • 여기서 볼 수 있는 큰 문제 중 하나는 이러한 것들이 실제로 보호하는 반사에서 멀리 떨어져 있다는 것입니다.

  • 궁극적으로 우리가 원하는 솔루션은 타겟팅된 항목을 실제로 부풀리는, 여기 있는 잘못된 리플렉션 코드 바로 옆에 있는 것입니다.

Jetpack Keep 소개

  • 이제 이 문제를 해결하기 위해 설계된 AndroidX의 새로운 라이브러리 및 플러그인인 Jetpack Keep에 대해 이야기해 보겠습니다.

  • Jetpack Keep의 전체 목표는 주석에서 유지 규칙을 생성하는 것으로 시작하지만, 더 나아가 코드베이스에 이미 있는 리플렉션에서 직접 주석을 생성하는 것입니다.

  • 설정은 매우 간단합니다.

    plugins {
       id("com.android.library")
       id("androidx.annotation.keep")
    }
    
    dependencies {
        implementation("androidx.annotation.keep:keep")
        ...
    }
  • Gradle 플러그인이자 주석 라이브러리입니다.

  • 하지만 빌드 시 실제로 어떻게 실행되는지 살펴보겠습니다.

  • 따라서 알려진 유형의 서브클래스를 확장하는 이전 예시가 있다면 어떻게 해야 할까요?

  • lintFix를 실행하면 됩니다.

    ./gradlew analyticsbloat:lintFix
  • 여기서 린트는 이 메서드 내에서 발생하는 모든 리플렉션을 분석하고 '이 주석이 필요합니다'라고 알려줍니다.

    @UsesReflectionToConstruct(
       classConstant = EventFilter::class,
       parameterTypes = []
    )
    fun createEventFilter(className: String): EventFilter {
       return Class.forName(className)
           .getDeclaredConstructor()
           .newInstance() as? EventFilter
    }
  • 이 주석은 이 호출 내의 리플렉션이 수행하는 작업을 정확하게 표현합니다.

  • '이 유형의 클래스를 확장하고 있다'는 의미입니다.

  • 이 특정 사례에서 EventFilter는 알려진 클래스입니다.

  • 문자열을 사용하여 참조하지는 않습니다. 그럴 필요가 없기 때문입니다.

  • 이는 추가적인 안전 조치이며, 어떤 생성자가 유지되는지 정확하게 알 수 있도록 parameterTypes를 인코딩하고 있습니다.

  • 이 기능은 어떤 역할을 할까요?

  • 예를 들어 최종 R8 빌드에 어떤 영향을 미치나요?

  • 유지 규칙을 생성합니다. 이전에 보셨듯이, 이는 이전에 사용했던 것과 동일한 이상적인 유지 규칙입니다.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.EventTools {
        class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
    }
    -keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
        void <init>();
    }
  • 정확히 동일한 클래스, 하위 클래스를 타겟팅하며 이 반사 지점에 따라 조건부입니다.

  • 모든 서브클래스에 주석을 달 필요도 없습니다.

  • 다른 곳에 별도의 유지 규칙 파일을 보관할 필요가 없습니다.

  • 이렇게 하면 보관 규칙을 훨씬 더 쉽게 관리할 수 있습니다.

  • 선택적 종속 항목 예시를 다시 살펴보겠습니다.

    @UsesReflectionToConstruct(
       className = "com.example.analytics.bloat.crash.CrashReporterTransport",
       parameterTypes = [Context::class]
    )
    fun tryInitCrashReporter(context: Context): EventTransport? {
       return runCatching {
           Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
               .getDeclaredConstructor(Context::class.java)
               .newInstance(context) as? EventTransport
       }.getOrNull()
    }
  • 이제 클래스가 클래스 경로에 없지만 여전히 참조할 수 있습니다.

  • 문자열을 사용하면 되지만 다른 모든 것, 즉 매개변수 유형은 클래스 참조로 참조할 수 있으므로 그렇게 합니다.

  • 이 경우에도 이상적인 보관 규칙을 생성합니다.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.CrashConnection {
        class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
        void <init>(android.content.Context);
    }
  • 조건부이고 구체적이며 타겟팅되어 있고 타겟팅된 클래스에서 가능한 한 최적화를 지원합니다.

  • 여기서는 필드에 대한 리플렉션이 있습니다. 이 특정 사례에서는 객체 내부의 모든 필드를 반복하고 있었지만, 여기서는 이 별표 연산자를 사용하여 모든 필드를 타겟팅한다고 말함으로써 이를 잘 처리합니다.

    // TODO: migrate to Kotlinx Serialization or Moshi codegen.
    @UsesReflectionToAccessField(
       classConstant = UserActionEvent::class,
       fieldName = "*"
    )
    fun serializeEvent(event: UserActionEvent): JSONObject {
       val json = JSONObject()
       for (field in event.javaClass.declaredFields) {
           field.isAccessible = true
           json.put(field.name, field.get(event))
       }
       return json
    }
  • 이렇게 하면 모든 필드를 타겟팅할 수 있고, 앱에 있어서는 안 되는 매우 불행한 리플렉션에 대해 예상할 수 있는 정확한 유지 규칙을 생성합니다. 하지만 다시 돌아와서 정리할 것을 약속합니다.

    # Generated Keep Rule
    
    -if class com.example.analytics.bloat.serialization.EventSerializer {
        class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
    }
    -keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
        <fields>;
    }
  • 그렇다면 Jetpack Keep은 어떤 역할을 할까요?

  • 보관 규칙 유지보수가 대폭 줄어듭니다.

  • 자동으로 생성할 수 있는 모든 항목은 보관 규칙을 유지하기 위한 우려 영역과 위험 영역에서 삭제됩니다.

  • 또한 린트의 결정론도 제공합니다.

  • 반복적으로 마이그레이션을 시도하는 과정에서 문제를 해결해 줄 뿐만 아니라, 코드가 변경될 때마다 새로운 리플렉션과 리플렉션 변경사항을 포착해 주므로 안전성이 매우 높습니다.

  • 이 기능을 사용할 수 있나요?

  • 그렇다면 여기에는 어떤 종속 항목이 있을까요?

  • 플러그인과 린트에는 AGP 8.7이 필요합니다.

  • 거의 모든 사람이 해당될 것입니다.

  • 하지만 이 기능은 사용 위치에 관해서도 매우 유연합니다.

  • 앱 내에서 사용할 수 있습니다.

  • Android 라이브러리, AAR, KMP 라이브러리, JAR 내에서 사용할 수 있습니다. 이 경우 약간의 수동 연결이 필요합니다.

  • 하지만 여기서 핵심 기능은 라이브러리 개발자가 안전하게 사용할 수 있다는 것입니다.

  • 누군가 오래된 버전의 R8을 사용하거나 라이브러리를 사용하는 서드 파티 도구를 사용하더라도 Google은 실제 표준 소비자 규칙을 내보내므로 누구나 라이브러리를 사용할 수 있으며 호환되지 않는 새로운 형식을 사용하는 것에 대해 걱정할 필요가 없습니다.

Android CLI로 앱 분석하기

  • 지금까지 Jetpack Keep이 최적화를 지원하고 리플렉션 코드 호출 보호에 대한 걱정을 덜어주는 데 어떻게 도움이 되는지 설명해 드렸습니다. 이제 마지막 출시 단계에서 다시 한번 확인하기 위해 최근 Android CLI에 출시한 새로운 기능에 대해 알아보겠습니다.

  • 브리아나가 앞서 언급했듯이 Play Findings에는 크기 정보, DEX 크기, 최적화 비율에 관한 정보를 알려주는 도구가 있습니다.

  • 이제 Android CLI를 통해서도 이 기능을 사용할 수 있습니다.

  • Play 스토어와 동일한 로직을 사용하므로 Play 스토어에 번들을 업로드하지 않고도 CLI에서 번들을 분석할 수 있습니다.

    $ android analyze <mybundle.aab>
    $ android analyze <mybundle.apk>
  • 또한 로컬 분석을 통해 정확히 동일한 정보가 즉시 직접 보고됩니다.

  • 이 경우 앱의 최적화 비율이 매우 낮다는 경고가 표시되고, Play 스토어에서와 마찬가지로 DEX 크기와 최적화 비율이 표시됩니다.

  • 또한 여기에는 JSON 출력이 있으므로 이러한 값이 급격히 떨어지는 것에 대한 회귀 테스트를 자동화하는 스크립트를 작성하려는 경우, 다시 말하지만 이러한 값은 Play에서 적용하는 숫자이므로 추적하고 싶다면 이 JSON 값, 즉 이러한 JSON 보고서를 사용하여 매우 쉽게 추적할 수 있습니다.

    {
      "optimization": {
        "bundleAnalysisResult": {
          "agpVersion": "9.3.2",
          "r8Json": {
            "version": "9.3.16",
            "options": {
              "isRepackageClassesEnabled": true,
              "isProguardCompatibilityModeEnabled": false
            },
            "stats": {
              "shrinkingRatio": 0.2306,
              "optimizationRatio": 0.2351,
              "obfuscationRatio": 0.2322
            },
            ...
          }
        "mappingObfuscationRatio": 0.21631891,
        "dexHeuristicObfuscationRatio": 0.25118626,
        "uncompressedDexSizeBytes": "16198152"
      }
  • 여기에서도 정확히 동일한 숫자가 표시됩니다.

  • android analyze의 기능은 이뿐만이 아닙니다.

  • android analyze는 라이브러리에서도 실행할 수 있습니다.

    $ android analyze <library.aar>
    $ android analyze <library.jar>
  • 브리아나가 언급했듯이 라이브러리 내부에 불행한 유지 규칙이 포함된 상태로 배포되는 라이브러리에는 많은 문제가 있습니다.

  • 이제 이를 사용하여 몇 가지 문제를 살펴보겠습니다.

  • 따라서 제가 가지고 있는 특정 AAR에서 이 도구를 실행하면, 이 라이브러리에 여러 문제가 있다는 것을 맨 위에 강조 표시된 부분에서 확인할 수 있습니다.

  • 이 라이브러리는 유지 비율이 높습니다. 즉, 제한되지 않은 유지 규칙으로 너무 많은 항목을 유지하고 있습니다.

  • 최적화를 방해하는 광범위한 유지 규칙이 포함되어 있으며, 아래에는 이것이 얼마나 나쁜지 정확히 보여주는 숫자가 있습니다. 또한 유지 규칙도 포함되어 있는데, 이 중 가장 무서운 것은 아무것도 타겟팅하지 않는다는 것입니다. 이는 아마도 제한 없는 방식으로 다른 누군가를 타겟팅하고 있다는 의미일 것입니다.

  • 매우 위험한 코드 스멜입니다.

  • 따라서 AAR에서 이 모든 것을 확인할 수 있습니다. 특히 라이브러리 게시자의 경우, 잘못된 유지 규칙을 배포하지 않고, 유지 규칙 작성에 능숙하지 않은 사람이 라이브러리를 작업할 때 유지 규칙이 유출되지 않도록 하는 좋은 방법입니다.

  • 여기에서 유지 비율도 출력되는 것을 확인할 수 있습니다.

  • 이는 라이브러리의 전반적인 품질을 평가하는 좋은 방법입니다.

  • R8을 사용하여 참조 없이 완전히 독립적으로 개별 라이브러리를 최적화하면 0바이트가 됩니다.

  • 내부에 보관된 항목이 없으므로 기본적으로 비어 있어야 합니다.

  • 그게 이상적일 것입니다.

  • 이 경우에는 그렇지 않습니다.

  • R8 및 D8 크기를 직접 확인할 수 있습니다.

  • 또한 최악의 규칙 중 일부를 강조 표시하여 더 잘 보이도록 합니다.

  • 이 역시 JSON 형식이므로 라이브러리 업데이트를 제어하려는 앱 개발자든 라이브러리 품질의 큰 회귀를 방지하려는 라이브러리 개발자든 동일한 분석을 수행하고 Android CLI를 래핑하는 간단한 스크립트로 이러한 문제를 방지할 수 있습니다.

    {
      "optimization": {
        "libraryAnalysisResult": {
          "dex": {
            "d8CompileResult": {
              "dexSizeBytes": 4523352
            },
            "r8CompileResult": {
              "dexSizeBytes": 3266596,
              "configuration": {
                "usedPackageWideKeepRules": [
                  {
                    "source": "-keep class com.example.analytics.bloat.** { *; }",
                    "keptItemCount": 125579,
                  },
                  ...
                ],
                "unusedPackageWideKeepRules": [
                  {
                    "source": "-keep class some.other.library.** { *; }",
                  },
                  ...
                ]
              },
              ...
    }
  • 따라서 라이브러리 문제를 발견하면 먼저 업데이트해 보세요.

  • 브리아나가 앞서 언급했듯이, Google은 12개 이상의 Google 라이브러리를 수정하여 몇 가지 매우 안타까운 유지 규칙을 수정했으며, 수정된 다른 라이브러리도 있지만 이를 사용하는 앱에서 업데이트되지 않고 있습니다.

  • 좋은 첫걸음입니다.

  • 둘째, 문제를 찾아 라이브러리 개발자에게 연락하고, 분석기 보고서를 첨부하여 '너무 많은 것을 유지하고 있습니다.

  • 앱을 최적화할 수 있도록 도와주세요."

핵심 내용 및 리소스

  • 요약하자면, R8 빌드에서 일어나는 모든 일, 최적화할 수 있는 것과 없는 것을 알려주는 모든 것을 살펴볼 수 있는 R8 Config Analyzer에 대해 이야기했습니다.

  • 손가락 하나 까딱하지 않고도 유지 규칙을 자동으로 작성할 수 있도록 도와주는 새로운 라이브러리인 Jetpack Keep에 대해 이야기했습니다.

  • 그리고 Android CLI는 마지막 단계를 다시 한번 확인하고, 팀의 에이전트나 주니어 엔지니어가 소비자를 위해 아무것도 망치지 않았는지 확인하는 데 도움이 되는 새로운 도구입니다.

  • 자세히 알아보려면 R8, 구성 분석기, Jetpack Keep에 관한 문서를 참고하세요.

  • Android CLI에 관한 문서에는 analyze 명령어와 사용 방법이 설명되어 있습니다.

  • 감사합니다.

코드

코드 스니펫

'DEX 최적화 소개'의 5분 15초 지점에 있는 Kotlin 스크립트 스니펫
android {
    buildTypes {
        release {
            optimization {
                // no need to specify default
                // rules and resource shrinking
                enabled = true
            }
        }
    }
    ...
}
'DEX 최적화 소개'의 5분 15초 지점에 있는 Kotlin 스크립트 스니펫
android {
    buildTypes {
        release {
            // Enables code optimizations.
            isMinifyEnabled = true

            // Enables resource shrinking.
            isShrinkResources = true

            proguardFiles(
                // Default rules.
                getDefaultProguardFile(
                    "proguard-android-optimize.txt"
                ),
                ... // list out each file with keep rules
            )
            ...
        }
    }
    ...
}
'우수사례:WHOOP 최적화'의 13: 19에 나오는 셸 스니펫
$ android skills list
$ android skills add r8-analyzer
'DEX 최적화에서 리플렉션의 과제'의 14:41에 나오는 Kotlin 스니펫
fun createWithReflection(className: String): MyObject {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as MyObject
}
'DEX 최적화에서 리플렉션의 과제'의 15:40에 나오는 Kotlin 스니펫
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}

// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}

// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
'DEX 최적화에서 리플렉션의 과제'의 16:52에 있는 셸 스니펫
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };
'DEX 최적화에서 리플렉션의 과제'의 16:59에 나오는 Kotlin 스니펫
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
'DEX 최적화에서 리플렉션의 과제'의 17:07에 있는 셸 스니펫
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
    *
}
'DEX 최적화에서 리플렉션의 과제'의 17:15에 있는 셸 스니펫
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
    *
}
'DEX 최적화에서 리플렉션의 과제'의 17:19에 있는 셸 스니펫
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
    *
}
'DEX 최적화에서 리플렉션의 과제'의 17:40에 있는 셸 스니펫
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
'DEX 최적화에서 리플렉션의 과제'의 17:50에 있는 셸 스니펫
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
   void <init>();
}
'DEX 최적화에서 리플렉션의 과제'의 18:03에 있는 셸 스니펫
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
    class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
    void <init>();
}
'DEX 최적화에서 리플렉션의 과제'의 18:57에 나오는 Kotlin 스니펫
@androidx.annotation.Keep
class EventFilter { … }

@androidx.annotation.Keep
class SizeBasedEventFilter { … }

@androidx.annotation.Keep
class DropLogsEventFilter { … }
'DEX 최적화에서 리플렉션의 과제'의 19:22에 있는 Kotlin 스니펫
@Keep
class EventFilter { … }
'DEX 최적화에서 리플렉션의 과제'의 19:29에 나오는 Kotlin 스니펫
@Keep
class EventFilter { … }

@Keep
class SizeBasedEventFilter { … }

@Keep
class DropLogsEventFilter { … }
'Jetpack Keep 소개'의 20:38에 나오는 Kotlin 스크립트 스니펫
plugins {
   id("com.android.library")
   id("androidx.annotation.keep")
}

dependencies {
    implementation("androidx.annotation.keep:keep")
    ...
}
'Introduction to Jetpack Keep'의 20:54에 있는 셸 스니펫
./gradlew analyticsbloat:lintFix
'Introduction to Jetpack Keep'의 20:55에 나오는 Kotlin 스니펫
@UsesReflectionToConstruct(
   classConstant = EventFilter::class,
   parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
   return Class.forName(className)
       .getDeclaredConstructor()
       .newInstance() as? EventFilter
}
'Jetpack Keep 소개'의 21:25에 있는 셸 스니펫
# Generated Keep Rule

-if class com.example.analytics.bloat.EventTools {
    class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
    void <init>();
}
'Introduction to Jetpack Keep'의 21:52에 나오는 Kotlin 스니펫
@UsesReflectionToConstruct(
   className = "com.example.analytics.bloat.crash.CrashReporterTransport",
   parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
   return runCatching {
       Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
           .getDeclaredConstructor(Context::class.java)
           .newInstance(context) as? EventTransport
   }.getOrNull()
}
'Jetpack Keep 소개'의 22:05에 나오는 셸 스니펫
# Generated Keep Rule

-if class com.example.analytics.bloat.CrashConnection {
    class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
    void <init>(android.content.Context);
}
'Introduction to Jetpack Keep'의 22:16에 나오는 Kotlin 스니펫
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
   classConstant = UserActionEvent::class,
   fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
   val json = JSONObject()
   for (field in event.javaClass.declaredFields) {
       field.isAccessible = true
       json.put(field.name, field.get(event))
   }
   return json
}
'Jetpack Keep 소개'의 22:27에 나오는 셸 스니펫
# Generated Keep Rule

-if class com.example.analytics.bloat.serialization.EventSerializer {
    class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
    <fields>;
}
'Analyzing apps with Android CLI'의 24:22에 있는 셸 스니펫
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>
'Android CLI로 앱 분석하기'의 24:52에 나오는 JSON 스니펫
{
  "optimization": {
    "bundleAnalysisResult": {
      "agpVersion": "9.3.2",
      "r8Json": {
        "version": "9.3.16",
        "options": {
          "isRepackageClassesEnabled": true,
          "isProguardCompatibilityModeEnabled": false
        },
        "stats": {
          "shrinkingRatio": 0.2306,
          "optimizationRatio": 0.2351,
          "obfuscationRatio": 0.2322
        },
        ...
      }
    "mappingObfuscationRatio": 0.21631891,
    "dexHeuristicObfuscationRatio": 0.25118626,
    "uncompressedDexSizeBytes": "16198152"
  }
'Android CLI로 앱 분석하기'의 25:17에 있는 셸 스니펫
$ android analyze <library.aar>
$ android analyze <library.jar>
'Android CLI로 앱 분석하기'의 26:46에 있는 JSON 스니펫
{
  "optimization": {
    "libraryAnalysisResult": {
      "dex": {
        "d8CompileResult": {
          "dexSizeBytes": 4523352
        },
        "r8CompileResult": {
          "dexSizeBytes": 3266596,
          "configuration": {
            "usedPackageWideKeepRules": [
              {
                "source": "-keep class com.example.analytics.bloat.** { *; }",
                "keptItemCount": 125579,
              },
              ...
            ],
            "unusedPackageWideKeepRules": [
              {
                "source": "-keep class some.other.library.** { *; }",
              },
              ...
            ]
          },
          ...
}