R8 cung cấp hai chế độ: chế độ tương thích và chế độ đầy đủ. Chế độ đầy đủ mang đến cho bạn các hoạt động tối ưu hoá mạnh mẽ giúp cải thiện hiệu suất ứng dụng.
Hướng dẫn này dành cho những nhà phát triển Android muốn sử dụng các phương pháp tối ưu hoá mạnh mẽ nhất của R8. Tài liệu này khám phá những điểm khác biệt chính giữa chế độ tương thích và chế độ đầy đủ, đồng thời cung cấp các cấu hình rõ ràng cần thiết để di chuyển dự án một cách an toàn và tránh các sự cố phổ biến trong thời gian chạy.
Bật chế độ hoàn chỉnh
Để bật chế độ đầy đủ, hãy xoá dòng sau khỏi tệp gradle.properties:
android.enableR8.fullMode=false // Remove this line to enable full mode
Giữ lại các lớp được liên kết với các thuộc tính
Thuộc tính là siêu dữ liệu được lưu trữ trong các tệp lớp đã biên dịch không thuộc mã thực thi. Tuy nhiên, chúng có thể cần thiết cho một số loại phản chiếu. Các ví dụ thường gặp bao gồm Signature (giữ lại thông tin kiểu chung sau khi xoá kiểu), InnerClasses và EnclosingMethod (để phản ánh cấu trúc lớp) và chú thích có thể thấy trong thời gian chạy.
Đoạn mã sau đây cho thấy thuộc tính Signature trông như thế nào đối với một trường trong mã byte. Đối với một trường:
List<User> users;
Tệp lớp đã biên dịch sẽ chứa mã byte sau:
.field public static final users:Ljava/util/List;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/util/List<",
"Lcom/example/package/User;",
">;"
}
.end annotation
.end field
Các thư viện sử dụng nhiều tính năng phản chiếu (chẳng hạn như Gson) thường dựa vào những thuộc tính này để kiểm tra và tìm hiểu cấu trúc mã của bạn một cách linh hoạt. Theo mặc định ở chế độ đầy đủ của R8, các thuộc tính chỉ được giữ lại nếu lớp, trường hoặc phương thức được liên kết được giữ lại một cách rõ ràng.
Ví dụ sau đây minh hoạ lý do cần có các thuộc tính và những quy tắc giữ lại mà bạn cần thêm khi di chuyển từ chế độ tương thích sang chế độ đầy đủ.
Hãy xem xét ví dụ sau đây, trong đó chúng ta sẽ chuyển đổi một danh sách người dùng thành đối tượng bằng cách sử dụng thư viện Gson.
import com.google.gson.Gson
import 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. The JSON string for a list of users returned from remote
val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""
// 2. Deserialize the JSON string into a List<User>
// We must use TypeToken for generic types like List
val listType = object : TypeToken<List<User>>() {}.type
val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)
// Print the list
println("First user from list: ${deserializedList}")
}
Trong quá trình biên dịch, tính năng xoá kiểu của Java sẽ xoá các đối số kiểu chung. Điều này có nghĩa là trong thời gian chạy, cả List<String> và List<User> đều xuất hiện dưới dạng List thô. Do đó, các thư viện như Gson (dựa vào tính năng phản chiếu) không thể xác định các loại đối tượng cụ thể mà List được khai báo là chứa khi giải tuần tự hoá một danh sách JSON, điều này có thể dẫn đến các vấn đề về thời gian chạy.
Để giữ lại thông tin về loại, Gson sử dụng TypeToken. Thao tác bao bọc TypeToken sẽ giữ lại thông tin giải tuần tự hoá cần thiết.
Biểu thức Kotlin object:TypeToken<List<User>>() {}.type tạo một lớp bên trong ẩn danh mở rộng TypeToken và thu thập thông tin về kiểu chung. Trong ví dụ này, lớp ẩn danh có tên là $GsonRemoteJsonListExample$listType$1.
Ngôn ngữ lập trình Java lưu chữ ký chung của một siêu lớp dưới dạng siêu dữ liệu (còn gọi là thuộc tính Signature) trong tệp lớp đã biên dịch.
TypeToken sau đó sử dụng siêu dữ liệu Signature này để khôi phục loại trong thời gian chạy.
Điều này cho phép Gson sử dụng tính năng phản chiếu để đọc Signature và khám phá thành công loại List<User> đầy đủ mà nó cần để giải tuần tự hoá.
Khi R8 được bật ở chế độ tương thích, R8 sẽ giữ lại thuộc tính Signature cho các lớp, bao gồm cả các lớp bên trong ẩn danh như $GsonRemoteJsonListExample$listType$1, ngay cả khi bạn không xác định rõ các quy tắc giữ lại cụ thể. Do đó, chế độ tương thích R8 không yêu cầu thêm bất kỳ quy tắc giữ rõ ràng nào để ví dụ này hoạt động như mong đợi.
// keep rule for compatibility mode
-keepattributes Signature
Khi R8 được bật ở chế độ đầy đủ, thuộc tính Signature của lớp bên trong ẩn danh $GsonRemoteJsonListExample$listType$1 sẽ bị loại bỏ. Nếu không có thông tin về loại này trong Signature, Gson sẽ không tìm được loại ứng dụng chính xác, dẫn đến IllegalStateException. Các quy tắc lưu giữ cần thiết để ngăn chặn điều này là:
// keep rule required for 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: Quy tắc này hướng dẫn R8 giữ lại thuộc tính mà Gson cần đọc. Ở chế độ đầy đủ, R8 chỉ giữ lại thuộc tínhSignaturecho các lớp, trường hoặc phương thức được so khớp rõ ràng theo quy tắckeep.-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: Quy tắc này là cần thiết vìTypeTokenbao bọc loại đối tượng đang được chuyển đổi tuần tự. Sau khi xoá kiểu, một lớp bên trong ẩn danh sẽ được tạo để giữ lại thông tin kiểu chung. Nếu không giữ rõ ràngcom.google.gson.reflect.TypeToken, R8 ở chế độ đầy đủ sẽ không bao gồm loại lớp này trong thuộc tínhSignaturecần thiết cho quá trình chuyển đổi tuần tự.-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: Quy tắc này giữ lại thông tin về loại của các lớp ẩn danh mở rộngTypeToken, chẳng hạn như$GsonRemoteJsonListExample$listType$1trong ví dụ này. Nếu không có quy tắc này, R8 ở chế độ đầy đủ sẽ loại bỏ thông tin cần thiết về loại, khiến quá trình chuyển đổi tuần tự không thành công.
Bắt đầu từ Gson phiên bản 2.11.0, thư viện này gói các quy tắc giữ lại cần thiết để giải tuần tự hoá ở chế độ đầy đủ. Khi bạn tạo ứng dụng có bật R8, R8 sẽ tự động tìm và áp dụng các quy tắc này từ thư viện. Điều này giúp ứng dụng của bạn được bảo vệ mà bạn không cần phải thêm hoặc duy trì các quy tắc cụ thể này theo cách thủ công trong dự án.
Điều quan trọng là bạn phải hiểu rằng các quy tắc được chia sẻ trước đó chỉ giải quyết vấn đề khám phá loại chung (ví dụ: List<User>). R8 cũng đổi tên các trường của lớp. Nếu bạn không sử dụng chú thích @SerializedName trên các mô hình dữ liệu, Gson sẽ không thể chuyển đổi JSON thành đối tượng vì tên trường sẽ không còn khớp với các khoá JSON nữa.
Tuy nhiên, nếu đang sử dụng Gson phiên bản cũ hơn 2.11 hoặc nếu các mô hình của bạn không sử dụng chú giải @SerializedName, thì bạn phải thêm các quy tắc giữ lại rõ ràng cho những mô hình đó.
Giữ lại hàm khởi tạo mặc định
Ở chế độ đầy đủ của R8, hàm khởi tạo không có đối số/mặc định sẽ không được giữ lại một cách ngầm ẩn, ngay cả khi chính lớp đó được giữ lại. Nếu đang tạo một thực thể của lớp bằng class.getDeclaredConstructor().newInstance() hoặc class.newInstance(), bạn phải giữ lại hàm khởi tạo không có đối số một cách rõ ràng ở chế độ đầy đủ. Ngược lại, chế độ tương thích luôn giữ lại hàm khởi tạo không có đối số.
Hãy xem xét một ví dụ trong đó một thực thể của PrecacheTask được tạo bằng cách sử dụng tính năng phản chiếu để gọi phương thức run một cách linh động. Mặc dù trường hợp này không yêu cầu các quy tắc bổ sung ở chế độ tương thích, nhưng ở chế độ đầy đủ, hàm khởi tạo mặc định của PrecacheTask sẽ bị xoá. Do đó, bạn cần có một quy tắc giữ lại cụ thể.
// In library
interface StartupTask {
fun run()
}
// The library object that loads and executes the task.
object TaskRunner {
fun execute(taskClass: Class<out StartupTask>) {
// The class isn't removed, but its constructor might be.
val task = taskClass.getDeclaredConstructor().newInstance()
task.run()
}
}
// In app
class PreCacheTask : StartupTask {
override fun run() {
Log.d("Pre cache task", "Warming up the cache...")
}
}
fun runTaskRunner() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified
-keep class com.example.fullmoder8.PreCacheTask {
<init>();
}
Chế độ sửa đổi quyền truy cập được bật theo mặc định
Ở chế độ tương thích, R8 không thay đổi chế độ hiển thị của các phương thức và trường trong một lớp. Tuy nhiên, ở chế độ đầy đủ, R8 sẽ tăng cường hoạt động tối ưu hoá bằng cách thay đổi khả năng hiển thị của các phương thức và trường, ví dụ: từ riêng tư sang công khai. Điều này cho phép nhiều thao tác nội tuyến hơn.
Quá trình tối ưu hoá này có thể gây ra vấn đề nếu mã của bạn sử dụng tính năng phản chiếu và đặc biệt dựa vào các thành phần có khả năng hiển thị cụ thể. R8 sẽ không nhận ra cách sử dụng gián tiếp này, có thể dẫn đến sự cố ứng dụng. Để ngăn chặn điều này, bạn phải thêm các quy tắc -keep cụ thể để giữ lại các thành viên, đồng thời giữ nguyên khả năng hiển thị ban đầu của họ.
Để biết thêm thông tin, hãy xem ví dụ này để hiểu lý do bạn không nên truy cập vào các thành phần riêng tư bằng cách sử dụng tính năng phản chiếu và các quy tắc giữ lại để giữ lại những trường/phương thức đó.