JD Finance의 안드로이드 슬리밍 탐색 및 실습

저자: JD Technology Feng Jianhua

1. 배경

비즈니스의 지속적인 반복 업데이트로 앱의 크기도 급격히 증가하고 있으며 2019년부터 2022년까지 한때 1억 1700만 개를 초과했으며 이 기간 동안 그림 1의 빨간색 부분과 같이 일부 최적화도 수행했습니다. , 우리는 최적화하는 동안 새로운 도전에 직면하고 있습니다.증분 코드, 패키지 크기는 계속 증가했습니다 . 패키지 크기는 다운로드 전환율, 설치 시간, 디스크 공간과 같은 중요한 지표에 직간접적으로 영향을 미치므로 설치 패키지 크기의 더 깊은 최적화를 탐색하는 데 에너지를 쏟을 필요가 있습니다. Google 스토어의 내부 데이터에 따르면 APK 크기가 1,000만 감소할 때마다 다운로드 전환율이 평균 ~1.5% 증가할 수 있습니다(그림 2 참조).

그림 1 JD Finance Android 버전 2019-2022의 볼륨 변경 프로세스(빨간색 부분은 해당 기간 동안 수행된 최적화의 일부이지만 곧 반등)

그림 2 Google 스토어 앱 전환율 증가 / 천만 [1]

이에 2022년 9월부터 금융 APP 슬리밍을 위한 특단의 수정을 진행하였으며, 상황의 증가를 고려하지 않고 비즈니스 코드 삭제 없이 117M에서 74M로 슬리밍을 실현하였습니다. 이번에는 설치 패키지를 다운하면서 많은 함정에 부딪혔지만 경험도 축적되어 여러분과 공유하고자 합니다.

2. APK 분석

다음으로 Apk의 다양한 구성 요소와 ZIP으로 Apk의 표준 구조를 간략하게 분석하여 패키지 슬리밍의 목표 설정 및 작업 분해에 대한 데이터 지원을 제공합니다.

2.1 APK 콘텐츠 분석

그림 3 APK 구조

•classes.dex APK는 하나 이상의 classes.dex 파일을 포함할 수 있으며 애플리케이션의 Java/Kotlin 소스 코드는 결국 바이트코드 형태로 classes.dex 파일에 존재하게 됩니다.

•resources.arsc  aapt 도구는 리소스를 컴파일할 때 일부 리소스 또는 리소스 인덱스를 resources.arsc로 패키징합니다.

•res/소스코드 프로젝트에서 res 디렉토리 아래 값을 제외한 리소스 파일들과 이들 파일들의 경로는 resources.arsc에 동시에 기록된다.

• lib/nativeLibraries, 즉 소스 코드 프로젝트의 jni 디렉토리 아래에 있는 so 파일이고 보조 디렉토리는 NDK에서 지원하는 ABI입니다.

•assets/는 res/ 리소스 디렉토리와 다릅니다.assets/ 아래의 리소스 파일은 resources.arsc에 쿼리 항목을 생성하지 않으며, assets/ 아래의 리소스 디렉토리는 프로그램의 AssetManager 개체를 통해 완전히 사용자 정의하고 가져올 수 있습니다.

•META-INF/  이 폴더에는 주로 CERT.SF 및 CERT.RSA 서명 파일과 MANIFEST.MF 매니페스트 파일이 들어 있습니다 .

•AndroidManifest.xml 애플리케이션 매니페스트 파일은 주로 애플리케이션 패키지 이름, 애플리케이션 ID, 애플리케이션 구성요소, 필요한 권한, 기기 호환성 등 애플리케이션의 기본 정보를 설명하는 데 사용됩니다.

2.2 SDK 크기 분석

자체 개발한 에너지 효율 개선 플랫폼인 Pandora[7]를 통해 그림 4와 같이 SDK의 크기를 직관적으로 확인할 수 있습니다.

그림 4 SDK 크기 정렬(버전 번호 포함)

 

그림 5 SDK에 포함된 SO 라이브러리 목록 및 크기

SDK의 분석과 비즈니스와의 결합에 따라 플러그인에 적합한 비즈니스를 결정한 다음 패키지 크기를 직관적으로 줄입니다.

2.3 ZIP 구조 분석

