Các trường hợp sử dụng và ví dụ về quy tắc giữ lại

Các ví dụ sau đây dựa trên các trường hợp phổ biến mà bạn sử dụng R8 để tối ưu hoá, nhưng cần hướng dẫn nâng cao để soạn quy tắc giữ lại.

Phản chiếu

Nhìn chung, để có hiệu suất tối ưu, bạn không nên sử dụng tính năng phản chiếu. Tuy nhiên, trong một số trường hợp, bạn có thể không tránh khỏi việc sử dụng tính năng này. Các ví dụ sau đây cung cấp hướng dẫn về quy tắc giữ lại trong các trường hợp phổ biến sử dụng tính năng phản chiếu.

Phản chiếu với các lớp được tải theo tên

Thư viện thường tải các lớp một cách linh hoạt bằng cách sử dụng tên lớp làm String. Tuy nhiên, R8 không thể phát hiện các lớp được tải theo cách này và có thể xoá các lớp mà R8 cho là không sử dụng.

Ví dụ: hãy xem xét trường hợp sau đây, trong đó bạn có một thư viện và một ứng dụng sử dụng thư viện đó. Đoạn mã này minh hoạ một trình tải thư viện tạo thực thể cho giao diện StartupTask do một ứng dụng triển khai.

Sau đây là mã thư viện:

// The interface for a task that runs once.
interface StartupTask {
    fun run()
}

// The library object that loads and executes the task.
object TaskRunner {
    fun execute(className: String) {
        // R8 won't retain classes specified by this string value at runtime
        val taskClass = Class.forName(className)
        val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask
        task.run()
    }
}

Ứng dụng sử dụng thư viện có mã sau:

// The app's task to pre-cache data.
// R8 will remove this class because it's only referenced by a string.
class PreCacheTask : StartupTask {
    override fun run() {
        // This log will never appear if the class is removed by R8.
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is told to run the app's task by its name.
    TaskRunner.execute("com.example.app.PreCacheTask")
}

Trong trường hợp này, thư viện của bạn phải có tệp quy tắc giữ lại của đối tượng sử dụng với các quy tắc giữ lại sau:

-keep class * implements com.example.library.StartupTask {
    <init>();
}

Nếu không có quy tắc này, R8 sẽ xoá PreCacheTask khỏi ứng dụng vì ứng dụng không sử dụng trực tiếp lớp này, làm hỏng quá trình tích hợp. Quy tắc này tìm các lớp triển khai giao diện StartupTask của thư viện và giữ lại các lớp đó cùng với hàm khởi tạo không có đối số, cho phép thư viện tạo thực thể và thực thi PreCacheTask thành công.

Phản chiếu với ::class.java

Thư viện có thể tải các lớp bằng cách yêu cầu ứng dụng chuyển trực tiếp đối tượng Class. Đây là một phương thức mạnh mẽ hơn so với việc tải các lớp theo tên. Phương thức này tạo một tham chiếu mạnh mẽ đến lớp mà R8 có thể phát hiện. Tuy nhiên, mặc dù phương thức này ngăn R8 xoá lớp, nhưng bạn vẫn cần sử dụng quy tắc giữ lại để khai báo rằng lớp này được tạo thực thể một cách phản chiếu và để bảo vệ các thành phần được truy cập một cách phản chiếu, chẳng hạn như hàm khởi tạo.

Ví dụ: hãy xem xét trường hợp sau đây, trong đó bạn có một thư viện và một ứng dụng sử dụng thư viện đó. Trình tải thư viện tạo thực thể cho giao diện StartupTask bằng cách chuyển trực tiếp tham chiếu lớp.

Sau đây là mã thư viện:

// The interface for a task that runs once.
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()
    }
}

Ứng dụng sử dụng thư viện có mã sau:

// The app's task is to pre-cache data.
class PreCacheTask : StartupTask {
    override fun run() {
        Log.d("AppTask", "Warming up the cache...")
    }
}

fun onCreate() {
    // The library is given a direct reference to the app's task class.
    TaskRunner.execute(PreCacheTask::class.java)
}

Trong trường hợp này, thư viện của bạn phải có tệp quy tắc giữ lại của đối tượng sử dụng với các quy tắc giữ lại sau:

# Allow any implementation of StartupTask to be removed if unused.
-keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask
# Keep the default constructor, which is called via reflection.
-keepclassmembers class * implements com.example.library.StartupTask {
    <init>();
}

