Android용 빠른 반복 개발
이 페이지에서는 bazel mobile-install
가 Android의 반복 개발 속도를 어떻게 높이는지 설명합니다. 이 접근 방식은 기존 앱 설치 방법의 도전과제와 비교하여 이 방법의 이점을 설명합니다.
요약
Android 앱의 사소한 변경사항을 빠르게 설치하려면 다음을 실행하세요.
- 설치하려는 앱의
android_binary
규칙을 찾습니다. proguard_specs
속성을 삭제하여 Proguard를 사용 중지합니다.multidex
속성을native
로 설정합니다.dex_shards
속성을10
로 설정합니다.- USB를 통해 ART가 아닌 ART를 실행하는 기기를 연결하고 USB 디버깅을 사용 설정합니다.
bazel mobile-install :your_target
를 실행합니다. 앱 시작 속도가 평소보다 약간 느립니다.- 코드 또는 Android 리소스를 수정합니다.
bazel mobile-install --incremental :your_target
를 실행합니다.- 기다리지 않아도 됩니다.
Bazel에 유용한 몇 가지 명령줄 옵션은 다음과 같습니다.
--adb
는 사용할 adb 바이너리를 Bazel에 알립니다.--adb_arg
를 사용하여adb
의 명령줄에 인수를 추가할 수 있습니다. 워크스테이션에 연결된 기기가 여러 개 있다면 설치할 기기를 선택하는 것도 유용합니다.bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
--start_app
앱이 자동으로 시작됩니다.
확실하지 않은 경우 예시를 살펴보거나 Google에 문의하세요.
소개
개발자의 도구 모음에서 가장 중요한 속성 중 하나는 속도입니다. 코드가 변경되는 것과 1초 이내에 실행되는 것을 확인하는 것, 그리고 몇 분, 몇 시간씩 기다려야 변경사항이 예상대로 작동하는지 확인할 수 있습니다.
안타깝게도 .apk를 빌드하기 위한 기존의 Android 도구 모음에는 수많은 모놀리식 순차적 단계가 수반되며 Android 앱을 빌드하려면 이 작업을 모두 수행해야 합니다. Google에서는 한 줄의 변경사항을 빌드하는 데 5분을 기다려야 하는 경우가 Google 지도와 같은 대규모 프로젝트에서는 일반적이지 않았습니다.
bazel mobile-install
는 앱 코드를 변경하지 않고 Android 내부의 변경사항 프루닝, 작업 샤딩, 영리한 조작 조합을 사용하여 Android의 반복 개발을 훨씬 빠르게 합니다.
기존 앱 설치 관련 문제
Android 앱을 빌드하면 다음과 같은 문제가 발생합니다.
덱싱. 기본적으로 'dx'는 빌드에서 정확히 한 번 호출되며 이전 빌드의 작업을 재사용하는 방법을 알지 못합니다. 하나의 메서드만 변경되었더라도 모든 메서드를 다시 덱싱합니다.
기기에 데이터를 업로드하는 중입니다. adb는 USB 2.0 연결의 전체 대역폭을 사용하지 않으며 더 큰 앱은 업로드하는 데 시간이 오래 걸릴 수 있습니다. 리소스 또는 단일 메서드 등 사소한 부분만 변경된 경우에도 전체 앱이 업로드되므로 이는 심각한 병목 현상일 수 있습니다.
네이티브 코드 컴파일. Android L에서는 Dalvik과 같이 시의적절하게 앱을 컴파일하는 대신 사전에 앱을 컴파일하는 새로운 Android 런타임인 ART를 도입했습니다. 이렇게 하면 설치 시간이 길어지는 동시에 앱 속도가 훨씬 빨라집니다. 이는 일반적으로 앱을 한 번 설치하고 여러 번 사용하기 때문에 사용자에게 바람직한 단점이지만, 앱이 여러 번 설치되고 각 버전은 최대 몇 번만 실행되는 개발이 느려집니다.
bazel mobile-install
의 접근 방식
bazel mobile-install
개선된 내용은 다음과 같습니다.
샤딩 덱싱 앱의 자바 코드를 빌드한 후 Bazel은 클래스 파일을 거의 같은 크기의 부분으로 샤딩하여 각각에서
dx
를 호출합니다.dx
는 마지막 빌드 이후 변경되지 않은 샤드에서 호출되지 않습니다.증분 파일 전송. Android 리소스, .dex 파일, 네이티브 라이브러리가 기본 .apk에서 삭제되고 별도의 모바일 설치 디렉터리에 저장됩니다. 이렇게 하면 전체 앱을 재설치하지 않고도 코드와 Android 리소스를 독립적으로 업데이트할 수 있습니다. 따라서 파일을 전송하는 데 걸리는 시간이 줄어들고 변경된 .dex 파일만 기기에서 다시 컴파일됩니다.
.apk 외부에서 앱의 일부를 로드합니다. 작은 리소스 스텁 애플리케이션은 Android 리소스, 자바 코드, 네이티브 코드를 기기 내 모바일 설치 디렉터리에서 로드한 다음 컨트롤을 실제 앱으로 전송하는 .apk에 삽입됩니다. 이는 앱에 투명하게 적용됩니다. 단, 아래에 설명된 몇 가지 특수한 경우는 예외입니다.
샤딩 덱싱
샤딩 덱싱은 상당히 간단합니다. .jar 파일을 빌드하면 도구가 거의 동일한 크기의 별도의 .jar 파일로 샤딩한 후 이전 빌드 이후 변경된 .jar 파일에 dx
을 호출합니다. dex할 샤드를 결정하는 로직은 Android에만 국한되지 않습니다. Bazel의 일반 변경 프루닝 알고리즘만 사용합니다.
샤딩 알고리즘의 첫 번째 버전은 단순히 .class 파일을 알파벳순으로 정렬한 다음 목록을 동일한 크기의 부분으로 자랐지만 이 방법은 최적화되지 않은 것으로 나타났습니다. 클래스가 추가되거나 삭제되면 (중첩되거나 익명인 경우도 해당) 모든 클래스가 알파벳순으로 이동하므로 이 샤드가 다시 덱싱됩니다. 따라서 개별 클래스가 아닌 자바 패키지를 샤딩하기로 결정했습니다. 물론 새 패키지가 추가되거나 삭제되면 여전히 많은 샤드가 덱싱됩니다. 하지만 단일 클래스를 추가하거나 삭제하는 것보다 훨씬 드뭅니다.
샤드 수는 android_binary.dex_shards
속성을 사용하여 BUILD 파일에서 제어합니다. 이상적인 환경에서는 Bazel이 가장 많은 샤드를 자동으로 결정하지만 Bazel은 현재 일련의 작업을 실행하기 전에 알고 있어야 합니다 (예: 빌드 중에 실행되는 명령어). 최적의 자바 개수는 모릅니다. 자바 클래스가 최종적으로 몇 개나 될지는 모르고 있기 때문입니다. 일반적으로 샤드는 많을수록 빠를수록 출시가 빠를수록 좋습니다. 일반적으로 10~50개의 샤드가 있습니다.
증분 파일 전송
앱을 빌드한 후 다음 단계는 최대한 적게 설치하는 것입니다. 설치는 다음 단계로 구성됩니다.
- .apk 설치 (일반적으로
adb install
사용) - dex 파일, Android 리소스, 네이티브 라이브러리를 모바일 설치 디렉터리에 업로드
첫 번째 단계에서는 성과 증분이 많지 않습니다. 즉, 앱이 설치되어 있는지 여부를 나타냅니다. 필요한 경우 모든 경우에 확인할 수 없으므로 Bazel은 현재 --incremental
명령줄 옵션을 통해 이 단계를 수행해야 하는지 여부를 사용자에게 나타냅니다.
두 번째 단계에서는 빌드의 앱 파일을 기기에 있는 앱 파일과 체크섬을 나열하는 기기 매니페스트 파일과 비교합니다. 모든 새 파일이 기기에 업로드되고 변경된 파일이 업데이트되며 삭제된 모든 파일이 기기에서 삭제됩니다. 매니페스트가 없으면 모든 파일을 업로드해야 한다고 가정합니다.
기기의 파일을 변경하여 매니페스트에서 증분 설치 알고리즘을 속일 수 있지만 매니페스트에서 체크섬은 변경할 수 없습니다. 이는 기기에 있는 파일의 체크섬을 계산하여 보호될 수 있었지만 설치 시간을 늘리는 가치는 없는 것으로 판단되었습니다.
Stub 애플리케이션
스텁 애플리케이션은 기기 내 mobile-install
디렉터리에서 dex, 네이티브 코드 및 Android 리소스를 로드하는 매직이 발생하는 곳입니다.
실제 로드는 BaseDexClassLoader
의 서브클래스를 통해 구현되며 상당히 문서화된 기술입니다. 이는 앱의 클래스가 로드되기 전에 발생하므로 APK에 있는 모든 애플리케이션 클래스가 기기 내 mobile-install
디렉터리에 배치되어 adb install
없이 업데이트될 수 있게 됩니다.
이 작업은 앱의 클래스가 로드되기 전에 이루어져야 하므로, 애플리케이션 클래스를 .apk에 추가할 필요가 없습니다. 즉, 클래스 변경 시 완전히 다시 설치해야 합니다.
AndroidManifest.xml
에 지정된 Application
클래스를 스텁 애플리케이션으로 대체하면 됩니다. 그러면 앱이 시작될 때를 제어하고, Android 프레임워크 내부에서 자바 리플렉션을 사용하여 가장 빠른 시점에 클래스 생성자와 리소스 관리자를 적절하게 조정합니다.
스텁 애플리케이션의 또 다른 작업은 모바일 설치에서 설치한 네이티브 라이브러리를 다른 위치에 복사하는 것입니다. 동적 링커는 파일에 X
비트를 설정해야 하는데 이는 루트가 아닌 adb
에서 액세스할 수 있는 어떤 위치든 할 수 없기 때문입니다.
이러한 작업이 모두 완료되면 스텁 애플리케이션이 실제 Application
클래스를 인스턴스화하고 자체 참조를 모든 Android 프레임워크 내의 실제 애플리케이션으로 변경합니다.
결과
성능
일반적으로 bazel mobile-install
는 약간의 변경 후에 대규모 앱을 빌드하고 설치하는 속도를 4~10배 높입니다.
다음은 몇 가지 Google 제품에 대해 계산된 수치입니다.
물론 이는 변경의 특성에 따라 다릅니다. 기본 라이브러리를 변경한 후 다시 컴파일하는 데 더 많은 시간이 걸립니다.
제한사항
모든 경우에서 스텁 애플리케이션이 실행하는 트릭은 작동하지 않습니다. 다음 사례는 예상대로 작동하지 않는 위치를 강조하여 보여줍니다.
Context
가ContentProvider#onCreate()
의Application
클래스로 변환되는 경우. 이 메서드는 애플리케이션 시작 중에 호출되어Application
클래스의 인스턴스를 대체하기 전에ContentProvider
가 실제 애플리케이션 대신 스텁 애플리케이션을 계속 참조합니다. 이와 같은Context
를 다운캐스트하지 않았기 때문에 버그가 아닙니다. 하지만 Google의 일부 앱에서 발생하는 것 같습니다.bazel mobile-install
에서 설치한 리소스는 앱 내에서만 사용할 수 있습니다.PackageManager#getApplicationResources()
를 통해 다른 앱에서 리소스에 액세스하는 경우 이 리소스는 증분이 아닌 마지막 설치에서 가져옵니다.ART를 실행하지 않는 기기 스텁 애플리케이션이 Froyo 이상에서 잘 작동하더라도 특정 사례(예: 자바 주석이 특정한 방식으로 사용되는 경우)에서 코드가 여러 .dex 파일에 배포된 경우 앱이 잘못되었다고 생각하는 버그가 있습니다. 앱이 이러한 버그를 간과하지 않는 한 Dalvik도 함께 작동해야 합니다 (단, 이전 Android 버전 지원은 정확히 중점을 두지 않음).