zipinfo 명령을 사용하여 압축 패키지의 각 파일에 대한 자세한 정보 로그를 출력할 수 있습니다. 사용법: zipinfo -l --t --h test.apk > test.txt

그림 6과 같이 출력 로그 파일이 열립니다. 파일 이름, 원래 크기, 압축된 크기 및 기타 표시기를 포함하여 각 파일에 대한 한 줄의 압축 정보가 있습니다.

 

그림 6 APK의 파일 정보 크기

위의 로그 정보를 한 줄씩 분석하고 난독화 된 파일 이름 경로와 파일 유형에 따라 분류 및 카운트하면 다양한 유형의 파일 수, 총 크기, 단일 파일 크기 및 기타 지표 및 파일 크기 인덱스를 생성합니다.

3. 슬리밍 연습

전체 구현 경로는 그림 7에 나와 있으며 주로 다음과 같이 나뉩니다.

1. 기존 기술 솔루션은 Gradle 플러그인(코드 침입 없음, 자동화)을 통해 컴파일 중에 APP 슬리밍을 완료합니다.

2. 고급 기술 솔루션, 플러그인 또는 SO 동적 다운로드를 통해 일부 비즈니스 라인을 다르게 변환합니다.비즈니스 변환이 많을수록 혜택이 높아집니다.

3. 비즈니스 라인의 데이터 매장 지점을 기반으로 비즈니스 최적화 계획은 순위를 위한 액세스 UV를 생성하고 UV가 낮은 비즈니스 라인을 아키텍처 위원회에 피드백하여 오프라인 또는 고급 기술 솔루션을 통해 변환할 수 있는지 평가합니다. (2), 패키지 크기 줄이기.

 

그림 7 전체 구현 경로

3-1 기존 기술 솔루션

3-1-1 이미지 처리

위의 APP 분석 결과 이미지가 가장 큰 볼륨을 차지하는 것으로 결론이 나므로 SDK를 포함한 APP 내 모든 이미지는 컴파일 및 패키징 과정에서 슬리밍 작업을 통해 자동으로 최적화되어 전체적인 최적화 방안을 보여준다. 그림 8:

그림 8 사진 최적화 체계

1. 다중 DPI 최적화:

Android는 해상도나 모드가 다른 기기에 적응하기 위해 동일한 리소스의 여러 구성을 가진 개발자를 위한 리소스 경로를 설계했습니다. 명백한 문제는 고해상도 장치에 저해상도 쓸모없는 이미지가 포함되어 있거나 저해상도 장치에 고해상도 쓸모없는 이미지가 포함되어 있다는 것입니다.

정상적인 상황에서 국내 애플리케이션 시장의 경우 패키지 크기를 줄이기 위해 앱은 모든 장치와 호환되도록 시장 점유율이 가장 높은 dpi 세트(구글은 xxhdpi 권장)를 선택합니다. 해외 애플리케이션 시장을 겨냥한 대부분의 앱은 패키징되어 AppBundle을 통해 Google Play에 업로드되며 dpi를 동적으로 분배하는 기능을 누릴 수 있습니다. 모든 기기를 만족시키는 dpi의. . 프로젝트에서 일부 사진에는 dpi가 한 세트만 있고 일부는 여러 dpi 세트가 있습니다.위 두 시나리오의 경우 패키징할 때 리소스를 병합하고 리소스를 복사하여 패키지 크기를 줄였습니다.

2. webp 형식으로 변환:

WebP 는 Google에서 제공하는 이미지 파일 형식으로 손실 압축과 무손실 압축을 지원하며 JPEG 또는 PNG보다 더 나은 압축을 제공할 수 있습니다. Android 4.0(API 레벨 14)에서 손실 WebP 이미지 지원, Android 4.3(API 레벨 18) 이상에서 무손실 투명 WebP 이미지 지원

따라서 플러그인을 사용하여 컴파일 기간 동안 Google에서 제공하는 셸 프로그램을 통해 이미지 형식을 변환하고 변환 시 성공적으로 이전 이미지를 삭제하여 APK 슬리밍 효과를 얻습니다.

3.png 압축