Các quy tắc này được thiết kế để hoạt động hoàn hảo với loại phản chiếu này, cho phép tối ưu hoá tối đa trong khi vẫn đảm bảo rằng mã hoạt động chính xác. Các quy tắc này cho phép R8 làm rối mã nguồn tên lớp và rút gọn hoặc xoá quá trình triển khai lớp StartupTask nếu ứng dụng không bao giờ sử dụng lớp này. Tuy nhiên, đối với mọi quá trình triển khai, chẳng hạn như PrecacheTask được sử dụng trong ví dụ, các quy tắc này sẽ giữ lại hàm khởi tạo mặc định (<init>()) mà thư viện của bạn cần gọi.

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: Quy tắc này nhắm đến mọi lớp triển khai giao diện StartupTask.
    • -keep class * implements com.example.library.StartupTask: Quy tắc này giữ lại mọi lớp (*) triển khai giao diện của bạn.
    • ,allowobfuscation: Quy tắc này hướng dẫn R8 rằng mặc dù giữ lại lớp, nhưng R8 có thể đổi tên hoặc làm rối mã nguồn lớp đó. Điều này là an toàn vì thư viện của bạn không dựa vào tên của lớp; thư viện này nhận trực tiếp đối tượng Class.
    • ,allowshrinking: Đối tượng sửa đổi này hướng dẫn R8 rằng R8 có thể xoá lớp nếu lớp đó không được sử dụng. Điều này giúp R8 xoá một cách an toàn quá trình triển khai StartupTask không bao giờ được chuyển đến TaskRunner.execute(). Tóm lại, quy tắc này ngụ ý như sau: Nếu một ứng dụng sử dụng một lớp triển khai StartupTask, thì R8 sẽ giữ lại lớp đó. R8 có thể đổi tên lớp để giảm kích thước và có thể xoá lớp đó nếu ứng dụng không sử dụng.
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: Quy tắc này nhắm đến các thành phần cụ thể của các lớp đã được xác định trong quy tắc đầu tiên – trong trường hợp này là hàm khởi tạo.
    • -keepclassmembers class * implements com.example.library.StartupTask: Quy tắc này giữ lại các thành phần cụ thể (phương thức, trường) của lớp triển khai giao diện StartupTask, nhưng chỉ khi chính lớp được triển khai đó đang được giữ lại.
    • { <init>(); }: Đây là bộ chọn thành phần. <init> là tên nội bộ đặc biệt cho hàm khởi tạo trong mã byte Java. Phần này nhắm đến cụ thể hàm khởi tạo mặc định, không có đối số.
    • Quy tắc này rất quan trọng vì mã của bạn gọi getDeclaredConstructor().newInstance() mà không có đối số nào, phản ánh việc gọi hàm khởi tạo mặc định. Nếu không có quy tắc này, R8 sẽ thấy rằng không có mã nào gọi trực tiếp new PreCacheTask(), giả định rằng hàm khởi tạo không được sử dụng và xoá hàm khởi tạo đó. Điều này khiến ứng dụng của bạn gặp sự cố trong thời gian chạy với InstantiationException.

Phản chiếu dựa trên chú thích phương thức

Thư viện thường xác định các chú thích mà nhà phát triển sử dụng để gắn thẻ các phương thức hoặc trường. Sau đó, thư viện sử dụng tính năng phản chiếu để tìm các thành phần được chú thích này trong thời gian chạy. Ví dụ: chú thích @OnLifecycleEvent được dùng để tìm các phương thức cần thiết trong thời gian chạy.

Ví dụ: hãy xem xét trường hợp sau đây, trong đó bạn có một thư viện và một ứng dụng sử dụng thư viện đó. Ví dụ này minh hoạ một bus sự kiện tìm và gọi các phương thức được chú thích bằng @OnEvent.

Sau đây là mã thư viện:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class OnEvent

class EventBus {
    fun dispatch(listener: Any) {
        // Find all methods annotated with @OnEvent and invoke them
        listener::class.java.declaredMethods.forEach { method ->
            if (method.isAnnotationPresent(OnEvent::class.java)) {
                try {
                    method.invoke(listener)
                } catch (e: Exception) { /* ... */ }
            }
        }
    }
}

Ứng dụng sử dụng thư viện có mã sau:

