Bật tối ưu app với R8
Để người dùng có trải nghiệm tốt nhất, app nên nhỏ và chạy nhanh nhất có thể. Công cụ tối ưu app của Android tên là R8: nó xoá code và resource không dùng tới, viết lại code để chạy nhanh hơn, và nhiều việc khác nữa. Với người dùng, kết quả là:
- App khởi động nhanh hơn
- Dùng ít bộ nhớ hơn
- Render và chạy mượt hơn
- Ít ANR hơn
Tổng quan về tối ưu của R8
Phần tiêu đề “Tổng quan về tối ưu của R8”R8 tối ưu app qua nhiều giai đoạn để giảm kích thước và tăng tốc độ. Các thao tác chính:
- Code shrinking (còn gọi là tree shaking): R8 tìm và xoá code không thể chạy tới, cả trong app lẫn trong các library dependency. Bắt đầu từ các entry point của app (ví dụ
Activity,Servicekhai báo trong manifest), R8 dựng một đồ thị code được tham chiếu, phần nào không nằm trong đồ thị sẽ bị xoá. - Logical optimization: R8 viết lại code để chạy hiệu quả hơn, giảm overhead. Hai kỹ thuật chính:
- Method inlining: thay lời gọi method bằng chính phần thân của method đó. Việc này bỏ được chi phí gọi hàm và mở đường cho các tối ưu tiếp theo.
- Class merging: gộp một nhóm class và interface thành một class. Số class trong app giảm, áp lực bộ nhớ giảm và app khởi động nhanh hơn.
- Obfuscation (còn gọi là minification): để file DEX nhỏ hơn, R8 rút ngắn tên class, field và method (ví dụ
com.example.MyActivitycó thể thànha.b.a).
Từ Android Gradle Plugin (AGP) 8.12.0, R8 tối ưu cả resource trong các giai đoạn tối ưu của nó. Xem mục Optimized resource shrinking bên dưới.
Bật tối ưu
Phần tiêu đề “Bật tối ưu”Dùng DSL phù hợp với phiên bản AGP của dự án. Chỉ nên bật tối ưu cho bản cuối cùng mà bạn kiểm thử trước khi phát hành (thường là bản release), vì tối ưu làm build lâu hơn và khiến debug khó hơn do code bị biến đổi.
AGP 9.3 trở lên
Phần tiêu đề “AGP 9.3 trở lên”Với AGP 9.3 trở lên:
- Đặt
enable = truetrong khốioptimizationcủa build type release, trong build script cấp app. - Đặt keep rule của bạn vào file có đuôi
.keeptrong source setsrc/<variant>/keepRules, ví dụsrc/main/keepRules/custom-rules.keep.
android { buildTypes { release { optimization { enable = true // Bật tối ưu code và resource. } } }}android { buildTypes { release { optimization { enable = true // Bật tối ưu code và resource. } } }}DSL cũ cho AGP dưới 9.3
Phần tiêu đề “DSL cũ cho AGP dưới 9.3”Đặt isMinifyEnabled = true (tối ưu code) và isShrinkResources = true (tối ưu resource) trong build type release của build script cấp app. Nên luôn bật cả hai.
android { buildTypes { release {
// Bật tối ưu code. isMinifyEnabled = true
// Bật resource shrinking. isShrinkResources = true
proguardFiles( // File mặc định chứa các rule tối ưu được sinh sẵn. getDefaultProguardFile("proguard-android-optimize.txt"),
... ) ... } } ...}android { buildTypes { release {
// Bật tối ưu code. minifyEnabled = true
// Bật resource shrinking. shrinkResources = true
// File mặc định chứa các rule tối ưu được sinh sẵn. proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
... } }}Tăng hiệu quả tối ưu của R8
Phần tiêu đề “Tăng hiệu quả tối ưu của R8”Lợi ích R8 mang lại tỉ lệ thuận với phần codebase mà R8 được phép tối ưu. Để tận dụng tối đa, làm theo các best practice sau:
- Bật R8 ở full mode
- Bật đủ obfuscation, optimization và shrinking
- Bật resource shrinking và optimized resource shrinking
- Tinh chỉnh keep rule để class, field và method được tối ưu nhiều nhất có thể
Để tinh chỉnh keep rule, dùng R8 Configuration Analyzer. Công cụ này giúp bạn:
- Theo dõi và cải thiện chất lượng cấu hình R8 tổng thể qua các chỉ số trong báo cáo.
- Tìm những keep rule rộng nhất (cản trở tối ưu nhiều nhất) và biết chúng chặn loại tối ưu nào, từ đó thu hẹp lại.
R8 Configuration Analyzer có từ AGP 9.3.0-alpha05 hoặc R8 9.3.7-dev.
Optimized resource shrinking để app nhỏ hơn nữa
Phần tiêu đề “Optimized resource shrinking để app nhỏ hơn nữa”AGP 8.12.0 giới thiệu optimized resource shrinking, gộp tối ưu resource vào chung quy trình tối ưu code để app nhỏ và nhanh hơn nữa. Trước đây, Android Asset Packaging Tool (AAPT2) sinh ra keep rule khiến resource shrinking bị xử lý tách biệt với code, nên thường giữ lại những code hoặc resource không còn dùng được nhưng vẫn tham chiếu lẫn nhau.
Với optimized resource shrinking, resource được coi như một phần của code chương trình và cùng nằm trong đồ thị tham chiếu. Nhóm code hoặc resource nào không được tham chiếu và không được keep rule bảo vệ thì có thể bị xoá.
Bật optimized resource shrinking
Phần tiêu đề “Bật optimized resource shrinking”Với AGP 8.12 hoặc 8.13, thêm dòng sau vào gradle.properties của dự án:
android.r8.optimizedResourceShrinking=trueTừ AGP 9.0.0, bạn không cần đặt cờ này nữa. Optimized resource shrinking tự áp dụng khi isShrinkResources = true được bật trong cấu hình build.
Kiểm tra và cấu hình R8
Phần tiêu đề “Kiểm tra và cấu hình R8”Để R8 dùng được toàn bộ khả năng tối ưu, xoá dòng sau khỏi gradle.properties nếu có:
android.enableR8.fullMode=falseLưu ý bật tối ưu sẽ khiến stack trace khó đọc, nhất là khi R8 đổi tên class hoặc method. Cách lấy lại stack trace đúng với mã nguồn xem bài R8: Kiểm thử và gỡ lỗi.
Khi đã bật R8, bạn nên tạo Startup Profile để app khởi động nhanh hơn nữa.
Nếu bật tối ưu xong app bị lỗi, có vài hướng xử lý:
- Thêm keep rule để giữ nguyên một phần code.
- Bật tối ưu từng bước.
- Đổi sang library phù hợp với tối ưu hơn.
Nếu bạn muốn build nhanh hơn, xem Configure how R8 runs để cấu hình R8 theo môi trường build.
Thay đổi hành vi theo phiên bản AGP và R8
Phần tiêu đề “Thay đổi hành vi theo phiên bản AGP và R8”Bảng sau tóm tắt các tính năng chính được thêm vào qua các phiên bản AGP và R8.
| Phiên bản AGP | Tính năng mới |
|---|---|
| 9.3 | DSL đơn giản hơn: khối optimization {} mới cần ít cấu hình hơn. Optimized resource shrinking luôn bật, keep rule mặc định của Android được thêm sẵn (muốn bỏ thì phải tắt).Source set keepRules: nếu dùng DSL mới, bạn bắt buộc đặt keep rule trong src/<variant>/keepRules, file đuôi .keep. Source set này cũng dùng được với DSL cũ.Consumer rule cho KMP (Kotlin Multiplatform): KMP không còn yêu cầu đặt publish = true, và hỗ trợ source set src/<variant>/keepRules với đuôi .keep.DSL cũ vẫn được hỗ trợ. |
| 9.1 | Repackage class mặc định: R8 chuyển class vào unnamed package (cấp cao nhất) để DEX gọn hơn nữa, bạn không cần khai báo -repackageclasses. Cách hoạt động và cách tắt xem bài R8: Keep rule (mục global option). |
| 9.0 | Optimized resource shrinking bật mặc định (điều khiển bằng android.r8.optimizedResourceShrinking). Tối ưu đồng thời tham chiếu code và resource giúp tìm và xoá resource chỉ được tham chiếu từ code không dùng, tốt hơn hẳn cách tối ưu tách rời trước đây. Đặc biệt hữu ích với app dùng chung nhiều resource và code giữa các form factor, đã đo được mức giảm kích thước app hơn 50%. App nhỏ hơn nghĩa là tải về nhanh hơn, cài nhanh hơn, khởi động nhanh hơn, render tốt hơn và ít ANR hơn.Lọc rule từ library: không còn hỗ trợ global option (ví dụ -dontobfuscate) trong consumer rule của library, app sẽ lọc bỏ chúng.Kotlin null check: được tối ưu mặc định (điều khiển bằng -processkotlinnullchecks). Phiên bản này cũng cải thiện đáng kể tốc độ build.Tối ưu một số package cụ thể: dùng packageScope (đang ở dạng thử nghiệm), xem mục bên dưới.Tối ưu mặc định: bỏ hỗ trợ getDefaultProguardFile("proguard-android.txt") vì file này có -dontoptimize, thứ nên tránh. Hãy dùng "proguard-android-optimize.txt". Nếu thật sự cần tắt tối ưu toàn app, tự thêm cờ đó vào file proguard. |
| 8.12 | Optimized resource shrinking: hỗ trợ ban đầu (điều khiển bằng android.r8.optimizedResourceShrinking), phải tự bật ở phiên bản AGP này.Retrace trong Logcat: cửa sổ Logcat của Android Studio tự retrace stack trace. |
| 8.6 | Retrace tốt hơn: mặc định retrace cả tên file và số dòng cho mọi minSdk (trước đây, từ bản 8.2, cần minSdk 26 trở lên). Cập nhật R8 giúp stack trace của bản đã obfuscate dễ đọc hơn, giúp các công cụ như Logcat của Android Studio tự retrace crash về đúng mã nguồn. |
| 8.0 | Full mode mặc định: R8 full mode tối ưu mạnh hơn hẳn và được bật mặc định. Có thể tắt bằng android.enableR8.fullMode=false. |
| 7.0 | Có full mode: ra mắt dưới dạng tuỳ chọn, bật bằng android.enableR8.fullMode=true. Full mode tối ưu mạnh hơn bằng cách giả định chặt hơn về cách code dùng reflection và các tính năng động khác. App nhỏ và nhanh hơn, nhưng có thể cần thêm keep rule để code cần thiết không bị xoá. |
Full mode
Phần tiêu đề “Full mode”R8 có hai chế độ: compatibility mode và full mode. Full mode cho các tối ưu mạnh nhất để cải thiện hiệu năng app. Phần này nêu những khác biệt chính giữa hai chế độ và các cấu hình cần thêm để chuyển sang full mode an toàn, tránh crash lúc chạy.
Bật full mode
Phần tiêu đề “Bật full mode”Xoá dòng sau khỏi file gradle.properties:
android.enableR8.fullMode=false // Xoá dòng này để bật full modeGiữ class đi kèm với attribute
Phần tiêu đề “Giữ class đi kèm với attribute”Attribute là metadata lưu trong file class đã biên dịch, không thuộc phần code thực thi. Tuy vậy, một số kiểu reflection cần tới chúng. Ví dụ hay gặp: Signature (giữ thông tin kiểu generic sau khi type erasure), InnerClasses và EnclosingMethod (để reflect cấu trúc class), và các annotation thấy được lúc chạy (runtime-visible).
Ví dụ attribute Signature của một field trong bytecode. Với field:
List<User> users;File class đã biên dịch sẽ chứa bytecode:
.field public static final users:Ljava/util/List; .annotation system Ldalvik/annotation/Signature; value = { "Ljava/util/List<Lcom/example/package/User;>;" } .end annotation.end fieldCác library dùng nhiều reflection (như Gson) thường dựa vào các attribute này để đọc cấu trúc code lúc chạy. Ở full mode, mặc định attribute chỉ được giữ nếu class, field hoặc method tương ứng được keep tường minh.
Ví dụ dưới đây cho thấy vì sao cần attribute và cần thêm keep rule nào khi chuyển từ compatibility mode sang full mode. Ngoài việc keep class, field hoặc method bị reflect, bạn còn phải keep tường minh các attribute mà chúng dựa vào.
Xét ví dụ deserialize một danh sách user bằng Gson:
import com.google.gson.Gsonimport com.google.gson.reflect.TypeToken
data class User( @SerializedName("username") var username: String? = null, @SerializedName("age") var age: Int = 0)
fun GsonRemoteJsonListExample() { val gson = Gson()
// 1. Chuỗi JSON danh sách user trả về từ server val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""
// 2. Deserialize chuỗi JSON thành List<User> // Phải dùng TypeToken cho kiểu generic như List val listType = object : TypeToken<List<User>>() {}.type val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)
// In danh sách println("First user from list: ${deserializedList}")}Khi biên dịch, type erasure của Java xoá tham số kiểu generic. Lúc chạy, cả List<String> và List<User> đều chỉ là List thô. Vì vậy các library dựa vào reflection như Gson không biết List được khai báo chứa kiểu gì khi deserialize một mảng JSON, dẫn tới lỗi lúc chạy.
Để giữ thông tin kiểu, Gson dùng TypeToken. Bọc kiểu trong TypeToken giúp giữ lại thông tin cần cho việc deserialize.
Biểu thức Kotlin object:TypeToken<List<User>>() {}.type tạo ra một anonymous inner class kế thừa TypeToken và giữ thông tin kiểu generic. Trong ví dụ này, anonymous class có tên $GsonRemoteJsonListExample$listType$1.
Java lưu generic signature của superclass dưới dạng metadata, gọi là attribute Signature, trong file class đã biên dịch. TypeToken đọc metadata Signature này để khôi phục kiểu lúc chạy. Nhờ vậy Gson dùng reflection đọc Signature và biết được kiểu đầy đủ List<User> để deserialize.
Ở compatibility mode, R8 giữ attribute Signature cho các class, kể cả anonymous inner class như $GsonRemoteJsonListExample$listType$1, ngay cả khi không có keep rule riêng. Vì vậy compatibility mode không cần thêm keep rule nào để ví dụ chạy đúng.
// keep rule cho compatibility mode-keepattributes SignatureỞ full mode, attribute Signature của anonymous inner class $GsonRemoteJsonListExample$listType$1 bị xoá. Không có thông tin kiểu trong Signature, Gson không tìm được kiểu đúng và ném IllegalStateException.
Nếu dùng Gson cũ hơn 2.11.0, các keep rule cần thêm là:
// keep rule cần cho full mode-keepattributes Signature-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken-keepattributes Signature: bảo R8 giữ attribute mà Gson cần đọc. Ở full mode, R8 chỉ giữSignaturecho class, field hoặc method khớp tường minh với một rulekeep.-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: cần vìTypeTokenbọc kiểu của đối tượng được deserialize. Sau type erasure, một anonymous inner class được tạo ra để giữ thông tin kiểu generic. Nếu không keep tường minhcom.google.gson.reflect.TypeToken, full mode sẽ không đưa kiểu class này vào attributeSignaturecần cho việc deserialize.-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: giữ thông tin kiểu của các anonymous class kế thừaTypeToken, như$GsonRemoteJsonListExample$listType$1trong ví dụ. Thiếu rule này, full mode xoá thông tin kiểu cần thiết và việc deserialize thất bại.
Cần hiểu là các rule trên chỉ giải quyết việc tìm ra kiểu generic (ví dụ List<User>). R8 còn đổi tên field của class. Nếu data model không dùng annotation @SerializedName, Gson sẽ không deserialize được JSON vì tên field không còn khớp với key trong JSON.
Nếu dùng Gson cũ hơn 2.11, hoặc model không dùng @SerializedName, bạn phải thêm keep rule tường minh cho các model đó.
Giữ constructor mặc định
Phần tiêu đề “Giữ constructor mặc định”Ở full mode, constructor không tham số (constructor mặc định) không tự được giữ, kể cả khi class được giữ. Nếu bạn tạo instance bằng class.getDeclaredConstructor().newInstance() hoặc class.newInstance(), bạn phải keep tường minh constructor không tham số. Còn compatibility mode thì luôn giữ constructor này.
Ví dụ tạo instance PreCacheTask bằng reflection rồi gọi method run. Ở compatibility mode không cần thêm rule, nhưng ở full mode, constructor mặc định của PreCacheTask sẽ bị xoá, nên cần một keep rule riêng.
// Trong libraryinterface StartupTask { fun run()}// Object của library, nạp và chạy task.object TaskRunner { fun execute(taskClass: Class<out StartupTask>) { // Class không bị xoá, nhưng constructor của nó có thể bị xoá. val task = taskClass.getDeclaredConstructor().newInstance() task.run() }}
// Trong appclass PreCacheTask : StartupTask { override fun run() { Log.d("Pre cache task", "Warming up the cache...") }}
fun runTaskRunner() { // Library nhận tham chiếu trực tiếp tới class task của app. TaskRunner.execute(PreCacheTask::class.java)}# Keep rule cho full mode# phải chỉ rõ constructor mặc định
-keep class com.example.fullmoder8.PreCacheTask { <init>();}Access modification được bật mặc định
Phần tiêu đề “Access modification được bật mặc định”Ở compatibility mode, R8 không đổi visibility của method và field trong class. Ở full mode, R8 đổi visibility (ví dụ từ private thành public) để inline được nhiều hơn.
Tối ưu này có thể gây lỗi nếu code dùng reflection dựa vào việc member có visibility cụ thể. R8 không nhận ra cách dùng gián tiếp đó nên app có thể crash. Để tránh, thêm rule -keep cho các member liên quan, rule này cũng giữ nguyên visibility gốc của chúng.
Ví dụ vì sao không nên dùng reflection truy cập member private, cùng keep rule để giữ chúng, xem bài R8: Ví dụ keep rule và Jetpack Keep.
Metadata riêng của Kotlin
Phần tiêu đề “Metadata riêng của Kotlin”Khi biên dịch Kotlin, compiler lưu metadata riêng của ngôn ngữ (nullability, extension function, signature của coroutine…) trong annotation @kotlin.Metadata trên mỗi file class.
Nếu app hoặc dependency dùng Kotlin reflection (kotlin.reflect), thư viện reflection sẽ đọc metadata này lúc chạy để biết cấu trúc class. Ở full mode, R8 mặc định xoá annotation không được keep tường minh. Ngoài ra, nếu R8 minify hoặc shrink class mà không giữ và cập nhật metadata, Kotlin reflection sẽ lỗi lúc chạy, gây hành vi khó lường hoặc crash (ví dụ KotlinReflectionInternalError).
Để Kotlin reflection chạy đúng sau khi minify, bạn phải giữ các annotation runtime-visible và keep tường minh class kotlin.Metadata:
# Giữ annotation runtime-visible, cần để đọc metadata-keepattributes RuntimeVisibleAnnotations
# Giữ Kotlin metadata để kotlin.reflect chạy đúng-keep class kotlin.Metadata { *; }Tối ưu một số package với packageScope (thử nghiệm)
Phần tiêu đề “Tối ưu một số package với packageScope (thử nghiệm)”Bạn có thể chỉ tối ưu một số package cụ thể bằng R8 với packageScope. Tính năng này được thiết kế làm bước đầu tuỳ chọn cho app chưa dùng R8, và không khuyến nghị cho app đã dùng R8.
Có nên dùng packageScope không
Phần tiêu đề “Có nên dùng packageScope không”Nếu app đã dùng R8, đừng dùng packageScope: đây là cấu hình kém tối ưu hơn về hiệu năng và kích thước app. Thay vào đó, hãy cải thiện cấu hình R8 bằng keep rule tốt hơn, hoặc chuyển sang full mode nếu đang dùng compatibility mode.
App chưa dùng R8 và đang chuyển sang có thể dùng packageScope để chuyển dần. Vì R8 tối ưu mạnh tới mức có thể đổi hành vi app, việc giới hạn tối ưu vào những package an toàn (như AndroidX và Kotlin) cho bạn lợi ích hiệu năng với rủi ro thấp. Khi app đã ổn định, bạn mở rộng dần sang phần còn lại của codebase và dependency, kiểm thử ở mỗi bước.
Điều kiện
Phần tiêu đề “Điều kiện”- Android Gradle Plugin 9.0 trở lên.
- R8 full mode, không phải compatibility mode.
Cấu hình
Phần tiêu đề “Cấu hình”Bước 1. Chọn library để tối ưu. Nên bắt đầu với AndroidX và Kotlin: androidx.**, kotlin.** và kotlinx.**, vì đây là các library ổn định đã được cấu hình tương thích với R8.
Bước 2. Bật hỗ trợ packageScope. Thêm vào gradle.properties:
android.r8.gradual.support=trueBước 3. Khai báo khối optimization. Trong build.gradle.kts (hoặc build.gradle) cấp module, thêm khối optimization vào build type release. Trong khối này, dùng packageScope liệt kê các package cần tối ưu. Với build.gradle.kts, bọc danh sách trong setOf().
android { buildTypes { release { optimization { enable = true packageScope = setOf("androidx.**","kotlin.**", "kotlinx.**") } } }}android { buildTypes { release { optimization { enable = true packageScope = ["androidx.**", "kotlin.**", "kotlinx.**"] } } }}Bước 4. Kiểm thử. Sau khi thêm hoặc sửa packageScope, kiểm thử app kỹ để chắc không có crash hay thay đổi hành vi bất ngờ.
Chuyển từ tối ưu một số package sang tối ưu toàn app
Phần tiêu đề “Chuyển từ tối ưu một số package sang tối ưu toàn app”Để tận dụng tối đa, bạn nên dần dần bỏ packageScope và dùng R8 cho toàn app, bằng cách mở rộng phạm vi tối ưu từng chút:
- Bắt đầu với library ổn định. Chỉ đưa vào
packageScopecác library phổ biến, ổn định, tương thích với tối ưu của R8:androidx.**,kotlin.**,kotlinx.**. - Thêm dần package. Thêm dần các tiền tố package mới vào
packageScope:- Đánh giá dependency. Xem lại các library của app. Ứng viên tốt là library chính thức của Google (ví dụ
com.google.**) và các library chắc chắn như OkHttp (okhttp3.**,okio.**). Ưu tiên library ít dùng reflection, serialization hay gọi native (JNI). - Ưu tiên theo kích thước package. Dùng APK Analyzer của Android Studio để tìm package chiếm nhiều dung lượng nhất:
- Build AAB hoặc APK release với R8 tắt.
- Mở nó trong Analyzer và xem các file
dex. - Sắp xếp package theo kích thước. Package lớn nhất cho lợi ích cao nhất khi tối ưu. Nhắm vào chúng trước giúp giảm kích thước nhiều nhất ngay từ đầu, miễn là các library đó không có keep rule quá rộng. Xem bài R8: Chọn và viết thư viện.
- Kiểm tra thay đổi hành vi. Sau mỗi lần thêm tiền tố package, kiểm thử toàn diện để phát hiện và sửa lỗi hồi quy hay hành vi lạ.
- Đánh giá dependency. Xem lại các library của app. Ứng viên tốt là library chính thức của Google (ví dụ
- Thêm package của app sau cùng. Nếu package của app ít dùng reflection, đưa vào
packageScopevà thêm keep rule dần khi cần. Nếu dùng nhiều reflection, đưa vàopackageScopekèm keep rule cấp package cho các package cần thiết, rồi tinh chỉnh dần các rule đó. - Chuyển sang R8 cho toàn app. Khi phần lớn dependency đã nằm trong
packageScopevà app ổn định, xoápackageScopeđể tối ưu toàn app ở full mode.
Bật tối ưu từng bước
Phần tiêu đề “Bật tối ưu từng bước”Mặc định R8 tối ưu rất nhiều thứ, nhưng chưa chắc tất cả chạy được ngay với app của bạn. Nếu lần đầu bật R8 (hoặc bật full mode) cho một app lớn, hãy bật từng bước: tạm tắt obfuscation và chỉ bật R8 cho từng phần code thay vì toàn bộ. Nên làm vậy khi phát triển ở máy local, nhưng cũng có thể áp dụng khi QA nội bộ, thậm chí trên production theo kiểu phát hành dần. Các bước cụ thể tuỳ vào tiến độ mong muốn và mức độ tin tưởng vào độ phủ kiểm thử trước khi phát hành.
Giới hạn loại tối ưu
Phần tiêu đề “Giới hạn loại tối ưu”R8 làm nhiều loại tối ưu: xoá code, viết lại code và xoá resource. Ở mức tổng quát:
- Code shrinking (tree shaking): xoá code không được tham chiếu
- Obfuscation (identifier minification): rút ngắn tên class và method
- Optimization: viết lại code, ví dụ inline
Để giảm khả năng lỗi, bạn có thể chỉ bật một vài loại trước.
Chỉ bật tree shaking
Phần tiêu đề “Chỉ bật tree shaking”Tree shaking xoá code có vẻ không được tham chiếu. Nên bắt đầu chỉ với tree shaking vì nó dễ hiểu nhất.
Thêm vào proguard-rules.pro để tắt các loại tối ưu còn lại. Tắt obfuscation là quan trọng nhất vì stack trace sẽ dễ đọc hơn nhiều.
-dontobfuscate // Dùng tạm để tắt identifier minification-dontoptimize // Dùng tạm để tắt optimizationCấu hình này không nên dùng khi phát hành vì nó hạn chế rất nhiều khả năng tối ưu của R8, nhưng là điểm khởi đầu tốt khi lần đầu bật R8 cho một codebase lớn đang có nhiều lỗi cần sửa.
Dùng compat mode
Phần tiêu đề “Dùng compat mode”Mặc định R8 chạy ở full mode. Full mode cho hiệu năng và mức giảm kích thước tốt hơn hẳn, nhưng khi bật minify lần đầu bạn có thể tạm tắt nó và dùng compat mode.
Để dùng compat mode, thêm vào gradle.properties:
android.enableR8.fullMode = false // Dùng tạm để tắt full modeBật các tối ưu còn lại
Phần tiêu đề “Bật các tối ưu còn lại”Khi tree shaking đã chạy ổn với app, xoá các cấu hình trên để bật lại obfuscation, optimization và full mode. Obfuscation làm debug khó hơn, nên mới khuyên xử lý lỗi tree shaking trước.
Cách giải mã stack trace bị obfuscate xem bài R8: Kiểm thử và gỡ lỗi.
Giới hạn phạm vi tối ưu
Phần tiêu đề “Giới hạn phạm vi tối ưu”Bản build tối ưu đầy đủ sẽ tối ưu mọi code trong mọi library và package, nên khi mới bật R8 thường sẽ gặp lỗi. Nếu có vấn đề ở một phần của app, đừng tắt R8 hoàn toàn, vì như vậy bạn mất lợi ích ở mọi chỗ khác. Thay vào đó, tạm tắt R8 chỉ ở phần đang gây lỗi.
Dùng keep rule cấp package
Phần tiêu đề “Dùng keep rule cấp package”Nên dùng keep rule cấp package để tạm tắt R8 ở một phần app. Đây chỉ là giải pháp tạm để vượt qua chỗ có vấn đề, bạn luôn phải quay lại sửa sau.
Ví dụ một phần app dùng Gson rất nhiều và gây lỗi khi tối ưu. Cách sửa lý tưởng là thêm keep rule nhắm đúng chỗ hoặc chuyển sang giải pháp codegen. Nhưng để phần còn lại của app vẫn được tối ưu, bạn có thể gom code định nghĩa các kiểu Gson vào một subpackage riêng và thêm rule sau vào proguard-rules.pro:
-keep class com.myapp.json.** { *; }Nếu một library bạn dùng có reflection vào thành phần nội bộ của nó, bạn cũng có thể thêm keep rule cho cả library. Bạn cần xem code hoặc file JAR/AAR của library để tìm đúng package cần keep. Một lần nữa, đây không phải cách để giữ lâu dài, chỉ giúp phần còn lại của app được tối ưu:
-keep class com.somelibrary.** { *; }Gỡ keep rule cấp package
Phần tiêu đề “Gỡ keep rule cấp package”Khi app đã chạy đúng với keep rule cấp package, bạn nên quay lại và thay bằng keep rule nhắm đúng chỗ, hoặc bỏ hẳn đoạn reflection hay library khiến bạn phải viết keep rule đó.
Ví dụ, AndroidX rất hay keep mọi class kế thừa một class nhất định để chỉ giữ những class liên quan. Nói chung, reflection chỉ nên nhắm vào class hoặc method kế thừa một abstract class nhất định, implement một interface nhất định, hoặc có một runtime annotation nhất định. Cả ba cách này đều có cú pháp keep rule hỗ trợ, nên bản app tối ưu đầy đủ cuối cùng không cần keep rule cấp package.
Nếu reflection nằm trong code của chính bạn, bạn có thể thay keep rule cấp package bằng Jetpack Keep. Rule sinh ra là có điều kiện: R8 chỉ giữ đích của lời gọi reflection nếu nó cũng giữ đoạn code thực hiện lời gọi đó. Để chuyển một package, dùng lint check của Jetpack Keep để tìm và gắn annotation cho các lời gọi reflection, rồi xoá rule cấp package.
Dùng R8 với hệ thống build khác
Phần tiêu đề “Dùng R8 với hệ thống build khác”AGP là hệ thống build được khuyến nghị và hỗ trợ chính thức cho app Android, nhưng bạn có thể dùng hệ thống khác như Bazel. Nếu dùng Bazel, bạn có thể tích hợp R8 vào pipeline build để shrink, obfuscate và tối ưu app.
Cách tối ưu app Android bằng Bazel xem tài liệu rules_android của Bazel. Lưu ý Bazel không được hỗ trợ chính thức cho việc phát triển app Android.