bazel 모바일 설치

Android의 빠른 반복 개발

이 페이지에서는 bazel mobile-install을 사용하여 Android의 반복 개발 을 훨씬 빠르게 만드는 방법을 설명합니다. 이 접근 방식의 이점과 기존 앱 설치 방법의 문제점을 설명합니다.

요약

Android 앱에 작은 변경사항을 매우 빠르게 설치하려면 다음 단계를 따르세요.

  1. 설치하려는 앱의 android_binary 규칙을 찾습니다.
  2. proguard_specs 속성을 삭제하여 Proguard를 사용 중지합니다.
  3. multidex 속성을 native로 설정합니다.
  4. dex_shards 속성을 10으로 설정합니다.
  5. USB를 통해 ART (Dalvik 아님)를 실행하는 기기를 연결하고 USB 디버깅을 사용 설정합니다.
  6. bazel mobile-install :your_target을 실행합니다. 앱 시작이 평소보다 약간 느립니다.
  7. 코드 또는 Android 리소스를 수정합니다.
  8. bazel mobile-install --incremental :your_target을 실행합니다.
  9. 오래 기다리지 않아도 됩니다.

유용할 수 있는 Bazel의 일부 명령줄 옵션은 다음과 같습니다.

  • --adb는 Bazel에 사용할 adb 바이너리를 알려줍니다.
  • --adb_arg를 사용하여 adb의 명령줄에 추가 인수를 추가할 수 있습니다. 이 방법의 유용한 애플리케이션 중 하나는 워크스테이션에 여러 기기가 연결되어 있는 경우 설치할 기기를 선택하는 것입니다. to if you have multiple devices connected to your workstation: 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"는 빌드에서 정확히 한 번 호출되며 이전 빌드의 작업을 재사용하는 방법을 알지 못합니다. 메서드가 하나만 변경되었더라도 모든 메서드를 다시 덱싱합니다.

  • 기기에 데이터 업로드. adb는 USB 2.0 연결의 전체 대역폭을 사용하지 않으며 대용량 앱은 업로드하는 데 시간이 오래 걸릴 수 있습니다. 예를 들어 리소스 또는 단일 메서드와 같이 작은 부분만 변경되었더라도 전체 앱이 업로드되므로 주요 병목 현상이 될 수 있습니다.

  • 네이티브 코드로 컴파일. Android L은 Dalvik과 같이 JIT 컴파일하는 대신 AOT 컴파일하는 새로운 Android 런타임인 ART를 도입했습니다. 이렇게 하면 설치 시간이 길어지는 대신 앱이 훨씬 빨라집니다. 사용자는 일반적으로 앱을 한 번 설치하고 여러 번 사용하므로 이는 사용자에게 좋은 절충안이지만 앱이 여러 번 설치되고 각 버전이 최대 몇 번 실행되는 개발 속도가 느려집니다.

bazel mobile-install의 접근 방식

bazel mobile-install은 다음과 같이 개선합니다.

  • 샤딩된 덱싱. 앱의 Java 코드를 빌드한 후 Bazel은 클래스 파일을 대략 동일한 크기의 부분으로 샤딩하고 dx을 별도로 호출합니다. dx는 마지막 빌드 이후 변경되지 않은 샤드에서는 호출되지 않습니다.

  • 증분 파일 전송. Android 리소스, .dex 파일, 네이티브 라이브러리가 기본 .apk에서 삭제되고 별도의 mobile-install 디렉터리에 저장됩니다. 이렇게 하면 전체 앱을 다시 설치하지 않고도 코드와 Android 리소스를 독립적으로 업데이트할 수 있습니다. 따라서 파일을 전송하는 데 시간이 덜 걸리고 변경된 .dex 파일만 기기에서 다시 컴파일됩니다.

  • .apk 외부에서 앱의 일부 로드. Android 리소스, Java 코드, 네이티브 코드를 기기 내 mobile-install 디렉터리에서 로드한 후 실제 앱으로 제어를 전송하는 작은 스텁 애플리케이션이 .apk에 배치됩니다. 이는 아래에 설명된 몇 가지 특수한 경우를 제외하고 앱에 완전히 투명합니다.

샤딩된 덱싱

샤딩된 덱싱은 비교적 간단합니다. .jar 파일이 빌드되면 a tool 도구가 이를 대략 동일한 크기의 별도 .jar 파일로 샤딩한 후 이전 빌드 이후 변경된 파일에서 dx를 호출합니다. 덱싱할 샤드를 결정하는 로직은 Android에만 국한되지 않습니다. Bazel의 일반적인 변경사항 가지치기 알고리즘을 사용합니다.

샤딩 알고리즘의 첫 번째 버전은 단순히 .class 파일을 알파벳순으로 정렬한 후 목록을 동일한 크기의 부분으로 잘랐지만 이는 최적이 아닌 것으로 밝혀졌습니다. 클래스가 추가되거나 삭제되면 (중첩되거나 익명 클래스도 포함) 알파벳순으로 그 뒤에 있는 모든 클래스가 하나씩 이동하여 해당 샤드를 다시 덱싱하게 됩니다. 따라서 개별 클래스 대신 Java 패키지를 샤딩하기로 결정했습니다. 물론 새 패키지가 추가되거나 삭제되면 여전히 많은 샤드가 덱싱되지만 이는 단일 클래스를 추가하거나 삭제하는 것보다 훨씬 덜 자주 발생합니다.