class MyEventListener {
    @OnEvent
    fun onSomethingHappened() {
        // This method will be removed by R8 without a keep rule
        Log.d(TAG, "Event received!")
    }
}

fun onCreate() {
    // Instantiate the listener and the event bus
    val listener = MyEventListener()
    val eventBus = EventBus()

    // Dispatch the listener to the event bus
    eventBus.dispatch(listener)
}

Thư viện phải có tệp quy tắc giữ lại của đối tượng sử dụng, tự động giữ lại mọi phương thức sử dụng chú thích của thư viện đó:

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: Quy tắc này giữ lại các chú thích được dùng để đọc trong thời gian chạy.
  • -keep @interface com.example.library.OnEvent: Quy tắc này giữ lại chính lớp chú thích OnEvent.
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: Quy tắc này chỉ giữ lại một lớp và các thành phần cụ thể nếu lớp đó đang được sử dụng và chứa các thành phần đó.
    • -keepclassmembers: Quy tắc này chỉ giữ lại một lớp và các thành phần cụ thể nếu lớp đó đang được sử dụng và chứa các thành phần đó.
    • class *: Quy tắc này áp dụng cho mọi lớp.
    • @com.example.library.OnEvent <methods>;: Quy tắc này giữ lại mọi lớp có một hoặc nhiều phương thức (<methods>) được chú thích bằng @com.example.library.OnEvent, đồng thời giữ lại chính các phương thức được chú thích.

Phản chiếu dựa trên chú thích lớp

Thư viện có thể sử dụng tính năng phản chiếu để quét các lớp có chú thích cụ thể. Trong trường hợp này, lớp trình chạy tác vụ sẽ tìm tất cả các lớp được chú thích bằng ReflectiveExecutor bằng cách sử dụng tính năng phản chiếu và thực thi phương thức execute.

Ví dụ: hãy xem xét trường hợp sau đây, trong đó bạn có một thư viện và một ứng dụng sử dụng thư viện đó.

Thư viện có mã sau:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class ReflectiveExecutor

class TaskRunner {
    fun process(task: Any) {
        val taskClass = task::class.java
        if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) {
            val methodToCall = taskClass.getMethod("execute")
            methodToCall.invoke(task)
        }
    }
}

Ứng dụng sử dụng thư viện có mã sau:

// In consumer app

@ReflectiveExecutor
class ImportantBackgroundTask {
    fun execute() {
        // This class will be removed by R8 without a keep rule
        Log.e("ImportantBackgroundTask", "Executing the important background task...")
    }
}

// Usage of ImportantBackgroundTask

fun onCreate(){
    val task = ImportantBackgroundTask()
    val runner = TaskRunner()
    runner.process(task)
}

Vì thư viện sử dụng tính năng phản chiếu để lấy các lớp cụ thể, nên thư viện phải có tệp quy tắc giữ lại của đối tượng sử dụng với các quy tắc giữ lại sau:

# Retain annotation metadata for runtime reflection.
-keepattributes RuntimeVisibleAnnotations

# Keep the annotation interface itself.
-keep @interface com.example.library.ReflectiveExecutor

# Keep the execute method in the classes which are being used
-keepclassmembers @com.example.library.ReflectiveExecutor class * {
   public void execute();
}

Cấu hình này rất hiệu quả vì cho R8 biết chính xác những gì cần giữ lại.

Phản chiếu để hỗ trợ các phần phụ thuộc không bắt buộc

Một trường hợp sử dụng phổ biến cho tính năng phản chiếu là tạo phần phụ thuộc mềm giữa thư viện cốt lõi và thư viện bổ trợ không bắt buộc. Thư viện cốt lõi có thể kiểm tra xem ứng dụng có chứa thư viện bổ trợ hay không và nếu có, thì có thể bật các tính năng bổ sung. Điều này cho phép bạn vận chuyển các mô-đun bổ trợ mà không buộc thư viện cốt lõi phải có phần phụ thuộc trực tiếp vào các mô-đun đó.

Thư viện cốt lõi sử dụng tính năng phản chiếu (Class.forName) để tìm một lớp cụ thể theo tên. Nếu tìm thấy lớp đó, tính năng này sẽ được bật. Nếu không, tính năng này sẽ không hoạt động.

Ví dụ: hãy xem xét đoạn mã sau đây, trong đó AnalyticsManager cốt lõi kiểm tra lớp VideoEventTracker không bắt buộc để bật tính năng phân tích video.

