ProGuard và R8 là các công cụ mã hóa, thu nhỏ và tối ưu hóa cho ứng dụng Android. ProGuard, được tạo ra vào năm 2002, từ lâu đã là tiêu chuẩn thực tế để bảo vệ mã Java. R8 là phiên bản kế thừa của nó, được Google phát triển và tích hợp vào Android Gradle Plugin từ AGP 3.4. Cả hai công cụ đều giảm kích thước APK, loại bỏ mã chết và làm phức tạp kỹ thuật đảo ngược. Theo Android Developers, R8 thực hiện build nhanh hơn 2–3 lần so với ProGuard với chất lượng mã hóa tương đương.
Những Điểm Chính
ProGuard là một công cụ mã nguồn mở (Apache 2.0) để mã hóa, thu nhỏ, tối ưu hóa và xác minh trước bytecode Java. Nó được phát triển bởi Eric Lafarge vào năm 2002 trong khuôn khổ dự án SourceForge. ProGuard nhận đầu vào là các lớp Java đã biên dịch (.class) hoặc kho lưu trữ JAR và tạo ra các lớp đã xử lý cùng định dạng, nhưng có kích thước nhỏ hơn và các phần tử đã được đổi tên.
Trong một thời gian dài, ProGuard là tiêu chuẩn duy nhất để bảo vệ ứng dụng Android khỏi kỹ thuật đảo ngược. Google chính thức khuyến nghị sử dụng nó trong Android SDK và cung cấp cấu hình mặc định trong tệp proguard-android-optimize.txt bên trong các công cụ SDK. ProGuard hoạt động như một công cụ riêng biệt, được khởi chạy sau khi biên dịch mã Java thành bytecode và trước khi đóng gói vào DEX.
ProGuard bao gồm bốn giai đoạn tuần tự: shrink (loại bỏ các lớp không sử dụng), optimize (tối ưu hóa bytecode — nội tuyến, loại bỏ mã chết), obfuscate (đổi tên các lớp, phương thức và trường thành tên ngắn), preverify (kiểm tra tương thích JVM). Mỗi giai đoạn được điều khiển bằng các quy tắc riêng từ tệp cấu hình.
Trong giai đoạn mã hóa, ProGuard tạo tệp ánh xạ (mapping.txt) ánh xạ tên gốc sang tên đã mã hóa. Tệp này rất quan trọng để giải mã nhật ký sự cố từ các bản build phát hành bằng tiện ích retrace. Nếu không có tệp ánh xạ, dấu vết ngăn xếp sẽ trở thành một tập hợp các chữ cái a(), b(), c() mà không có cách nào khôi phục ngữ cảnh ban đầu.
| Giai đoạn ProGuard | Mục đích | Kết quả |
|---|---|---|
| Shrink | Phân tích đồ thị cuộc gọi và loại bỏ mã chết | Ít lớp hơn trong APK |
| Optimize | Nội tuyến phương thức, loại bỏ tham số không sử dụng | Thực thi mã nhanh hơn |
| Obfuscate | Đổi tên các lớp, trường và phương thức | Bảo vệ khỏi kỹ thuật đảo ngược |
| Preverify | Thêm thuộc tính StackMap cho JVM | Tương thích Java 6+ |
R8 là công cụ mã hóa và thu nhỏ thế hệ tiếp theo của Google, lần đầu tiên được giới thiệu trong Android Studio 3.3 (tháng 11 năm 2018) và trở thành tiêu chuẩn trong AGP 3.4 (tháng 8 năm 2019). Không giống như ProGuard, R8 là một phần của trình biên dịch D8/R8, nơi chuyển đổi bytecode Java sang định dạng DEX. R8 thực hiện tất cả các giai đoạn — mã hóa, thu nhỏ và tối ưu hóa — trong một lần duy nhất, mà không cần truyền tệp trung gian giữa các công cụ.
Google phát triển R8 với hai mục tiêu: tăng tốc độ build (ProGuard hoạt động như một công cụ bên ngoài) và đảm bảo tích hợp liền mạch với ngăn xếp Android hiện đại (Desugar, Core Library Desugaring, D8). R8 được viết bằng Kotlin và Java và là một phần của kho lưu trữ R8/Desugar trên AOSP (Dự án Nguồn Mở Android).
Một lợi thế quan trọng của R8 là tương thích ngược hoàn toàn với các quy tắc ProGuard. Các tệp .pro hiện có hoạt động mà không cần thay đổi. R8 thậm chí còn hỗ trợ các chỉ thị ProGuard cụ thể, bao gồm -whyareyoukeeping, -printconfiguration và -printmapping. Điều này có nghĩa là việc chuyển đổi từ ProGuard sang R8 rất minh bạch: chỉ cần cập nhật AGP.
// build.gradle.kts — bật R8 qua minifyEnabled
android {
buildTypes {
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
// Cấu hình cơ bản từ Android SDK
getDefaultProguardFile("proguard-android-optimize.txt"),
// Quy tắc dự án tùy chỉnh
"proguard-rules.pro"
)
}
}
}Mã này hiển thị cấu hình build phát hành tiêu chuẩn. Cờ isMinifyEnabled = true kích hoạt R8 cho mã hóa và tối ưu hóa. isShrinkResources = true loại bỏ thêm các tài nguyên không sử dụng. getDefaultProguardFile tải các quy tắc mặc định từ SDK, trong khi proguard-rules.pro chứa các cài đặt dành riêng cho dự án.
Mã hóa là quá trình chuyển đổi mã nguồn thành dạng khó phân tích đối với con người nhưng vẫn giữ nguyên đầy đủ chức năng. Trong bối cảnh Android, mã hóa có nghĩa là đổi tên các lớp, phương thức và trường thành các tên ngắn, vô nghĩa: com.example.app.auth.LoginManager trở thành a.a.a, phương thức authenticateUser trở thành a, trường userToken trở thành b.
Các tệp APK Android là kho lưu trữ có thể được mở bằng bất kỳ trình giải nén nào (ZIP, 7z, WinRAR). Nếu không mã hóa, kẻ tấn công có được bản đồ hoàn chỉnh của ứng dụng: tên gói, lớp, phương thức và trường. Các công cụ như jadx hoặc Bytecode Viewer có thể khôi phục mã Java gần như ban đầu từ các tệp DEX trong vài giây. Mã hóa không làm cho mã trở nên bất khả xâm phạm, nhưng nó nâng cao đáng kể rào cản gia nhập: thay vì các tên có ý nghĩa, người đọc thấy a(), b(), c().
Các mục tiêu mã hóa điển hình: bảo vệ logic thương mại (thuật toán, công thức tính toán), cản trở việc đánh cắp khóa API và token, ngăn chặn thay thế lớp thông qua reflection, và ngăn chặn việc vá và sửa đổi APK (tấn công đóng gói lại). Trên thực tế, 70% nhiệm vụ được giải quyết chỉ bằng cách đổi tên — đó là lý do tại sao ProGuard/R8 được sử dụng.
Dưới đây là tệp proguard-rules.pro điển hình cho dự án Android với Retrofit, Gson và Parcelable. Các quy tắc -keep giữ lại các lớp và phương thức cần thiết cho hoạt động của thư viện thông qua reflection. Nếu không có các quy tắc này, R8 sẽ loại bỏ hoặc đổi tên các lớp mà thư viện truy cập theo tên chuỗi.
# =====================
# Retrofit — bảo tồn giao diện
# =====================
-keep,allowobfuscation,allowshrinking interface retrofit2.** { *; }
-keepattributes Signature, Exceptions
# =====================
# Gson — tuần tự hóa JSON
# =====================
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keep class *.serialization.** {
<fields>;
}
# =====================
# Parcelable — Creator
# =====================
-keepclassmembers class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}
# =====================
# Logging — loại bỏ nhật ký khỏi bản phát hành
# =====================
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(String, int);
public static int v(...);
public static int d(...);
public static int i(...);
public static int w(...);
public static int e(...);
}
# =====================
# Lớp dữ liệu Kotlin — bảo tồn hàm tạo
# =====================
-keepclassmembers class * {
@kotlin.Metadata <fields>;
}
# =====================
# Activity — điểm vào
# =====================
-keep class * extends android.app.Activity {
@android.annotation.SuppressLint <methods>;
}Mỗi chỉ thị trong tệp .pro giải quyết một nhiệm vụ cụ thể. -keep ngăn toàn bộ lớp bị xóa hoặc đổi tên. -keepclassmembers chỉ bảo vệ các thành viên của lớp (trường và phương thức) nhưng cho phép xóa chính lớp đó nếu không được sử dụng. -assumenosideeffects báo cho R8 biết rằng một lời gọi phương thức không có tác dụng phụ và có thể được loại bỏ một cách an toàn. Chỉ thị -keepattributes giữ lại siêu dữ liệu trong bytecode — chú thích, chữ ký, ngoại lệ.
Quy tắc -keep,allowobfuscation,allowshrinking cho Retrofit cho phép R8 đổi tên các giao diện nhưng không được xóa chúng. Điều này cần thiết vì Retrofit truy cập các giao diện thông qua proxy động (java.lang.reflect.Proxy), và việc xóa sẽ dẫn đến ClassNotFoundException trong thời gian chạy. Tương tự, Gson sử dụng reflection để truy cập các trường được chú thích bằng @SerializedName — nếu không có -keepclassmembers, các trường sẽ bị xóa vì không được sử dụng.
Thu nhỏ (shrinking) là quá trình loại bỏ mã và tài nguyên không sử dụng khỏi bản build cuối cùng. ProGuard và R8 phân tích đồ thị cuộc gọi bắt đầu từ các điểm vào (Activity, Service, BroadcastReceiver) và loại bỏ các lớp và phương thức không thể truy cập được qua chuỗi cuộc gọi. ShrinkResources là giai đoạn bổ sung loại bỏ các tài nguyên không sử dụng khỏi res/ (layout, drawable, string, color).
Thu nhỏ mang lại lợi ích lớn nhất trong các dự án lớn có thư viện. Một kịch bản điển hình: một dự án sử dụng 10% mã từ một thư viện được kết nối (ví dụ: Google Play Services). Nếu không thu nhỏ, toàn bộ mã thư viện sẽ vào APK. Với thu nhỏ, R8 loại bỏ 70–90% mã thư viện, chỉ để lại các lớp và phương thức thực sự được sử dụng. Điều này ảnh hưởng trực tiếp đến kích thước APK, thời gian tải và mức tiêu thụ bộ nhớ.
Cơ chế ShrinkResources hoạt động kết hợp với thu nhỏ mã. Sau khi R8 xác định các lớp nào được sử dụng, việc thu nhỏ tài nguyên sẽ phân tích các tham chiếu tài nguyên từ mã: R.layout.main, R.drawable.icon, getString(R.string.title). Tất cả các tài nguyên không có tham chiếu trực tiếp hoặc gián tiếp sẽ bị xóa khỏi APK hoặc AAB cuối cùng. Điều này được thực hiện bằng cách sử dụng tệp tài nguyên resources.arsc và các thư mục res/.
Một sắc thái quan trọng: các tài nguyên có thể được truy cập qua getIdentifier() hoặc Resources.getResourceName() bằng tên chuỗi, bỏ qua lớp R. Trong những trường hợp như vậy, R8 không thấy liên kết trực tiếp và có thể xóa một tài nguyên thực sự đang được sử dụng. Để bảo vệ các tài nguyên như vậy, có chỉ thị -keep class **.R$* { *; } — nó bảo tồn tất cả các định danh của lớp R.
<!-- Ví dụ: tài nguyên chỉ được sử dụng qua getIdentifier() -->
<string name="dynamic_title_welcome">Chào mừng</string>
<string name="dynamic_title_share">Chia sẻ</string>
<!-- Mã Kotlin truy cập bằng chuỗi -->
<!-- val title = getString(resources.getIdentifier( -->
<!-- \"dynamic_title_${type}\", \"string\", packageName)) -->Trong trường hợp này, R8 không thấy tham chiếu tĩnh đến dynamic_title_welcome trong lớp R vì quyền truy cập thông qua getIdentifier với tên động. Để bảo tồn các tài nguyên như vậy, hãy thêm chỉ thị -keepclassmembers class **.R$string { *; } vào proguard-rules.pro — nó ngăn chặn việc xóa bất kỳ trường nào khỏi tất cả các lớp R$string.
| Chỉ thị | Mục đích | Ví dụ |
|---|---|---|
| -keep | Bảo tồn lớp và tất cả các thành viên của nó | -keep class com.example.api.** { *; } |
| -keepclassmembers | Chỉ bảo tồn các thành viên của lớp | -keepclassmembers class * { @SerializedName <fields>; } |
| -keepattributes | Bảo tồn siêu dữ liệu bytecode | -keepattributes *Annotation*, Signature |
| -assumenosideeffects | Loại bỏ các cuộc gọi không có tác dụng phụ | -assumenosideeffects class Log { d(...); } |
| -dontwarn | Bỏ qua cảnh báo | -dontwarn com.example.legacy.** |
Mặc dù R8 là phiên bản kế thừa của ProGuard, nhưng có những khác biệt cơ bản giữa các công cụ về kiến trúc, hiệu suất và hành vi. Google đã chính thức ngừng hỗ trợ ProGuard trong Android Gradle Plugin từ AGP 7.0, nhưng ProGuard vẫn tiếp tục được sử dụng trong các dự án yêu cầu hành vi tối ưu hóa cụ thể không có trong R8.
| Đặc điểm | ProGuard | R8 |
|---|---|---|
| Nhà phát triển | GuardSquare (Eric Lafarge) | |
| Năm phát hành | 2002 | 2018 (ổn định năm 2019) |
| Kiến trúc | 4 giai đoạn riêng biệt (shrink → optimize → obfuscate → preverify) | Một lần: shrink + optimize + obfuscate đồng thời |
| Tích hợp AGP | Công cụ bên ngoài, chạy sau javac | Tích hợp trong trình biên dịch D8 DEX |
| Tốc độ build | Chậm hơn 2–3 lần | Nhanh hơn nhờ một lần và tích hợp gốc |
| Hỗ trợ Kotlin | Hạn chế (vấn đề với inline, lambdas, coroutines) | Đầy đủ: coroutines, hàm inline, data class |
| Tệp ánh xạ | mapping.txt (tương thích với retrace) | mapping.txt (cùng định dạng) |
| Tùy chỉnh tối ưu hóa | 60+ tùy chọn -optimizationpasses, -optimizations | Hạn chế: hầu hết tối ưu hóa được bật mặc định |
| Trạng thái hỗ trợ | Đã được thay thế bằng R8 (AGP 7.0+ không sử dụng) | Đang phát triển tích cực, một phần của AOSP |
R8 tích cực hơn ProGuard trong việc loại bỏ mã mà nó cho là chết. Điều này dẫn đến các tình huống trong đó bản build gỡ lỗi hoạt động nhưng bản build phát hành bị lỗi với ClassNotFoundException hoặc NoSuchMethodException. Các trường hợp điển hình: thư viện sử dụng reflection theo tên lớp (Gson, Moshi, Retrofit, Room, Dagger); cuộc gọi ServiceLoader hoặc java.util.ServiceLoader; proxy động (java.lang.reflect.Proxy); phương thức gốc (JNI). Giải pháp là thêm -keep cho tất cả các lớp được gọi thông qua reflection.
# Vấn đề reflection điển hình — R8 không thấy liên kết tĩnh
# Room — bảo tồn DAO và di chuyển
-keep class * extends androidx.room.RoomDatabase { *; }
-keep class *.DatabaseMigrations { *; }
# Dagger / Hilt — bảo tồn các thành phần
-keep class * extends dagger.hilt.android.components.** { *; }
# JNI — không đổi tên phương thức gốc
-keepclasseswithmembernames class * {
native <methods>;
}
# Data Binding — bảo tồn các lớp Binding
-keep class *.databinding.** { *; }Nếu sau khi thêm các quy tắc mà build vẫn bị lỗi, hãy sử dụng cờ -printconfiguration full-config.txt trong proguard-rules.pro. R8 sẽ tạo một tệp cấu hình hoàn chỉnh hiển thị các quy tắc được áp dụng và các lớp được bảo tồn. Chỉ thị -whyareyoukeeping class com.example.MyClass cũng hữu ích — nó hiển thị lý do tại sao R8 quyết định bảo tồn lớp cụ thể đó.
Cấu hình đúng cách của ProGuard rules là chìa khóa để mã hóa ổn định mà không có lỗi thời gian chạy. Dưới đây là quy trình thiết lập từng bước cho một dự án mới hoặc cho một dự án nơi mã hóa gây ra lỗi.
Bắt đầu bằng cách bao gồm tệp SDK Android tiêu chuẩn — proguard-android-optimize.txt. Nó chứa các quy tắc cho các thành phần Android cơ bản: Activity, Service, BroadcastReceiver, ContentProvider, View, Fragment. Tệp này nằm trong thư mục SDK: $ANDROID_HOME/tools/proguard/proguard-android-optimize.txt. Nếu bạn sử dụng AGP, getDefaultProguardFile sẽ tự động tải nó.
Mỗi thư viện phổ biến đều có các quy tắc ProGuard được khuyến nghị. Retrofit, OkHttp, Glide, Fresco, Coil, Room, Dagger/Hilt, Kotlin Coroutines — tất cả đều yêu cầu các quy tắc -keep cụ thể. Thông thường, các quy tắc được bao gồm trong thư viện AAR và được liên kết tự động thông qua các quy tắc bảo vệ người tiêu dùng. Kiểm tra xem thư viện có cung cấp tệp proguard.txt bên trong AAR không — điều này cho thấy các quy tắc đã được tính đến.
Trước khi xuất bản, hãy đảm bảo kiểm tra bản build phát hành trên thiết bị thực hoặc trình giả lập. Các vấn đề mã hóa chỉ xuất hiện trong thời gian chạy. Kiểm tra: xác thực (đăng nhập/đăng ký), tải dữ liệu từ mạng, điều hướng giữa các màn hình, máy ảnh và thư viện ảnh, thông báo đẩy, Deeplinks, WebView. Mỗi sự cố trong bản build phát hành cần được giải mã bằng retrace với tệp ánh xạ và thêm các quy tắc -keep còn thiếu.
Tệp ánh xạ được tạo tại build/outputs/mapping/release/mapping.txt. Tệp này phải được lưu giữ: nếu không có nó, không thể giải mã nhật ký sự cố từ Google Play Console. Bao gồm mapping.txt trong hệ thống kiểm soát phiên bản của bạn hoặc tải nó lên dưới dạng tạo phẩm CI. Google Play Console chấp nhận tệp ánh xạ tự động khi tải lên AAB với uploading mapping.txt được bật.
Dưới đây là quy trình thiết lập mã hóa hoàn chỉnh trong tệp proguard-rules.pro với các chú thích cho từng nhóm quy tắc.
# ===========================================
# proguard-rules.pro — ví dụ đầy đủ
# ===========================================
# --- Cài đặt chung ---
-keepattributes *Annotation*, Signature, Exceptions, InnerClasses, EnclosingMethod
-dontpreverify
# --- Thành phần Android ---
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.Fragment
-keep public class * extends androidx.fragment.app.Fragment
-keep public class * extends android.view.View
# --- OkHttp / Retrofit ---
-dontwarn okhttp3.**
-dontwarn okio.**
-keep class retrofit2.** { *; }
-keepattributes Exceptions
# --- Gson / Moshi ---
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-keep class com.google.gson.** { *; }
# --- Firebase ---
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
# --- Kotlin Coroutines ---
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
# --- Tuần tự hóa ---
-keepclassmembers class * implements java.io.Serializable {
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# --- Chỉ R8: bảo tồn bắt buộc ---
# (ProGuard bỏ qua chỉ thị này)
-keep,allowobfuscation class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator CREATOR;
}Sau khi cấu hình, chạy build: ./gradlew assembleRelease. Xác minh rằng các tệp đã xuất hiện trong build/outputs/mapping/release/: mapping.txt (ánh xạ tên gốc sang tên đã mã hóa), seeds.txt (các lớp được bảo tồn bởi quy tắc -keep), usage.txt (các lớp bị xóa trong quá trình thu nhỏ). Kích thước APK sau khi mã hóa sẽ giảm 20–50% tùy thuộc vào số lượng thư viện được kết nối.
Câu hỏi Thường gặp
R8 là phiên bản kế thừa của ProGuard, được Google phát triển. R8 thực hiện mã hóa, thu nhỏ và tối ưu hóa trong một lần, nhanh hơn 2–3 lần so với ProGuard và được tích hợp trực tiếp vào Android Gradle Plugin. ProGuard sử dụng bốn giai đoạn riêng biệt và yêu cầu thực thi bên ngoài. Từ AGP 7.0, ProGuard không được sử dụng — R8 hoạt động mặc định.
Có, R8 sử dụng cùng ProGuard rules (tệp .pro). Các chỉ thị -keep, -keepclassmembers, -keepattributes, -assumenosideeffects hoạt động giống hệt nhau. Các quy tắc cơ bản có trong proguard-android-optimize.txt từ Android SDK, trong khi các quy tắc dành riêng cho thư viện (Retrofit, Room, Gson) được thêm vào proguard-rules.pro của dự án. Nếu không có các quy tắc này, R8 có thể xóa các lớp cần thiết cho các thư viện hoạt động thông qua reflection.
R8 được bật mặc định trong Android Gradle Plugin từ AGP 3.4. Để kích hoạt thu nhỏ, đặt isMinifyEnabled = true trong khối release buildType của tệp build.gradle.kts. Cờ bổ sung isShrinkResources = true bật loại bỏ tài nguyên không sử dụng. Trong gradle.properties, bạn có thể buộc tắt R8 qua android.enableR8=false, nhưng điều này không được khuyến nghị — R8 nhanh hơn và ổn định hơn.
Mã hóa — đổi tên các lớp, phương thức và trường thành các tên ngắn vô nghĩa (a, b, c). Lớp com.example.app.auth.LoginManager trở thành a.a.a, phương thức authenticateUser trở thành a. Điều này làm phức tạp việc đảo ngược ứng dụng nhưng không ảnh hưởng đến logic thực thi. ProGuard và R8 chỉ đổi tên các phần tử không được bảo vệ bởi quy tắc -keep. Tệp ánh xạ bảo tồn sự tương ứng của tên gốc và tên đã mã hóa để giải mã nhật ký sự cố.
Để giải mã dấu vết ngăn xếp, hãy sử dụng tiện ích retrace (một phần của ProGuard/R8 SDK). Lệnh: retrace mapping.txt crash-stacktrace.txt. Tệp ánh xạ nằm tại build/outputs/mapping/release/mapping.txt. Google Play Console cũng hỗ trợ tải lên mapping.txt khi xuất bản AAB — nhật ký sự cố được tự động giải mã trong bảng điều khiển. Nếu không có tệp ánh xạ, dấu vết ngăn xếp sẽ chỉ chứa các tên đã mã hóa như a.b.c(), vô dụng cho việc gỡ lỗi.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm