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