Thư viện cốt lõi có mã sau:

object AnalyticsManager {
    private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker"

    fun initialize() {
        try {
            // Attempt to load the optional module's class using reflection
            Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance()
            Log.d(TAG, "Video tracking enabled.")
        } catch (e: ClassNotFoundException) {
            Log.d(TAG,"Video tracking module not found. Skipping.")
        } catch (e: Exception) {
            Log.e(TAG, e.printStackTrace())
        }
    }
}

Thư viện video không bắt buộc có mã sau:

package com.example.analytics.video

class VideoEventTracker {
    // This constructor must be kept for the reflection call to succeed.
    init { /* ... */ }
}

Nhà phát triển thư viện không bắt buộc chịu trách nhiệm cung cấp quy tắc giữ lại cần thiết cho đối tượng sử dụng. Quy tắc giữ lại này đảm bảo rằng mọi ứng dụng sử dụng thư viện không bắt buộc đều giữ lại mã mà thư viện cốt lõi cần tìm.

# In the video library's consumer keep rules file
-keep class com.example.analytics.video.VideoEventTracker {
    <init>();
}

Nếu không có quy tắc này, R8 có thể sẽ xoá VideoEventTracker khỏi thư viện không bắt buộc vì không có gì trong mô-đun đó sử dụng trực tiếp. Quy tắc giữ lại sẽ giữ lại lớp và hàm khởi tạo của lớp đó, cho phép thư viện cốt lõi tạo thực thể thành công.

Phản chiếu để truy cập các thành phần riêng tư

Việc sử dụng tính năng phản chiếu để truy cập mã riêng tư hoặc được bảo vệ không thuộc API công khai của thư viện có thể gây ra các vấn đề đáng kể. Mã như vậy có thể thay đổi mà không cần thông báo trước, điều này có thể dẫn đến hành vi ngoài dự kiến hoặc sự cố trong ứng dụng của bạn.

Khi dựa vào tính năng phản chiếu cho các API không công khai, bạn có thể gặp phải các vấn đề sau:

  • Bản cập nhật bị chặn: Các thay đổi trong mã riêng tư hoặc được bảo vệ có thể ngăn bạn cập nhật lên các phiên bản thư viện cao hơn.
  • Bỏ lỡ lợi ích: Bạn có thể bỏ lỡ chức năng mới, các bản sửa lỗi quan trọng hoặc các bản cập nhật bảo mật thiết yếu.

Tối ưu hoá R8 và phản chiếu

Nếu bạn phải phản ánh vào mã riêng tư hoặc được bảo vệ của thư viện, hãy chú ý đến các hoạt động tối ưu hoá của R8. Nếu không có tham chiếu trực tiếp đến các thành phần này, R8 có thể giả định rằng các thành phần này không được sử dụng và sau đó xoá hoặc đổi tên các thành phần đó. Điều này có thể dẫn đến sự cố trong thời gian chạy, thường có các thông báo lỗi gây hiểu lầm như NoSuchMethodException hoặc NoSuchFieldException.

Ví dụ: hãy xem xét trường hợp sau đây minh hoạ cách bạn có thể truy cập vào một trường riêng tư từ lớp thư viện.

Một thư viện mà bạn không sở hữu có mã sau:

class LibraryClass {
    private val secretMessage = "R8 will remove me"
}

Ứng dụng của bạn có mã sau:

fun accessSecretMessage(instance: LibraryClass) {
    // Use Java reflection from Kotlin to access the private field
    val secretField = instance::class.java.getDeclaredField("secretMessage")
    secretField.isAccessible = true
    // This will crash at runtime with R8 enabled
    val message = secretField.get(instance) as String
}

Thêm quy tắc -keep vào ứng dụng của bạn để ngăn R8 xoá trường riêng tư:

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: Quy tắc này chỉ giữ lại các thành phần cụ thể của một lớp nếu chính lớp đó được giữ lại.
  • class com.example.LibraryClass: Quy tắc này nhắm đến chính xác lớp chứa trường.
  • private java.lang.String secretMessage;: Quy tắc này xác định trường riêng tư cụ thể theo tên và loại.

Giao diện gốc của Java (JNI)