Pngquant는 사용하기 쉬운 png 압축 도구, 손실 이미지 압축을 수행할 수 있는 명령줄 도구이므로 1과 2의 처리 후 Pngquant를 사용하여 2차 압축을 수행하여 더 나은 이미지 슬리밍을 달성할 수 있습니다.

3-1-2 R 파일 인라인 최적화

DEX는 Java/Kotlin 소스 코드에서 컴파일된 바이트 코드 파일입니다.DEX의 최적화는 실제로 바이트 코드 파일을 최적화하는 방법입니다.DEX에는 많은 리소스 인덱스 R 파일이 포함되어 있습니다.여기서는 주로 리소스 ID를 통해 인라인하는 방법에 대해 이야기합니다. APK 슬리밍의 목적을 달성하기 위해 R 파일을 삭제합니다.

R 파일 슬리밍의 타당성 분석

일일 개발 단계에서 R.xx.xx를 통해 메인 프로젝트의 리소스를 참조하고 컴파일 후 R 클래스 참조에 해당하는 상수를 클래스로 컴파일합니다.

setContentView(2131427356);

이 변경을 인라인이라고 하며 이는 Java의 메커니즘입니다(상수가 static final로 표시되면 상수는 Java 컴파일 중에 코드에 인라인되어 변수의 메모리 주소 지정을 한 번 줄임). 기본이 아닌 프로젝트에서 R 클래스 리소스 ID는 참조에 의해 클래스로 컴파일되며 인라인이 생성되지 않습니다.

setContentView(R.layout.activity_main);

이 현상의 원인은 AGP 패키징 도구 때문입니다. 자세한 내용은 R 파일에서 android gradle 플러그인의 처리 과정을 확인할 수 있습니다. 결론: R 클래스 id 인라인 후 프로그램을 실행할 수 있지만 모든 프로젝트가 자동으로 인라인 현상을 생성하는 것은 아닙니다. 적시에 R 클래스 id를 프로그램에 인라인하기 위한 기술적 수단을 사용해야 합니다. R 파일에 의존하는 경우 다시 말하지만 응용 프로그램이 정상적으로 실행되는 동안 패키지 크기를 줄이는 목적을 달성하기 위해 R 파일을 삭제할 수 있습니다.그림 9와 같이 컴파일이 완료된 후 많은 수의 R 파일이 생성됩니다.

그림 9 프로젝트 R 파일 생성의 개략도

전체 계획은 그림 10에 나와 있습니다.

그림 10 R 파일 최적화 프로세스

참고 : 교체 단계에서 아래와 같이 교체가 완료된 후 런타임에 ResourceNotFind 예외가 나타나지 않도록 보조 검사를 추가해야 합니다.

try {
    int value = RManager.checkInt(type, name);
}catch (Exception e){
    String errorMsg = "resource is not found(I),className="+className+",fieldName="+owner+"."+name;
    throw new ResourceNotFoundException(errorMsg);
}
try {
    int[] value = RManager.checkIntArray(type, name);
}catch (Exception e){
    String errorMsg = "resource is not found(I[]),className="+className+",fieldName="+owner+"."+name;
    throw new ResourceNotFoundException(errorMsg);
}

 

리소스 혼란에 대한 3-1-3 AndResGuard

1. 자원 로딩 프로세스 분석

개발 과정에서 aapt에서 생성한 R.java의 상수를 자원으로 사용하며, 컴파일 후 상수가 사용되는 곳은 아래와 같이 상수 값으로 대체됩니다.

final View layout = inflater.inflate(2131165182, container, false);

즉, int 값을 사용하여 Resource를 통해 리소스를 찾습니다. 그러면 Resource는 int 값을 통해 특정 리소스를 어떻게 찾습니까? apk 압축을 풀면 내부에 resources.arsc 파일이 있음을 알 수 있습니다. 이 파일도 aapt에서 생성한 파일입니다. 리소스 ID와 리소스 키 간의 매핑 관계가 파일에 저장됩니다. 리소스는 이 매핑 관계에 따라 리소스를 찾습니다. .

2.resources.arsc:

그림 11은 resources.arsc에 저장된 매핑 관계를 보여주고 있으며, resources.arsc는 리소스 매핑 데이터베이스로 이해할 수 있으며 특정 경로와 이름은 ID에 따라 매핑됩니다.

 