샤드 수는 BUILD 파일에 의해 제어됩니다 (android_binary.dex_shards 속성 사용). 이상적인 세계에서 Bazel은 최적의 샤드 수를 자동으로 결정하지만 Bazel은 현재 작업을 실행하기 전에 작업 집합 (예: 빌드 중에 실행할 명령어)을 알아야 하므로 앱에 최종적으로 포함될 Java 클래스 수를 알 수 없으므로 최적의 샤드 수를 결정할 수 없습니다. 일반적으로 샤드가 많을수록 빌드 및 설치가 빨라지지만 동적 링커가 더 많은 작업을 해야 하므로 앱 시작이 느려집니다. 일반적으로 10~50개의 샤드가 적절합니다.

증분 파일 전송

앱을 빌드한 후 다음 단계는 가능한 한 적은 노력으로 앱을 설치하는 것입니다. 설치는 다음 단계로 구성됩니다.

  1. .apk 설치 (일반적으로 adb install 사용)
  2. .dex 파일, Android 리소스, 네이티브 라이브러리를 mobile-install 디렉터리에 업로드

첫 번째 단계에서는 증분성이 크지 않습니다. 앱이 설치되거나 설치되지 않습니다. Bazel은 모든 경우에 필요한지 여부를 확인할 수 없으므로 현재 사용자가 이 단계를 실행해야 하는지 --incremental 명령줄 옵션을 통해 표시하도록 합니다.

두 번째 단계에서는 빌드의 앱 파일이 기기에 있는 앱 파일과 체크섬을 나열하는 기기 내 매니페스트 파일과 비교됩니다. 새 파일은 기기에 업로드되고 변경된 파일은 업데이트되며 삭제된 파일은 기기에서 삭제됩니다. 매니페스트가 없으면 모든 파일을 업로드해야 한다고 가정합니다.

기기에서 파일을 변경하되 매니페스트의 체크섬은 변경하지 않음으로써 증분 설치 알고리즘을 속일 수 있습니다. 기기의 파일 체크섬을 계산하여 이를 방지할 수 있었지만 설치 시간 증가의 가치가 없다고 판단되었습니다.

스텁 애플리케이션

스텁 애플리케이션은 기기 내 mobile-install 디렉터리에서 dex, 네이티브 코드, Android 리소스를 로드하는 마법이 일어나는 곳입니다.

실제 로드는 BaseDexClassLoader를 서브클래싱하여 구현되며 비교적 잘 문서화된 기법입니다. 이는 앱의 클래스가 로드되기 전에 발생하므로 apk에 있는 모든 애플리케이션 클래스를 기기 내 mobile-install 디렉터리에 배치하여 adb install 없이 업데이트할 수 있습니다.

앱의 클래스가 로드되기 전에 발생해야 하므로 애플리케이션 클래스가 .apk에 있을 필요가 없습니다. 즉, 이러한 클래스를 변경하려면 전체 재설치가 필요합니다.

이는 Application 클래스를 AndroidManifest.xml 에 지정된 스텁 애플리케이션으로 대체하여 수행됩니다. 이렇게 하면 앱이 시작될 때 제어 권한을 가져오고 Android 프레임워크의 내부에 Java 리플렉션을 사용하여 가장 빠른 순간 (생성자)에 클래스 로더와 리소스 관리자를 적절하게 조정합니다.

스텁 애플리케이션이 하는 또 다른 작업은 mobile-install에서 설치한 네이티브 라이브러리 를 다른 위치로 복사하는 것입니다. 동적 링커는 파일에 X 비트가 설정되어 있어야 하므로 이는 필요합니다. 루트가 아닌 adb에서 액세스할 수 있는 위치에서는 이를 실행할 수 없습니다.

이러한 작업이 모두 완료되면 스텁 애플리케이션은 실제 Application 클래스를 인스턴스화하여 Android 프레임워크 내에서 자체에 대한 모든 참조를 실제 애플리케이션으로 변경합니다.

결과

성능

일반적으로 bazel mobile-install을 사용하면 작은 변경사항 후 대용량 앱을 빌드 하고 설치하는 속도가 4~10배 빨라집니다.

다음 숫자는 몇 가지 Google 제품에 대해 계산되었습니다.

물론 이는 변경사항의 특성에 따라 다릅니다. 기본 라이브러리를 변경한 후 다시 컴파일하는 데 시간이 더 걸립니다.

제한사항

스텁 애플리케이션이 실행하는 트릭은 모든 경우에 작동하지 않습니다. 다음 사례는 예상대로 작동하지 않는 경우를 보여줍니다.

  • ContextContentProvider#onCreate()에서 Application 클래스로 캐스팅되는 경우. 이 메서드는 애플리케이션 시작 중에 호출되므로 Application 클래스의 인스턴스를 대체할 기회가 있기 전에 ContentProvider는 실제 애플리케이션 대신 스텁 애플리케이션을 계속 참조합니다. 이러한 방식으로 Context를 다운캐스팅해서는 안 되므로 버그가 아니라고 주장할 수 있지만 Google의 일부 앱에서 발생하는 것으로 보입니다.

  • bazel mobile-install에서 설치한 리소스는 앱 내에서만 사용할 수 있습니다. 리소스가 PackageManager#getApplicationResources()를 통해 다른 앱에서 액세스되는 경우 이러한 리소스는 마지막 비증분 설치에서 가져옵니다.

  • ART를 실행하지 않는 기기. 스텁 애플리케이션은 Froyo 이상에서 잘 작동하지만 Dalvik에는 특정 경우(예: Java 주석이 특정 방식으로 사용되는 경우)에 코드가 여러 .dex 파일에 배포되면 앱이 잘못되었다고 생각하게 만드는 버그가 있습니다. 앱이 이러한 버그를 발생시키지 않는 한 Dalvik에서도 작동해야 합니다. 단, 이전 Android 버전 지원은 Google의 주요 관심사가 아님)