이 글은 공식문서와 Chat GPT를 사용한 결과 입니다. 틀린 내용있으면 댓글 달아주세요.
이 글은 R8이 프로젝트에서 컴파일 시간 작업을 하는 방법과 작업을 맞춤설정하는 방법을 설명한다.
컴파일 시간 작업
- 코드 축소 (Tree Shaking)
- 앱 및 라이브러리 종속 항목에서 미사용 클래스, 필드, 메서드, 속성을 감지하여 안전하게 삭제한다.
- 예를 들어 라이브러리 종속항목에서 몇개의 API만 사용한다면 축소는 앱이 사용하지않는 라이브러리 코드를 식별하고 앱에서 그 코드만 삭제할수있다.
- 리소스 축소
- 앱 라이브러리 종속 항목의 미사용 리소스를 포함하여 패키징된 앱에서 사용하지 않는 리소스를 삭제한다.
- 더 이상 참조되지 않는 리소스도 안전하게 삭제할 수 있다.
- 난독화
- 클래스와 멤버 이름을 줄여 DEX 파일 크기를 줄인다.
- 최적화
- 코드를 검사하고 다시 작성하여 앱 DEX 파일의 크기를 더 줄입니다.
- 예를 들어 주어진 if/else 구문의 else{} 분기가 전혀 사용되지 않음을 R8에서 감지하면 R8이 else{} 분기 코드를 삭제한다.
- 특징
- 앱의 출시버전을 빌드할 때 기본적으로 R8은 위에서 설명한 컴파일 시간 작업을 자동으로 진행한다.
- ProGuard 규칙 파일을 사용하여 특정작업을 중지하거나 R8의 맞춤설정할 수 있습니다.
- R8은 기존의 모든 ProGuard 규칙 파일과 호환되므로 R8을 사용하도록 Android Gradle 플러그인을 업데이트하면 기존 규칙을 변경할 필요가 없다.
설정
android {
buildTypes {
getByName("release") {
// 프로젝트의 릴리스 빌드 유형에 대해서만 코드 축소, 난독화 및 최적화를 활성화합니다.
isMinifyEnabled = true
// Android Gradle 플러그인에서 수행되는 리소스 축소를 활성화합니다.
isShrinkResources = true
// Android Gradle 플러그인과 함께 패키지된 기본 ProGuard 규칙 파일을 포함합니다.
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
...
}
- R8 사용하기 위한 설정
- R8은 프로젝트의 자바 바이트 코드를 Android 플랫폼에서 실행되는 DEX 형식으로 변환하는 기본 컴파일러이다.
- R8은 기본적으로 사용설정되지 않는다.
- 개발자가 유지할 코드를 적절하게 맞춤설정하지 않았을 때 버그가 발생할 수 있다.
- 따라서 위의 설정을 해주어야한다.
R8 구성 파일
- R8은 ProGuard 규칙 파일을 사용하여 기본 동작을 수정하고 앱 코드의 진입점역할을 하는 클래스와 앱 구조를 더 잘 이해합니다.
- 규칙의 일부를 수정할 수 있지만 일부규칙은 AAPT2와 같은 컴파일 시간 도구에서 자동으로 생성되거나 앱 라이브러리 종속항목에서 상속될 수 있습니다.
- 다음 내용은 R8이 사용하는 ProGuard 규칙 파일의 소스를 설명한다.
@Keep
public void foo() {
...
}
- proguard-rules.pro
- 소스 : Android Studio
- 위치 : <module-dir>/proguard-rules.pro
- 설명
- Android Studio에서 새 모듈을 만들때 IDE는 새 모듈의 루트 디렉터리에 proguard-rules.pro 파일을 만든다.
- 기본적으로 이 파일은 규칙을 적용하지 않는다.
- 이 글의 유지할 코드 맞춤설정과 같은 직접만든 ProGuard 규칙을 여기에 포함하자.
- proguard-android-optimize.txt
- 소스 : Android Gradle Plugin
- 위치 : 컴파일 시간에 Android Gradle Plugin에서 생성된다.
- 설명
- Android Gradle Plugin은 proguard-android-optimize.txt를 생성하는데
- 이 파일은 대부분의 Android 프로젝트에 유용한 규칙을 포함하고 @keep* 주석을 사용한다.
- @keep* 주석
- 이 주석은 코드 축소 시 특정요소(메서드나 클래스)를 제거하지 않도록 지시하는 데 사용된다.
- 이는 주로 reflection을 통해서만 접근되는 메소드나 클래스에 사용된다.
- reflection : 런타임에 객체의 클래스 정보를 조사하고 수정할 수 있는 기능을 말한다.
- 컴파일러는 이러한 리플렉션을 통해 접근되는 코드를 사용되지 앟는 코드로 오인할 수 있기 때문에 코드가 축소되는 과정에서 제거하지 않도록 만들어줌
- @keep* 주석
- 기본적으로 Android Studio를 사용하여 새 모듈을 만들 때 모듈 수준 build.gradle 파일은 출시 빌드에 이 규칙 파일을 포함한다.
- AAR 라이브러리
- 소스 : 라이브러리 종속 항목
- 위치
- AAR library : <library-dir>/progruard.txt
- JAR library : <library-dir>/META-INF/proguard/
- 설명
- AAR library는 'proguard.txt'라는 자체 ProGuard 규칙 파일을 포함할 수 있습니다.
- AAR library
- 안드로이드 스튜디오에서 사용하는 라이브러리 포맷으로 자바 클래스, 안드로이드 리소스, 매니페스트 파일등을 포함할 수 있다.
- 프로젝트에 AAR library를 포함하는 것은 그 라이브러이가 제공하는 코드나 리소스를 사용하기 위해 필요하다.
- AAR library
- AAR library를 프로젝트에 컴파일 시간 종속 항목으로 포함할 경우 R8은 라이브러리와 함께 제공된 ProGuard 규칙을 자동으로 적용한다.
- 종속 항목 : 프로그램이 올바르게 작동하기 위해 필요한 외부 라이브러리나 모듈
- AAR library에 포함된 ProGuard 규칙은 삭제할 수 없으며, 앱의 다른 부분에도 영향을 미칠 수 있다.
- 예를 들어 라이브러이에 코드 최적화를 중지하는 규칙이 포함되어있으면 이 규칙은 전체 프로젝트의 코드 최적화에 영향을 줄 수 있다.
- AAR library는 'proguard.txt'라는 자체 ProGuard 규칙 파일을 포함할 수 있습니다.
- AAPT2
- 소스 : Android Asset Package Tool 2 (AAPT2)
- 위치 : minifyEnabled true: <module-dir>/build/intermediates/proguard-rules/debug/aapt_rules.txt
- 설명
- AAPT2
- 안드로이드 앱의 리소스(레이아웃 파일, 이미지 등)를 컴파일하고 패키징하는 데 사용됩니다.
- 앱의 리소스를 더 효율적으로 처리하여 빌드시간을 단축하고 앱의 성능을 향상시키는 역할을 한다.
- keep 규칙
- AAPT2는 빌드 과정에서 앱의 매니페스트. 레이아웃 및 기타 리소스에서 참조하는 클래스에 대한 keep 규칙을 자동으로 생성한다.
- 예를 들어, 앱 메니페스트 파일에서 선언된 각 Activity에 대해 AAPT2는 해당 클래스를 난독화하는 과정에서 제외하는 keep 규칙을 포함한다. 이는 앱의 정상적인 작동을 보장하기 위해 필요
- 안드로이드 시스템이 액티비티 같은 것을 정확하게 식별하고 엑세스할 수 있어야하기 때문이다.
- 난독화 과정에서 클래스 이름이 변경되면 시스템이 해당 액티비티를 정확하게 찾을 수 없다.
- 액티비티의 이름은 난독화 되지 않지만 액티비티의 내부 구현은 난독화될 수 있다.
- AAPT2
- 맞춤구성파일
- 소스 : 맞춤구성파일
- 위치 : 기본적으로 Android Studio에서 새 모듈을 만들 때 IDE는 자체 규칙을 추가할 수 있도록 <module-dir>/proguard-rules.pro를 만든다.
- 설명
- 이 파일은 개발자가 앱에 대한 추가적인 ProGuard 또는 R8 최적화 규칙을 정의할 수 있도록 한다.
- 앱의 최종바이너리에 영향을 미친다.
// minify
// You can specify any path and filename.
-printconfiguration ~/tmp/full-r8-config.txt
추가 구성 포함
- 기본 규칙 파일 생성
- Android Studio는 새로운 프로젝트나 모듈을 만들 때 기본적으로
- <module-dir>/proguard-rules.pro 파일을 생성한다.
- 이 파일은 개발자가 앱에 대한 추가적인 ProGuard/R8 최적화 규칙을 정의할 수 있도록 한다.
- build.gradle
- build.gradle 파일에서 proguardFiles 속성을 이용하여 추가 ProGuard/R8 규칙 파일을 지정할 수 이싿.
- 이를 통해 기본 규칙외에 추가적인 규칙을 적용할 수 있다.
- 빌드 변형 및 productFlavor 설정
- productFlavor
- Android Studio에서는 여러 poductFlavor를 정의하여 다양한 빌드 변형을 관리할 수 있다.
- 이를 통해 각각의 빌드 변형에 대해 다른 설정을 적용할 수 있다.
- flavor별 proguard 규칙
- 예를 들어, 특정 플레이버에(예: flavor2)에 대해 특별한 ProGuard/R8 규칙을 적용하고 싶다면, 해당 플레이버에 proguardFiles 속성에 해당 규칙 파일을 추가할 수 있다.
- productFlavor
android {
...
// 앱의 빌드 유형을 정의
// release 빌드 유형에 대한 설정이 포함됨
buildTypes {
getByName("release") {
isMinifyEnabled = true
//프로가드에 사용할 규칙 파일을 지정한다.
proguardFiles(
//안드로이드 기본 프로가드 설정을 불러온다.
getDefaultProguardFile("proguard-android-optimize.txt"),
// 개별 모듈 레벨에서 정의된 추가 프로가드 규칙을 포함한다.
//안드로이드 스튜디오는 각 모듈의 루트 디렉터리에 기본적으로 빈 규칙파일을 생성한다.
//여기에 사용자가 정의한 추가 규칙을 추가할 수 있다.
"proguard-rules.pro"
)
}
}
//productFlavor의 차원을 정의
//version이라는 차원이 추가됨
flavorDimensions.add("version")
productFlavors {
//flavor1, flavor2라는 두가지 플레이버를 정의했다.
create("flavor1") {
...
}
create("flavor2") {
//flavor2-rules.pro에는 flavor2 플레이버에 특화된 규칙이 포함될 수 있다.
proguardFile("flavor2-rules.pro")
}
}
}
- productFlavor
- 앱의 다양한 변형을 정의하는 방법
- 이를 통해 하나의 코드베이스에서 여러 버전의 앱을 생성할 수 있다.
- 예를 들어 무료버전과 유료버전의 앱, 다양한 시장을 대상으로 하는 앱등을 하나의 프로젝트에서 관리할 수 있다.
- 각 플레이버는 다음과 같은 측면에서 차이를 가진다.
- 리소스(이미지, 문자열)
- 소스 코드(특정기능 활성화/비활성화)
- 의존성(특정라이브러리만 포함)
- 앱 ID, 버전 정보등
- Flavor Dimensions(플레이버 차원)
- 플레이버 차원은 서로 다른 종류의 플레이버 그룹을 구분하는데 사용된다.
- 플레이버 차원을 사용하면 각 차원에 속한 플레이버를 결합하여 다양한 빌드 변형을 생성할 수 있다.
- 예를 들어 하나의 차원이 버전이라면(예: 유료,무료), 다른 차원은 '시장'일수 있습니다.
코드 축소
- minifyEnabled 속성을 true로 설정하면 기본적으로 R8에서 코드 축소가 사용설정 됩니다.
- 코드 축소는 런타임에 필요하지 않다고 R8이 판단한 코드를 삭제하는 프로세스입니다.
- 앱 코드를 축소하기 위해 R8은 먼저 결합된 구성 파일 조합(위의 R8 구성 파일 참조)을 기반으로 앱 코드의 모든 진입점을 결정한다.
- 이 진입점에는 Android 플랫폼이 앱의 액티비티 또는 서비스를 여는 데 사용할 수 있는 모든 클래스가 포함된다.
- R8은 각 진입점에서 시작하여 앱의 코드를 검사해 앱이 런타임에 엑세스할 수 있는 모든 메서드, 멤버 변수, 기타 클래스의 그래프를 작성합니다.
- 그래프에 연결되지 않은 코드는 연결할 수 없는 것으로 간주되면 앱에서 삭제된다.
- R8 프로젝트의 R8 구성 파일의 -keep 규칙을 이용하여 진입점을 결정한다.
- keep 규칙은 앱을 축소할 때 R8이 삭제하면 안되는 클래스를 지정하고 R8은 이 클래스를 앱의 진입점으로 사용할 수 있다고 간주합니다.
- Android Gradle plugin과 AAPT2는 앱의 액티비티, 뷰 및 서비스와 같이 대부분의 앱 프로젝트에서 필요한 keep 규칙을 자동으로 생성합니다.
- 그러나 추가적인 keep 규칙을 사용하여 이 기본 동작을 맞춤 설정해야하는 경우 유지할 코드를 맞춤설정하는 방법에 관한 섹션을 참조하세요.
유지할 코드를 맞춤설정
- 대부분의 상황에서는 기본 ProGuard 규칙 파일(proguard-android-optimze.txt)만 있으면 R8을 이용하여 미사용 코드르 삭제할 수 있습니다.
- 그러나 R8에서 정확하게 분석하기 어려운 상황도 있으며 실제로 앱에서 사용하는 코드를 삭제하는 경우도 발생할 수 있습니다.
- 코드를 잘못 삭제할 수 잇는 몇가지 예이다.
- 앱이 자바 네이티브 인터페이스(JNI)에서 메서드를 호출하는 경우
- 앱이 런타임에 리플랙션등을 사용하여 코드를 찾는 경우
- 앱을 테스트하면 잘못된 코드 삭제로 인한 오류가 나타나지만 삭제된 코드 보고서를 생성하여 삭제된 코드를 검사할수도 있습니다.
- 오류를 수정하고 R8이 특정 코드를 유지하도록 하려면 ProGuard 규칙 파일에 -keep 줄을 추가한다. 밑의 코드 참조
- 또는 유지하려는 코드에 @Keep 주석을 추가할 수 있다.
- @Keep 클래스에 추가하면 전체 클래스가 그대로 유지된다.
- 이 주석을 메서드나 필드에 추가하면 메서드/필드 및 그 이름뿐만 아니라 클래스 이름도 그대로 유지됩니다.
- 참고로 이 주석은 축소 사용방법에서 설명한 대로 AndroidX 주석 라이브러리를 사용항고 Android Gradle Plugin과 함께 패키징된 ProGuard 규칙파일을 포함할 때만 사용가능하다.
- 규칙 파일을 맞춤 설명하는 방법에 대한 정보는 ProGuard 설명서 참고(https://www.guardsquare.com/en/products/proguard/manual/usage)
-keep public class MyClass
삭제된 코드 보고서 생성 방법
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
// 삭제된 코드 보고서 활성화
buildConfigField "boolean", "GENERATE_UNUSED_CODE_REPORT", "true"
}
}
}
- R8이 코드 최적화 및 난독화 과정에서 잘못 삭제한 코드를 추적하려면 삭제된 코드 보고서를 생성할 수 있습니다.
- 어떤 코드가 최적화 중에 삭제 되었는지 확인할 수 있습니다.
- R8은 빌드시 삭제된 코드에 대한 보고서를 자동으로 생성할 수 있습니다.
- 이를 위해 build.gradle 파일에서 minifyEnabled가 true로 설정되어 있어야 하며, 추가적으로 shrinkResource 옵션을 활성화 하고 -printconfiguration 플래그를 사용해야 합니다.
- 위의 설정을 통해 삭제된 코드에 대한 보고서를 생성할 수 있습니다.
- 그런 다음 R8은 로그에서 -unused 플래그를 사용하여 삭제된 코드를 보고할 수 있습니다.
- 플래그
- 삭제된 코드를 보고하는 방식과 관련이 있음
- -printConfiguration
- R8의 작업이 끝난 후, 최적화된 코드와 난독화 정보에 대한 상세 보고서를 생성합니다.
- 이 보고서는 최적화 및 난독화 처리 중 어떤 코드가 삭제 되었는지, 사용되지 않은 코드는 무엇인지 등을 보여줍니다.
- 플래그
- 빌드를 완료한 후
네이티브 라이브러리 제거
- 개요
- 네이티브 코드 라이브러리는 앱의 출시 빌드에서 제거됩니다.
- 네이티브 코드 라이브러리
- 그래픽 처리, 오디오 처리, 물리 계산 등 특정한 기능을 구현하는데 사용된다.
- 네이티브 코드 라이브러리
- 제거 되는 항목은 앱에서 사용하는 모든 네이티브 라이브러리에 포함된 기호표와 디버깅 정보이다.
- 네이티브 코드 라이브러리를 제거하여 크기를 크게 줄일수 있지만 누락된 정보 (예: 클래스 이름 및 함수 이름)로 인해 Google Play Console에서 비정상종료를 진단할 수 없습니다.
- 네이티브 코드 라이브러리는 앱의 출시 빌드에서 제거됩니다.
- 네이티브 충돌 지원
- 개요
- Google Play Console은 Android vitals에서 네이티브 충돌을 보고한다.
- 몇 단계만 거치면 앱의 네이티브 디버그 기호 파일을 생성하고 업로드할 수 있다.
- 이 파일로 Android vitals에서 기호화된 네이티브 비정상 종료 스택 트레이스(클래스 및 함수 이름 포함)를 사용 설정하여 프로덕션에서 앱을 디버그할 수 있습니다.
- 이러한 단계는 프로젝트에서 사용하는 Android Gradle 플러그인의 버전과 프로젝트의 빌드 출력에 따라 다르다.
- Android Gradle Plugin 버전 4.1 이상
- 프로젝트가 Android App Bundle을 빌드하는 경우 네이티브 디버그 기호 파일을 자동으로 포함할 수 있습니다.
- 이 파일을 출시 빌드에 포함하려면 앱의 build.gradle 파일에 다음을 추가합니다.
- android.buildTypes.release.ndk.debugSymbolLevel = {SYMBOL_TABLE | FULL}
- 다음에서 디버그 기호 수준을 선택합니다.
- SYMBOL_TABLE을 사용하여 Play Console의 기호화된 스택 트레이스에서 함수 이름을 가져옵니다. 이 수준은 TombStone(https://source.android.com/devices/tech/debug?hl=ko)을 지원합니다.
- FULL을 사용하여 Play Console의 기호화된 스택 트레이스에서 함수 이름, 파일, 행 번호를 가져옵니다.
- 프로젝트가 APK를 빌드하는 경우 이저에 보여준 build.gradle 빌드 설정을 사용하여 네이티브 디버그 기호 파일을 별도로 생성한다.
- Google Play Console에 수동으로 네이티브 디버그 기호 파일을 업로드 한다.(https://support.google.com/googleplay/android-developer/answer/9848633?hl=ko#upload_file)
- 빌드 프로세스의 일부로 Android Gradle 플러그 인은 다음 프로젝트 위치에 이 파일을 출력한다.
- app/build/outputs/native-debug-symbols/variant-name/native-debug-symbols.zip
- Android Gradle Plugin 버전 4.0이하(및 기타 빌드 시스템)
- 빌드 프로세스의 일부로 Android Gradle 플러그인은 제거되지 않은 라이브러리의 사본을 프로젝트 디렉터리에 유지한다.
- 아래에 디렉터리 구조를 첨부한다.
- 방법
- 다음 디렉터리의 콘텐츠를 압축(아래 코드 참조)
- Google Play Console에 수동으로 symbol.zip(https://support.google.com/googleplay/android-developer/answer/9848633?hl=ko#upload_file) 파일을 업로드 한다.
- 주의
- 디버그 기호 파일은 300MB로 제한합니다.
- 파일이 너무 큰 경우 .so 파일에 기호 테이블(함수이름) 및 DWARF 디버깅 정보(파일 이름 및 코드 줄)가 포함되어 있기 때문일 수 있다.
- 이러한 테이블 및 정보는 코드를 기호화하는 데 필요하지 않다.
- 아래의 명령어를 사용하여 삭제할 수 있다.
- 개요
//디렉터리 콘텐츠를 압축하기 위한 명령어
cd app/build/intermediates/cmake/universal/release/obj
zip -r symbols.zip .
//디렉토리 구조
app/build/intermediates/cmake/universal/release/obj/
├── armeabi-v7a/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
├── arm64-v8a/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
├── x86/
│ ├── libgameengine.so
│ ├── libothercode.so
│ └── libvideocodec.so
└── x86_64/
├── libgameengine.so
├── libothercode.so
└── libvideocodec.so
//DWARF 디버깅 정보 삭제 명령어
$OBJCOPY --strip-debug lib.so lib.so.sym
//여기서 $OBJCOPY는 제거할 ABI의 특정버전을 가리킨다.
//예: nbk-bundle/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-objcopy
리소스 축소
android {
...
buildTypes {
getByName("release") {
isShrinkResources = true
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android.txt"),
"proguard-rules.pro"
)
}
}
}
- 리소스 축소는 코드 축소와 함께 사용할 때만 작동한다.
- 코드 축소기가 사용하지 않는 코드를 모두 삭제하면 리소스 축소기에서 아직 앱에 사용되는 리소스를 식별할 수 있습니다.
- 이는 리소스를 포함하는 코드 라이브러리를 추가하는 경우에 특히 그렇습니다.
- 사용하지 않는 라이브러리 코드는 삭제해야 라이브러리 리소스가 참조되지 않으며 리소스 축소기가 삭제할 수 있다.
- 리소스 축소를 사용하려면 build.gradle 파일에서 shrinkResource 속성을 true로 설정합니다.
- 코드 축소의 경우 minifyEnabled도 설정
- 코드 축소의 경우 minifyEnabled를 사용하여 앱을 아직 빌드하지 않았다면 shrinkResource를 사용하기 전에 먼저 빌드한다.
- 리소스 제거를 시작하기 전에 동적으로 생성되거나 호출되는 클래스나 메서드를 유지하려면 proguard-rules.pro 파일을 수정할 필요가 있습니다.
유지할 리소스 맞춤 설정
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
tools:discard="@layout/unused2" />
- 특정 리소스를 유지하거나 삭제하려는 경우, <resources> 태그로 프로젝트에서 XML 파일을 생성하고 tools:keep 속성에서 유지할 각 리소스를 지정하고 tools:discard 속성에서 삭제할 각 리소스를 지정합니다.
- 두 속성은 모두 쉼표로 구분된 리소스 이름 목록을 허용합니다.
- *를 와일드 카드로 사용할 수 있다.
- 이 파일을 프로젝트 리소스에 저장한다.
- 예: res/raw/keep.xml 에 저장
- 빌드는 앱에 이 파일을 패키징 하지 않는다.
- 리소스를 삭제할 수 있는데도 삭제할 리소스를 지정하는 것이 이상해 보일 수 있지만 빌드 변형을 사용할 때는 이 방법이 유용할 수 있습니다.
- 예를 들어 어떤 리소스가 코드에서 사용되는 것 처럼 보이지만(이에 따라 축소기에서 삭제하지 않음) 실제로는 주어진 빌드 변형(예: debug, release)에서 사용하지 않는다는 것을 알고 있다면 모든 리소스를 공용 프로젝트 디렉터리에 넣고 각 빌드 변형을 위해 다른 keep.xml 파일을 생성할 수 있다.
- 또한 컴파일러가 리소스 ID를 코드에 직접 삽입하는 '인라인'처리를 할 때 리소스 분석기가 이러한 인라인 처리된 정수값을 실제 리소스 참조와 구별하지 못한다. 이로 인해 빌드 도구가 리소스를 잘못 식별하고 필요한 리소스를 삭제할 위험이 있다.
- 컴파일러가 리소스 ID를 인라인 처리하면, 원래의 R.id 형식의 참조가 코드 내에서 정수 값으로 바뀐다.
- 이 때문에 리소스 분석기가 해당 정수 값을 리소스와 관련된 참조로 정확히 인식을 못한다.
- 리소스 분석기가 이러한 인라인 처리된 정숫값을 정확히 리소스 참조로 인식하지 못하면, 실제로 사용되고 있는 리소스임에도 불구하고 불필요한 리소스로 오인하여 제거할 수 있다.
엄격한 참조 확인
- 일반적으로 리소스 축소기는 리소스의 사용 여부를 정확하게 판별할 수 있다.
- 그러나 코드가 Resources.getIdentifier()를 호출하거나 임의 라이브러리가 호출을 실행하는 경우에(예: AppCompat 라이브러리가 호출을 실행), 이코드는 동적으로 생성된 문자열을 기반으로 리소스 이름을 찾는다.
- 이렇게 하면 리소스 축소기는 기본적으로 방어적인 동작을 하며 매칭 이름 형식을 가진 모든 리소스를 잠재적으로 사용중이며 삭제할 수 없는 리소스로 표시한다.
val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)
- 예를 들어, 다음 코드는 img_ 접두사가 있는 모든 리소스를 사용되는 리소스로 표시합니다.
- 리소스 축소기는 또한 다양한 res/raw/ 리소스와 코드에서 모든 문자열 상수를 찾고 file:///android_res/drawble//ic_plus_anim016.png와 유사한 형식의 리소스 URL을 찾습니다.
- 이러한 URL이 코드에서 참조되는 경우, 해당 리소스는 삭제되지 않습니다.
- 앱의 리소스를 참조하는 문자열또는 리소스 URL을 구성하는데 사용될 수 있는 문자열이 발견되면 해당 리소스는 보존된다.
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict" />
엄격한 축소 모드
- 안전모드는 잠재적으로 사용되는 리소스를 보수적으로 보존한다.
- 더 엄격한 리소스 관리를 원하는 경우 엄격한 축소모드(strict mode)를 사용할 수 있습니다.
- keep.xml 파일에서 shrinkMode를 strict로 설정함으로써, 리소스 축소가 확실하게 사용되는 리소스만을 보존하도록 지정할 수 있다.
<resources xmlns:tools="http://schemas.android.com/tools">
<tools:keep>
<!-- 여기에 유지하고자 하는 리소스의 이름을 명시합니다. -->
@drawable/dynamically_referenced_image
</tools:keep>
</resources>
- tools:keep 속성을 keep.xml 파일에 사용하여 동적으로 참조되는 리소스를 축소 과정에서 제외시킬 수 있다.
사용하지 않는 대체 리소스 삭제
android {
defaultConfig {
...
resourceConfigurations.addAll(listOf("en", "fr"))
}
}
- Gradle 리소스 축소기는 앱 코드에서 참조하지 않는 리소스만 삭제하며 다른 기기 설정을 위한 대체 리소스는 삭제하지 않는다.
- 대체리소스
- 다양한 기기 설정과 환경에 맞게 앱이 적절히 반응하도록 설계된 특정 리소스
- 다양한 화면 크기, 해상도, 언어 설정
- 대체리소스
- 필요한 경우 Android Gradle Plugin의 resConfigs 속성을 사용하여 앱에 불필요한 대체 리소스 파일을 제거할 수 있다.
- 예를 들어 언어 리소스가 포함된 라이브러리 (예: AppCompat 또는 Google Play 서비스)를 사용 중인 경우 앱은 앱의 나머지 부분이 동일 언어로 번역되었는지와 상관 없이 라이브러리의 메시지를 위해 번역된 모든 언어 문자열을 포함합니다.
- 앱에서 공식적으로 지원하는 언어만 유지하려면 resConfig 속성을 사용하여 언어를 지정할 수 있습니다.
- 지정되지 않은 언어의 리소스는 모두 삭제됩니다.
- Android App Bundle 형식을 사용하여 앱을 출시하는 경우 기본적으로 앱을 설치할 때 사용자 기기에서 구성된 언어만 다운로드 됩니다.
- 마찬가지로 기기의 화면 밀도와 일치하는 리소스 및 기기의 ABI와 일치하는 네이티브 라이브러리만 다운로드에 포함된다.
- APK로 출시되는 기종 앱의 경우 각각 다른 기기 설정을 타겟팅하는 여러 APK를 빌드하여 APK에 포함할 화면 밀도 또는 ABI 리소스를 맞춤설정할 수 있습니다.
중복 리소스 병합
- 기본적으로 Gradle은 이름이 동일한 리소스를 병합한다.
- 예: 동일한 이름의 드로어블이 다른 리소스 폴더에 있는 경우
- 코드가 찾고 있는 이름이 여러 리소스와 일치할 때 발생하는 오류를 피해야 하므로 이 동작은 shrinkResources 속성에 관계없이 항상 수행되며 사용자가 이 동작을 중지할 수 는 없습니다.
- 리소스 병합은 둘 이상의 파일이 동일한 리소스 이름, 유형 및 한정자를 공유하는 경우에만 발생한다.
- Gradle은 중복된 파일 중에서 가장 적합하다고 여겨지는 파일을 아래 설명한 우선순위에 따라 선택한 후 최종 아티팩트에 배포하기 위해 하나의 리소스만 AAPT에 전달한다.
- Artifact
- 아티팩트는 소프트웨어 개발 및 빌드 과정에서 생성되는 결과물을 의미
- 소스코드, 컴파일된 코드, 라이블러리, 실행가능파일, 패키징된 애플리케이션등 다양한 형태를 가질 수 있다.
- 안드로이드 앱 개발에서 가장 일반적인 예는 APK 파일이다.
- AAPT
- 안드로이드 앱 개발 과정에서 리소스를 관리하고 앱의 리소스와 소스코드를 APK 파일로 패키징하는데 사용되는 도구이다.
- Artifact
- 중복리소스 병합 과정
- 리소스 위치
- 중복 리소스는 다음 위치에서 찾을 수 있다.
- 기본 소스 세트와 연관된 리소스(일반적으로 src/main/res/ 에 위치)
- 빌드 유형 및 빌드 버전에서 파생된 변형 오버레이
- 빌드 유형(예: debug, release) 및 제품 플레이버(예: free, paid)에 따라 다른 리소스를 정의할 수 있습니다.
- 이러한 변형 오버레이는 특정 빌드 유형이나 플레이버에 맞춰진 리소스를 포함할 수 있고 기본 리소스를 오버라이드 할 수 있다.
- 라이브러리 프로젝트 종속 항목
- 라이브러리 프로젝트는 자체 리소스를 포함할 수 있고 이 것들도 최종 앱 아티팩트에 포함된다.
- 중복 리소스는 다음 위치에서 찾을 수 있다.
- 우선 순위 단계
- 종속 항목
- 프로젝트가 의존하는 외부 라이브러리나 모듈을 의미한다.
- 이들 종속 항목에 포함된 리소스가 중복된 경우, 우선 순위가 가장 낮다.
- 기본
- 프로젝트의 주 리소스 세트
- src/main/res 디렉토리에 위치한 리소스이다.
- 빌드 버전
- 특정 빌드 버전 또는 제품 플레이버에 해당하는 리소스를 말한다.
- 빌드 유형
- 빌드 유형
- debug나 release 같은 빌드 설정을 의미한다.
- 가장 높은 우선순위를 가진다.
- 빌드 유형
- 종속 항목
- 리소스 병합 오류
- 동일 소스 세트 내 중복
- 동일한 소스 세트 내에서 동일한 리소스가 중복되면 Gradle은 이를 병합할 수 없으며 리소스 병합 오류가 발생
- 오류 발생 조건: build.gradle 파일의 sourceSets 속성에 여러개의 소스 세트가 정의되어 있고, 같은 이름의 리소스가 여러 세트에 포함된 경우에 이 오류를 발생할 수 있습니다.
- 동일 소스 세트 내 중복
- 리소스 위치
코드 난독화
androidx.appcompat.app.ActionBarDrawerToggle$DelegateProvider -> a.a.a.b:
androidx.appcompat.app.AlertController -> androidx.appcompat.app.AlertController:
android.content.Context mContext -> a
int mListItemLayout -> O
int mViewSpacingRight -> l
android.widget.Button mButtonNeutral -> w
int mMultiChoiceItemLayout -> M
boolean mShowTitle -> P
int mViewSpacingLeft -> j
int mButtonPanelSideLayout -> K
- 코드 난독화란?
- 보안을 강화하고 앱의 크기를 줄이기 위해 소스 코드내의 클래스, 메서드, 필드 이름을 짧고 의미 없는 이름으로 변환하는 과정이다.
- 난독화를 통해 DEX 파일(안드로이드 앱의 실행파일) 크기를 줄일 수 있다.
- 난독화와 스택 트레이스
- 스택 트레이스
- 프로그램이 실행되는 동안 특정 시점에 호출 스택(call stack)의 상태를 나타내는 정보입니다.
- 호출 스택은 프로그램에서 함수 또는 메서드가 호출되어 실행되는 순서와 구조를 추적하는데 사용된다.
- 오류로 프로그램이 중단될 때, 스택 트레이스는 오류가 발생한 지점과 그 이전에 실행된 함수를 보여준다.
- 난독화된 앱에서 오류가 발생하면, 스택 트레이스도 난독화된 이름을 사용합니다.
- 이로 인해 스택 트레이스를 해석하기 어려워질 수 있다.
- 해결 방법
- 난독화된 스택 트레이스를 원래 형태로 되돌리기 위해서는 mapping file이 필요하다.
- 이 파일은 난독화 과정에서 생성되며, 원래 이름과 난독화된 이름간의 매핑 정보를 담고 있다.
- 스택 트레이스
- 리플렉션과 Keep 규칙
- 리플렉션
- 리플렉션은 프로그램이 자기 자신의 구조(클래스, 인터페이스, 필드, 메서드 등)에 대한 정보를 검사하고 조작할 수 있게 하는 프로그래밍 기능
- java, kotlin에서 리플렉션은 java.lang.reflect 패키지를 통해 제공된다.
- 사용 사례
- 런타임에 동적으로 객체를 생성하거나 메서드를 호출할 때 사용한다.
- 특정 클래스 이름이나 메서드 이름을 미리 알지 못하는 경우에 유용
- 많은 프레임워크와 라이브러리는 사용자 정의 클래스와 메서드를 자동으로 찾아내고 사용하기 위해 리플렉션을 사용합니다.
- 예를 들어 Java의 Spring 프레임 워크는 리플렉션을 사용하여 객체의 의존성을 주입한다.
- 리플렉션은 런타임에 클래스의 메타데이터를 조사하고, 클래스의 메서드나 필드에 접근하는 방법입니다.
- 리플렉션을 사용하는 코드는 예측 가능한 이름 지정을 필요로 합니다.
- Keep 규칙의 필요성
- 난독화 과정에서 리플렉션을 사용하는 클래스나 메서드의 이름이 변경되면, 리플렉션 호출이 실패할 수 있습니다.
- 따라서 난독화 도구(ProGuard, R8)에 이러한 클래스나 메서드의 원래 이름을 유지하도록 지시하는 keep규칙을 설정해야합니다.
- 리플렉션
- 난독화된 스택 트레이스 디코딩
- 밑에 스택트레이스 다시 추적이라는 글 참고
코드 최적화
class MyClass {
fun myMethod() {
println("Hello, World!")
}
fun callMethod() {
myMethod()
}
}
class MyClass {
fun callMethod() {
println("Hello, World!") // myMethod의 내용이 여기로 인라인 됨
}
}
abstract class BaseClass {
abstract fun doSomething()
}
class ConcreteClass : BaseClass() {
override fun doSomething() {
println("Do something")
}
}
class ConcreteClass {
fun doSomething() {
println("Do something")
}
}
- 앱을 더 많이 축소하기 위해 R8은 코드를 더 상세히 검사하여 사용하지 않는 코드를 추가로 삭제하거나 가능하다면 코드를 간결하게 다시 작성합니다.
- 최적화의 예시
- 주어진 if/else 구문에서 코드가 else{} 분기를 전혀 사용하지 않는 경우 R8은 else{}분기 코드를 삭제할 수 있습니다.
- 코드가 한 곳에서만 메서드를 호출하는 경우 R8은 이 메서드를 삭제하고 단일 호출 사이트에서 인라인으로 처리할 수 있습니다.
- 클래스에서 고유한 서브클래스가 하나만 있고 클래스 자체가 인스턴스화 되지 않는다고 판단되면(예: 하나의 구체적인 구현 클래스에서만 사용하는 추상적인 기본 클래스) R8은 두 개의 클래스를 결합하여 앱에서 클래스를 삭제할 수 있습니다.
- R8의 최적화 제한
- 사용자 정의 최적화 제한
- R8은 사용자가 임의로 최적화를 조정하거나 수정하는 것을 허용하지 않습니다.
- ProGuard에서 사용되는 -optimizations 또는 -optimizationpasses와 같은 설정을 통해 기본 최적화 과정을 변경하는 것을 R8은 무시합니다.
- 왜냐하면 최적화의 일관성 유지와 개발자가 직면할 수 있는 문제를 쉽게 해결하기 위함
- 사용자 정의 최적화 제한
- 스택 트레이스의 변경
- 최적화와 스택 트레이스
- R8의 최적화 과정 중 일부는 스택 트레이스를 변경할 수 있습니다.
- 예를 들어, 메서드 인라인 최적화는 스택 트레이스에서 해당 메서드 호출을 제거합니다.
- 이는 오류 추적과 디버깅을 복잡하게 만들 수 있습니다.
- ReTrace(재추적)
- 난독화 및 최적화된 앱의 스택트레이스를 원래 형태로 복원하기 위해 ReTrace 도구를 사용할 수 있다.
- 이 도구는 난독화 매핑 파일을 사용하여 난독화된 스택 트레이스를 원래의 가독성 있는 형태로 되돌립니다.
- 최적화와 스택 트레이스
- 더 적극적인 최적화 사용
- R8은 기본적으로 사용하지 않는 추가적인 최적화 조합을 포함합니다.
- 프로젝트의 gradle.properties 파일에 다음 내용을 포함하여 추가적인 최적화를 사용할 수 있습니다.
- android.enableR8.fullMode=true
- 추가적인 최적화를 사용하면 R8이 ProGuard와 다르게 동작하기 때문에 런타임 문제를 피하고자 ProGuard 규칙을 추가로 포함해야 할 수 도 있습니다.
- 예를 들어 코드가 Java Reflection API를 통해 클래스를 참조한다고 가정한다.
- 기본적으로 실제로 그렇게 코드를 작성하지 않았더라도 R8은 런타임에 클래스의 객체를 검사하고 조작하는 코드가 있다고 가정하고 자동으로 클래스와 클래스의 정적 초기화 프로그램을 유지한다.
- 그러나 full mode를 사용하면 R8은 이러한 가정을 하지 않으며 런타임에 코드가 클래스를 사용하지 않는다고 판단되면 앱의 최종 DEX에서 클래스를 삭제한다.
- R8은 정적 초기화 블록을 자동으로 보존하지 않는다. 그래서 keep 규칙을 설정해야한다.
- 즉, 클래스와 클래스의 정적 초기화 프로그램을 유지하려면 규칙파일에 keep 규칙을 포함해야합니다.
stacktraces 재추적
//proguard-rules.pro 파일에 위의 규칙을 추가하여 재추적할 충분한 정보를 빌드에 유지해야한다.
-keepattributes LineNumberTable,SourceFile
-renamesourcefileattribute SourceFile
- R8에서 처리된 코드는 스택 트레이스를 더 이해하기 어렵게 할 수 있는 여러 방식으로 변경됩니다.
- 스택 트레이스가 소스 코드와 정확히 일치하지 않기 때문입니다.
- 디버깅 정보가 유지되지 않을 때 줄 번호를 변경하는 경우를 예로 들 수 있습니다.
- 코드가 축소되고 난독화되면 줄 번호가 변경된다.
- 원래 스택 트레이스를 복구하기 위해 R8은 명령줄 도구 패키지와 함께 번들로 제공되는 retrace 명령줄 도구를 제공합ㄴ디ㅏ.
- 앱의 스택트레이스 재추적을 지원하려면 모듈의 proguard-rules.pro 파일에 위의 규칙을 추가하여 재추적할 충분한 정보를 빌드에 유지해야한다.
- LineNumberTable 속성은 바이트코드 내의 각 명령어가 원래 소스 파일에서 어느 중에 위치하는지에 대한 정보를 제공합니다.
- SourceFile 속성은 클래스 파일과 연결된 원본 소스 파일의 이름을 저장합니다.
- renamesourcefileattribute 속성
- 난독화 과정에서 모든 소스 파일 이름을 SourceFile이라는 단일 이름으로 변경합니다.
- 난독화된 스택 트레이스에서 소스 파일 이름을 익명화 항여 보안을 강화하는데 도움이 된다.
- 원본 소스 파일의 이름은 매핑 파일에 저장되어 있으므로, 난독화 과정에서 소스파일 이름을 변경해도 재추적(retrace) 도구를 사용하여 원본 스택 트레이스를 복구할 수 있습니다.
- 앱을 Google Play에 게시하는 경우 각 앱 버전의 mapping.txt 파일을 업로드할 수 있습니다.
- Android App Bundle을 사용하여 게시할 때 이 파일은 자동으로 App Bundle 콘텐츠의 일부로 포함됩니다.
- 그러면 Google Play는 사용자가 보고한 문제에서 수신한 스택 트레이스를 재추적하므로 Play Console에서 스택 트레이스를 검토할 수 있습니다.
- 비정상 종료 스택 트레이스를 가독화(https://support.google.com/googleplay/android-developer/answer/6295281?hl=ko)
R8 문제 해결
- 이 섹션은 R8에서 축소, 난독화 및 최적화를 사용할 때 발생하는 문제를 해결하기 위한 몇가지 전략을 설명한다.
- 아래 문제에서 해결책 못찾았다면 R8 FAQ 페이지(https://r8.googlesource.com/r8/+/refs/heads/master/compatibility-faq.md)와 ProGuard의 문제 해결 가이드(https://www.guardsquare.com/en/products/proguard/manual/troubleshooting)를 참조
keep 규칙을 적용해야 하는 이유
- 리플렉션을 사용하는 코드
- 예시 : 클래스, 메서드, 필드 이름이 리플렉션을 통해 런타임에 사용될 때
- 이유 : 난독화로 인해 리플렉션을 사용하는 코드가 실패할 수 있습니다.
- JNI(Java Native Interface)를 사용하는 코드
- 예시 : 네이티브 코드(C/C++)에서 호출되는 Java 메서드
- 이유 : JNI 호출은 문자열 기반으로 메서드 이름을 참조하기 때문에, 이러한 메서드의 이름은 변경되면 안됨
- 엔트리 포인트
- 예시 : Acitivity, Service, BroadcastReceiver, ContentProvider
- 이유 : 이러한 클래스는 앱의 메니페스트에 명시되며, 안드로이드 시스템에 의해 집접 호출 됩니다.
- 프레임워크 또는 라이브러리에 의존하는 코드
- 예시: 특정 라이브러리에 의해 사용되는 콜백 메서드나 인터페이스
- 이유: 라이브러리 내부에서 기대하는 이름과 서명을 유지해야 올바르게 작동함
- XML 파일에서 참조되는 리소스
- 예시 : 레이아웃 XML 파일에서 참조되는 클래스( 예: 사용자 정의 뷰)
- 이유 : XML 파일에서 참조되는 클래스 이름은 변경되어서는 안된다.
- Serializable 객체
- 예시 : 직렬화/ 역직렬화 과정에서 사용되는 클래스
- 이유 : 객체의 직렬화 형식은 클래스 이름에 의존합니다.
- 직렬화
- 객체를 바이트 스트림으로 변환합니다.
- 변환된 바이트 스트림은 파일 시스템, 네트워크등 다양한 저장 매체에 저장되거나 전송됩니다.
- 역직렬화
- 바이트 스트림을 읽어서 원래 객체의 상태로 복원한다.
- 복원된 객체는 원래의 속성과 값을 가집니다.
R8, Proguard 지시어
- -keepattributes Signature
- 중요성
- Signature 속성은 클래스, 인터페이스, 메서드 및 필드에 대한 제네릭 타입 정보를 포함한다.
- java나 kotlin에서 제네릭 타입은 컴파일 타임에만 타입정보를 제공하고, 런타임에는 타입소거로 인해 이 정보는 일반적으로 사용할 수 없습니다.
- 하지만 Signature 속성은 이 정보를 런타임에도 유지하도록 해준다.
- 적용 대상
- 앱의 모든 클래스 파일에 적용된다.
- 중요성
- -dontwarn org.conscrypt.**
- org.conscrypt 패키지와 그 하위 패키지에 대한 모든 경고를 무시합니다.
- -keep class androidx.**{*;}
- -keep class androidx.**
- androidx 패키지 및 그 하위 패키지에 있는 모든 클래스를 유지하라는 지시이다.
- {*;}
- 클래스 내의 모든 멤버(필드, 메서드 등)을 포함하라는 의미이다.
- *은 모든 멤버를 나타내며 ;는 명령어의 끝을 표시합니다.
- 클래스 이름과 클래스 구현을 난독화하지 않는다.
- 메니페스트에 있는 액티비티
- 이름은 난독화 안되고 구현은 난독화 됨
- 메니페스트에 있는 액티비티
- -keep class androidx.**
- -keepnames class androidx.**
- 클래스 이름은 유지하지만, 클래스의 멤버(메서드와 필드)는 난독화 대상이 될수 있다. 즉 멤버 이름은 변경될수 있다.
- keepclassmembers
- 이 지시어는 클래스 자체의 이름은 보존하지 않지만, 그 안에 있는 멤버드른 보호합니다.
-keepclassmembers class com.example.MyClass {
public <methods>;
}
// MyClass의 모든 public 메서드를 보존하려면 다음과 같이 작성합니다.
'android' 카테고리의 다른 글
[안드로이드] READ_EXTERNAL_STORAGE (0) | 2025.05.21 |
---|---|
안드로이드 FLAG 정리 (0) | 2025.05.20 |
17.Android 기기에 device owner를 설정하는 방법 (1) | 2024.01.05 |
15. 안드로이드 동시성 코루틴 공부 (0) | 2023.12.14 |
14. 안드로이드 Handler 공부 (1) | 2023.12.08 |