그림 11 resources.arsc 분석

APK를 압축 해제한 후 리소스 파일 이름을 짧은 체인으로 변환합니다(예: res/layout/hello.xml을 r/l/a.xml로 변경한 다음 resources.arsc에 해당하는 값을 변경하여 전체 슬리밍 효과 달성).

AndResGuard[5]는 WeChat에서 출시한 리소스 최적화 도구로, 기본 아이디어는 ProGuard의 난독화와 유사하여 위 솔루션을 실현할 수 있습니다.

3-1-4 7zip 압축

7zip 명령 설명:

-t: 압축 유형을 지정하고 7z, xz, split, zip, gzip, bzip2, tar 등을 지원합니다.

-m: 압축 알고리즘을 지정합니다. 기본값은 Deflate입니다.

구체적인 프로세스는 다음과 같습니다.

1단계: 7z 명령을 사용하여 서명되지 않은 패키지를 지정된 디렉토리에 압축 해제합니다. 7za x ${unsigned package} -o${7z decompressed directory}

2단계: 먼저 7z 명령을 사용하여 압축 해제된 모든 디렉토리를 압축합니다. 7za a -tzip -mx9 ${target 7z file name} ${7z decompressed directory}

3단계: Android SDK의 aapt 명령(aapt l -v ${unsigned package})을 통해 저장소 유형 파일을 가져오고 압축 모드가 Stored인 파일 목록을 가져옵니다.

4단계: 스토리지 유형 파일을 업데이트하고 7z 명령을 사용하여 스토리지 유형 파일을 두 번째 단계에서 생성된 7zip 설치 패키지로 업데이트합니다. 7za a -tzip -mx0 ${target 7z file name} ${storage type file directory }

3-1-5 CPU 아키텍처 구성

다른 CPU 아키텍처에 따라 다른 유형의 설치 패키지를 빌드합니다.현재 주류 장치는 64비트 시스템이므로 Android 시장은 주로 arm64-v8a를 기반으로 컴파일 및 구성된 설치 패키지를 출시합니다.

ndk {
    abiFilters arm64-v8a
}

3-1-6 arcc 압축

resources.arsc는 압축을 위해 높은 볼륨 이득을 갖지만 압축은 시작 속도 및 메모리 메트릭에 영향을 미칩니다. 그 이유는 시스템이 arsc 파일을 로드할 때 arsc 파일이 압축되어 있지 않으면 mmap을 메모리 매핑에 사용할 수 있고 arsc 파일이 압축되어 있으면 압축을 풀고 RAM 버퍼로 읽어야 하므로 메모리가 증가하기 때문입니다. 메모리 사용량 및 시작 속도가 느려집니다.

동일한 고려 사항에 대해 공식은 이 방법을 사용하여 targetSdkVersion>=30 이후에 resources.arsc를 강제할 수 없습니다. 그렇지 않으면 설치가 바로 실패하므로 이 문서에서는 자세히 설명하지 않습니다.

3-1-7 국제화된 언어 처리

JD금융 앱은 현재 국내 시장에서만 운영되고 있지만, 연결되어 있는 다수 의 SDK에 수십 개의 언어가 추가되어 전체 크기가 커졌다 . resConfigs.resConfigs

defaultConfig {
    resConfigs "zh","en"
}

3-1-8 수축 자원

shrinkResources: 컴파일 중 쓸모없는 리소스 파일, 즉 참조되지 않는 리소스를 탐지하고 삭제하는 데 사용됩니다.

minifyEnabled: 참조되지 않는 코드와 같이 쓸모없는 코드를 삭제할 수 있도록 하기 위해 사용되므로 리소스가 참조되는지 여부를 알아야 하는 경우 minifyEnabled와 함께 사용해야 합니다. 코드 및 참조되지 않은 리소스의 목표입니다.

그 기능은 res/raw/를 통해 액세스할 수 있는 작은 형식 파일로 참조되지 않은 리소스 파일을 대체하는 것입니다 (리소스 항목을 유지하면서 여전히 풋프린트가 있으므로 resources.arsc의 볼륨이 줄어들지 않음). .xml 파일은 shrinkMode 및 화이트리스트를 구성합니다.

buildTypes {
   release {
      // 不显示Log
      buildConfigField "boolean", "LOG_DEBUG", "false"
      //混淆
      minifyEnabled true
      // 移除无用的resource文件
      shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig sign.release
   }
}

3-1-9 코딩 제약

• 그림 12와 같이 열거형은 바이트코드로 컴파일된 후 부피가 많이 증가하므로 가능한 한 열거형 유형을 사용하십시오(22줄의 코드가 컴파일된 후의 바이트코드는 86줄임).

그림 12 열거형의 컴파일된 바이트코드 비교

• 불필요한 LOG 출력 제거

3-2 고급 기술 솔루션

SO 라이브러리 동적 다운로드 및 플러그인 기술은 본질적으로 동적 다운로드의 범주입니다.두 가지 솔루션은 비즈니스에서 장기간 지속적으로 사용할 수 있습니다.특정 사용 프로세스에서 선택하는 방법은 그림 13에 나와 있습니다.

그림 13 기업이 고급 솔루션을 선택하는 방법

3-2-1 SO 라이브러리 동적 로딩

APP의 일부 업무는 플러그인 변환에 적합하지 않으며 해체 후 SO 라이브러리가 많은 비율을 차지하므로 변환을 위해 동적 다운로드를 고려하여 크기를 줄일 수 있습니다.

SO 라이브러리 로딩의 두 가지 방법

첫 번째 방법으로 SO 라이브러리를 직접 다운로드하여 지정된 디렉토리에 넣을 수 있습니다.

두 번째 방법은 SO 라이브러리를 환경 변수로 설정한 디렉토리에 로드하는 것이므로 SO 라이브러리를 정상적으로 로드하려면 지정된 디렉토리를 환경 변수에 추가해야 합니다.

System.load("{安全路径}/libxxx.so") 
System.load("xxx") 

1. 앱에서 SO 라이브러리의 환경 변수 위치를 설정하는 방법(Tinker 참조):

final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
final Object dexPathList = pathListField.get(classLoader);

final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");

List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
if (origLibDirs == null) {
    origLibDirs = new ArrayList<>(2);
}
final Iterator<File> libDirIt = origLibDirs.iterator();
while (libDirIt.hasNext()) {
    final File libDir = libDirIt.next();
    if (folder.equals(libDir)) {
        libDirIt.remove();
        break;
    }
}
origLibDirs.add(0, folder);

final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
if (origSystemLibDirs == null) {
    origSystemLibDirs = new ArrayList<>(2);
}

final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
newLibDirs.addAll(origLibDirs);
newLibDirs.addAll(origSystemLibDirs);

final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);

final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);

final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
nativeLibraryPathElements.set(dexPathList, elements);

2. 그림 14와 같이 지정된 SO 라이브러리 및 전체 로드 프로세스를 삭제하는 방법:

그림 14 SO 라이브러리 삭제 및 로드 프로세스

3-2-2 플러그인

플러그인이란:

플러그인은 업무 기능에 따라 Apk를 서로 다른 하위 Apk(즉, 서로 다른 플러그인)로 분할하는 것으로 각 하위 Apk는 독립적으로 컴파일 및 패키징할 수 있으며 통합된 Apk가 최종적으로 릴리스 및 실행됩니다. Apk를 사용하면 각 플러그인이 동적으로 로드되며 플러그인도 핫픽스 및 핫업데이트할 수 있습니다.

• 호스트: 기본 앱을 사용하여 플러그인을 로드하고 호스트가 될 수도 있습니다.

• Plug-in: Plug-in App은 호스트가 불러온 App으로 일반 App과 동일한 Apk 파일이 될 수 있음

플러그인 변환에 적합한 비즈니스 형태:

• 비즈니스는 상대적으로 독립적이며 호스트 앱과 완전히 분리되어 있습니다.

• 낮은 리노베이션 비용과 상대적으로 높은 수입

• 많은 양을 차지함

일련의 평가 후 비디오 비즈니스는 위의 사항을 충족하며 변환 후 효과는 그림 15에 나와 있습니다.

 

그림 15 비디오 비즈니스 홀의 플러그인 변환 효과

3-3 사업 최적화 방안