Các hoạt động tối ưu hoá của R8 có thể gặp vấn đề khi làm việc với các lệnh gọi lên từ mã gốc (mã C/C++) đến Java hoặc Kotlin. Mặc dù điều ngược lại cũng đúng – các lệnh gọi xuống từ Java hoặc Kotlin đến mã gốc có thể gặp vấn đề – tệp mặc định proguard-android-optimize.txt bao gồm quy tắc sau để giữ cho các lệnh gọi xuống hoạt động. Quy tắc này bảo vệ các phương thức gốc khỏi bị cắt bớt.

-keepclasseswithmembernames,includedescriptorclasses class * {
  native <methods>;
}

Tương tác với mã gốc thông qua Giao diện gốc của Java (JNI)

Khi ứng dụng của bạn sử dụng JNI để thực hiện các lệnh gọi lên từ mã gốc (C/C++) đến Java hoặc Kotlin, R8 không thể biết phương thức nào được gọi từ mã gốc của bạn. Nếu không có tham chiếu trực tiếp đến các phương thức này trong ứng dụng của bạn, R8 sẽ giả định không chính xác rằng các phương thức này không được sử dụng và xoá các phương thức đó, khiến ứng dụng của bạn gặp sự cố.

Ví dụ sau đây cho thấy một lớp Kotlin có phương thức dự định được gọi từ thư viện gốc. Thư viện gốc tạo thực thể cho loại ứng dụng và chuyển dữ liệu từ mã gốc sang mã Kotlin.

package com.example.models

// This class is used in the JNI bridge method signature
data class NativeData(val id: Int, val payload: String)
package com.example.app
// In package com.example.app
class JniBridge {
    /**
     *   This method is called from the native side.
     *   R8 will remove it if it's not kept.
     */
    fun onNativeEvent(data: NativeData) {
        Log.d(TAG, "Received event from native code: $data")
    }
    // Use 'external' to declare a native method
    external fun startNativeProcess()

    companion object {
        init {
            // Load the native library
            System.loadLibrary("my-native-lib")
        }
    }
}

Trong trường hợp này, bạn phải thông báo cho R8 để ngăn loại ứng dụng được tối ưu hoá. Ngoài ra, nếu các phương thức được gọi từ mã gốc sử dụng các lớp riêng của bạn trong chữ ký làm tham số hoặc loại trả về, bạn cũng phải xác minh rằng các lớp đó không được đổi tên.

Thêm các quy tắc giữ lại sau vào ứng dụng của bạn:

-keepclassmembers,includedescriptorclasses class com.example.JniBridge {
    public void onNativeEvent(com.example.model.NativeData);
}

-keep class NativeData{
        <init>(java.lang.Integer, java.lang.String);
}

Các quy tắc giữ lại này ngăn R8 xoá hoặc đổi tên phương thức onNativeEvent và – quan trọng nhất – loại tham số của phương thức đó.

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: Quy tắc này chỉ giữ lại các thành phần cụ thể của một lớp nếu lớp đó được tạo thực thể trước tiên trong mã Kotlin hoặc Java – quy tắc này cho R8 biết rằng ứng dụng đang sử dụng lớp đó và R8 nên giữ lại các thành phần cụ thể của lớp đó.
    • -keepclassmembers: Quy tắc này chỉ giữ lại các thành phần cụ thể của một lớp nếu lớp đó được tạo thực thể trước tiên trong mã Kotlin hoặc Java – quy tắc này cho R8 biết rằng ứng dụng đang sử dụng lớp đó và R8 nên giữ lại các thành phần cụ thể của lớp đó.
    • class com.example.JniBridge: Quy tắc này nhắm đến chính xác lớp chứa trường.
    • includedescriptorclasses: Đối tượng sửa đổi này cũng giữ lại mọi lớp được tìm thấy trong chữ ký hoặc bộ mô tả của phương thức. Trong trường hợp này, đối tượng sửa đổi này ngăn R8 đổi tên hoặc xoá lớp com.example.models.NativeData được dùng làm tham số. Nếu NativeData được đổi tên (ví dụ: thành a.a), chữ ký phương thức sẽ không còn khớp với những gì mã gốc mong đợi, gây ra sự cố.
    • public void onNativeEvent(com.example.models.NativeData);: Quy tắc này chỉ định chính xác chữ ký Java của phương thức cần giữ lại.
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: Mặc dù includedescriptorclasses đảm bảo rằng chính lớp NativeData được giữ lại, nhưng mọi thành phần (trường hoặc phương thức) trong NativeData được truy cập trực tiếp từ mã JNI gốc của bạn đều cần có quy tắc giữ lại riêng.
    • -keep class NativeData: Quy tắc này nhắm đến lớp có tên là NativeData và khối này chỉ định các thành phần bên trong lớp NativeData cần giữ lại.
    • <init>(java.lang.Integer, java.lang.String): Đây là chữ ký của hàm khởi tạo. Chữ ký này xác định duy nhất hàm khởi tạo nhận 2 tham số: tham số đầu tiên là Integer và tham số thứ hai là String.

Lệnh gọi nền tảng gián tiếp

Chuyển dữ liệu bằng cách triển khai Parcelable

Khung Android sử dụng tính năng phản chiếu để tạo thực thể cho các đối tượng Parcelable. Trong quá trình phát triển Kotlin hiện đại, bạn nên sử dụng trình bổ trợ kotlin-parcelize, trình bổ trợ này sẽ tự động tạo quá trình triển khai Parcelable cần thiết, bao gồm cả trường CREATOR và các phương thức mà khung cần.

Ví dụ: hãy xem xét ví dụ sau đây, trong đó trình bổ trợ kotlin-parcelize được dùng để tạo lớp Parcelable:

import android.os.Parcelable
import kotlinx.parcelize.Parcelize

// Add the @Parcelize annotation to your data class
@Parcelize
data class UserData(
    val name: String,
    val age: Int
) : Parcelable

Trong trường hợp này, không có quy tắc giữ lại được đề xuất. Trình bổ trợ Gradle kotlin-parcelize sẽ tự động tạo các quy tắc giữ lại cần thiết cho các lớp mà bạn chú thích bằng @Parcelize. Trình bổ trợ này xử lý sự phức tạp cho bạn, đảm bảo rằng các hàm khởi tạo và CREATOR được tạo sẽ được giữ lại cho các lệnh gọi phản chiếu của khung Android.

Nếu bạn viết lớp Parcelable theo cách thủ công trong Kotlin mà không sử dụng @Parcelize, bạn có trách nhiệm giữ lại trường CREATOR và hàm khởi tạo chấp nhận Parcel. Việc quên thực hiện điều này sẽ khiến ứng dụng của bạn gặp sự cố khi hệ thống cố gắng huỷ tuần tự hoá đối tượng của bạn. Việc sử dụng @Parcelize là phương pháp tiêu chuẩn và an toàn hơn.

Khi sử dụng trình bổ trợ kotlin-parcelize, hãy lưu ý những điều sau:

  • Trình bổ trợ này tự động tạo các trường CREATOR trong quá trình biên dịch.
  • Tệp proguard-android-optimize.txt chứa các quy tắc keep cần thiết để giữ lại các trường này nhằm đảm bảo chức năng phù hợp.
  • Nhà phát triển ứng dụng phải xác minh rằng tất cả các quy tắc keep cần thiết đều có mặt, đặc biệt là đối với mọi quá trình triển khai tuỳ chỉnh hoặc phần phụ thuộc của bên thứ ba.

Các thư viện sử dụng tính năng phản chiếu hoặc chuyển đổi mã byte sẽ truy cập mã một cách linh hoạt trong thời gian chạy. Nếu R8 xoá hoặc đổi tên các lớp, trường hoặc phương thức được truy cập theo cách này, ứng dụng của bạn có thể gặp sự cố.

Tuy nhiên, các thư viện phổ biến của bên thứ ba (chẳng hạn như Gson, Retrofit và Kotlinx Serialization) sẽ tự động gói các quy tắc giữ lại của đối tượng sử dụng R8. Khi sử dụng các phiên bản gần đây của các thư viện này, bạn không cần thêm quy tắc giữ lại theo cách thủ công vào dự án của mình.

Gson

Gson là thư viện tuần tự hoá và huỷ tuần tự hoá JSON, dựa nhiều vào tính năng phản chiếu. Khi bạn sử dụng chế độ đầy đủ chức năng để tối ưu hoá ứng dụng, chế độ này sẽ loại bỏ các chữ ký loại chung, hàm khởi tạo mặc định và các trường không được chú thích, trừ phi được hướng dẫn rõ ràng.