비즈니스가 점점 더 많아지면서 일부 오래된 비즈니스 UV가 점점 낮아지고 있으므로 그림 16과 같이 일련의 비즈니스 오프라인 최적화 프로세스가 공식화되었습니다.

그림 16 비즈니스 최적화 솔루션 프로세스

4. 제어

슬리밍 계획의 실행은 매우 중요하며 후속 제어가 반등하지 않는 것이 더 중요합니다.슬리밍 거버넌스를 수행하는 동안 우리는 정상화 된 제어 메커니즘을 탐색하고 마침내 일련의 제어 규범을 촉진했습니다. 및 제어 메커니즘. 관리 및 제어의 목적은 비즈니스 반복이나 새로운 코드를 제한하는 것이 아니라 제한된 코드에서 기능을 구현하고 일상적인 코딩에서 엔지니어의 슬리밍에 대한 인식을 향상시키는 것입니다.

4.1 SDK 액세스 사양

SDK의 무질서한 확장을 방지하기 위해 SDK 액세스 사양을 공식화하고 기능을 보장한다는 전제하에 SDK의 크기를 엄격히 통제하고 APP 볼륨의 리바운드를 최대한 통제합니다.

4.2 제어 프로세스

 

그림 17 제어 프로세스

콘텐츠 추가, 콘텐츠 삭제, 콘텐츠 증가, 콘텐츠 축소, 중복 파일, 코드 관리 등 리소스 파일의 변경 사항에 따라 거버넌스 제어 사양 등과 결합하여 패키징 및 구성을 과거 버전과 비교합니다. 변경된 내용을 얻기 위해 최적화의 여지가 있는지 평가하고 최적화 목표를 부여하고 최적화 후 패키징 통합을 다시 빌드합니다.

5. 성과 및 향후 계획

5.1 성과

위의 조치를 통해 JD Finance의 Android 버전은 117M에서 현재 74M까지 2분기 동안 5개 버전으로 반복되었으며(그림 18) 전체적으로 제어 가능한 범위 내에서 유지되었습니다. 동시에 다음 버전에서는 APK 슬리밍을 정상화하고 패키지 크기를 항상 제어 가능한 범위 내에서 유지할 것입니다.

 

그림 18 금융 APP 슬림화 결과

5.2 후속 계획

기술적 수단의 지속적인 최적화:

비즈니스의 지속적인 축적과 반복은 항상 쓸모없는 리소스를 생성하므로 이러한 쓸모없는 파일과 코드를 정기적으로 정리하여 설치 패키지를 줄여야 합니다.

각 버전을 잘 모니터링하고 버전 간의 차이점을 비교하고 비즈니스에 영향을 주지 않고 기술적 수단을 사용하여 최적화할 수 있는지 확인합니다.

온라인 관리 및 제어 플랫폼 구축:

초기에는 오프라인 관리 및 제어를 사용하여 구현하는 데 약간의 시간이 소요되었지만 향후 온라인 관리 및 제어 플랫폼의 구축을 개선하고 전체 App 출시 및 구축 플랫폼과 통합하여 조립 라인 메커니즘을 형성하고 관리 및 제어를 잘 수행합니다.

요약 : 설치 패키지의 슬리밍을 탐색하려면 아직 갈 길이 멉니다.이 기사에서는 일반적으로 사용되는 일부 슬리밍 솔루션만 나열합니다.대형 프로젝트에 대한 최적화 외에도 프로젝트 간 거버넌스를 잘 수행해야 합니다. 사용자 경험을 개선하기 위해 앱의 크기를 계속 최적화합니다.

【참조】

[1] 패키지 크기 및 설치 전환율
https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2

[2] 프로가드  https://www.guardsquare.com/proguard

[3] R8 https://r8.googlesource.com/r8

[4] ProGuard와 R8 비교
https://www.guardsquare.com/blog/proguard-and-r8

[5] AndResGuard https://github.com/shwenzhang/AndResGuard

[6] AGP https://developer.android.com/studio/releases/gradle-plugin

[7] Pandora: 분산형 기술을 기반으로 R&D 및 테스트 단계에서 에너지 효율성을 개선하기 위한 도구

{{o.이름}}
{{이름}}

рекомендация

отmy.oschina.net/u/4090830/blog/8590988