Để đảm bảo các hàm Gson hoạt động chính xác, hãy thêm các quy tắc cụ thể để giữ lại các trường không tạm thời trong các lớp mô hình dữ liệu và giữ lại hệ phân cấp TypeToken:

# Preserve generic type information required for deserialization
-keepattributes Signature

# Keep all non-transient fields in your data model classes for reflection
-keepclassmembers class com.example.models.** {
    !transient <fields>;
}

# Keep TypeToken itself and any anonymous classes extending it
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken

Gson bỏ qua các trường được đánh dấu bằng đối tượng sửa đổi transient trong quá trình tuần tự hoá và huỷ tuần tự hoá. Đó là lý do quy tắc giữ lại nhắm đến cụ thể các trường không tạm thời (!transient).

Retrofit

Retrofit là thư viện mạng kiểm tra các phương thức giao diện dịch vụ được chú thích bằng chú thích HTTP (chẳng hạn như @GET hoặc @POST) bằng cách sử dụng tính năng phản chiếu để tạo yêu cầu mạng và chuyển đổi phản hồi.

Retrofit tạo linh hoạt các quá trình triển khai giao diện API của bạn trong thời gian chạy bằng cách sử dụng Proxy.newProxyInstance(). Vì R8 không thấy bất kỳ lớp nào triển khai các giao diện này một cách tĩnh, nên R8 có thể loại bỏ các phương thức hoặc loại trả về chung của các phương thức đó.

Quy tắc giữ lại được gói

Retrofit dựa vào tính năng phản chiếu trong thời gian chạy để kiểm tra các tham số chung, chú thích phương thức và chú thích tham số. Nếu không có cấu hình phù hợp, chế độ đầy đủ chức năng của R8 có thể loại bỏ hoàn toàn các chữ ký chung khỏi các loại trả về, phần tiếp theo của Kotlin và các lớp phản hồi, hoặc thậm chí thay thế các giá trị giao diện bằng giá trị rỗng vì các giao diện Retrofit được tạo thực thể một cách linh hoạt bằng proxy.

Kể từ Retrofit 2.10.0, thư viện này sẽ tự động gói các quy tắc giữ lại chính thức cần thiết để giữ lại các giá trị mặc định của chú thích, tham số phương thức dịch vụ và siêu dữ liệu lớp cần thiết. Để biết thêm thông tin, hãy xem Các quy tắc mà Retrofit sử dụng.

Giữ lại các loại trả về chung

Retrofit kiểm tra chữ ký chung của kiểu dữ liệu trả về (ví dụ: Observable<Data>) để huỷ tuần tự hoá chính xác phản hồi mạng. Nếu R8 loại bỏ chữ ký chung, Retrofit sẽ thay thế đối tượng được tạo thực thể bằng null.

Để ngăn chế độ đầy đủ chức năng của R8 loại bỏ chữ ký chung của các loại trả về, hãy sử dụng quy tắc có điều kiện sau:

# Preserve generic type information for Call/Observable return types
-keepattributes Signature

# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>)
-if interface * {
    @retrofit2.http.* public *** *(...);
}
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>

Lớp mô hình dữ liệu thực tế đang được trả về (ví dụ: Data trong Observable<Data>) cũng phải được giữ lại, vì lớp này sẽ được trình chuyển đổi (chẳng hạn như Gson) tạo một cách phản chiếu.

Coroutine

Khi bạn sử dụng coroutine Kotlin, trình biên dịch Kotlin sẽ chuyển đổi các hàm suspend bằng cách thêm tham số Continuation vào chữ ký phương thức đã biên dịch.

Khi các thư viện như Retrofit đọc một cách phản chiếu chữ ký chung của hàm suspend, các thư viện này sẽ dựa vào tham số Continuation đó. Khi sử dụng chế độ đầy đủ chức năng, thuộc tính Signature chỉ được giữ lại cho các lớp được giữ lại một cách rõ ràng. Vì Continuation là tham số tổng hợp, nên R8 sẽ loại bỏ chữ ký của tham số này theo mặc định, làm hỏng quá trình phản chiếu.

Để ngăn việc loại bỏ chữ ký và đảm bảo khả năng tương thích trong thời gian chạy ở chế độ đầy đủ chức năng, hãy đưa quy tắc sau vào:

# Keep the signature attribute globally
-keepattributes Signature

# Explicitly keep the Continuation class so its signature is not stripped
-keep class kotlin.coroutines.Continuation