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 có hướng dẫn nâng cao để soạn thảo các quy tắc giữ lại.

Phản chiếu

Nói chung, để đạt đượ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 được việc này. Các ví dụ sau đây cung cấp hướng dẫn về các quy tắc giữ lại trong những 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

Các thư viện thường tải các lớp một cách linh động 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à nó cho là không dùng đến.

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 đó – mã này minh hoạ một trình tải thư viện khởi tạo 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ó một tệp quy tắc lưu giữ người dùng với các quy tắc lưu giữ 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 gián đoạn quá trình tích hợp. Quy tắc này tìm thấy 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 khởi tạo và thực thi PreCacheTask thành công.

Phản chiếu bằng ::class.java

Các thư viện có thể tải các lớp bằng cách để ứng dụng truyề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. Thao tác này sẽ tạo một tham chiếu mạnh đến lớp mà R8 có thể phát hiện. Tuy nhiên, mặc dù điều 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 được khởi tạo 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 sẽ tạo một giao diện StartupTask bằng cách truyề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ó một tệp quy tắc lưu giữ người dùng với các quy tắc lưu giữ 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 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 hoạt động triển khai, chẳng hạn như PrecacheTask được dùng trong ví dụ, chúng 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 của bạn.
    • -keep class * implements com.example.library.StartupTask: Thao tác này sẽ giữ lại mọi lớp (*) triển khai giao diện của bạn.
    • ,allowobfuscation: Lệnh 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 phụ thuộc vào tên của lớp; thư viện sẽ trực tiếp nhận đối tượng Class.
    • ,allowshrinking: Sửa đổi này hướng dẫn R8 rằng nó có thể xoá lớp nếu lớp đó không được dùng. Điều này giúp R8 xoá một cách an toàn một phương thức triển khai của StartupTask mà không bao giờ được truyền đến TaskRunner.execute(). Nói tóm lại, quy tắc này ngụ ý rằng: 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 dùng đến.
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: Quy tắc này nhắm đến các thành viên cụ thể của những 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: Lệnh 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 đó được giữ lại.
    • { <init>(); }: Đây là bộ chọn thành viên. <init> là tên nội bộ đặc biệt cho một 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 đố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, điều này sẽ 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 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 một InstantiationException.

Phản ánh dựa trên chú giải phương thức

Các thư viện thường xác định những chú thích mà nhà phát triển dùng để gắn thẻ cho các phương thức hoặc trường. Sau đó, thư viện sẽ sử dụng tính năng phản chiếu để tìm những 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 bắt buộc 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ó một tệp quy tắc giữ lại người dùng tự động giữ lại mọi phương thức bằng cách sử dụng chú giải của phương thức đó:

-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 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 viên cụ thể nếu lớp đó đang được dùng và chứa những thành viê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 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 ánh dựa trên chú thích lớp

Các thư viện có thể sử dụng chế độ 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 này 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 chế độ phản chiếu để lấy các lớp cụ thể, nên thư viện phải có một 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 đây:

# 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ì nó cho R8 biết chính xác những gì cần giữ lại.

Phản ánh để 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 một 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 tiện ích bổ sung có được đưa vào ứng dụng hay không và nếu có, thư viện này 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ổ sung mà không bắt 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 chế độ phản chiếu (Class.forName) để tìm một lớp cụ thể theo tên của lớp đó. Nếu tìm thấy lớp học, tính năng này sẽ được bật. Nếu không, quá trình này sẽ thất bại một cách êm đẹp.

Ví dụ: hãy xem xét đoạn mã sau đây, trong đó một AnalyticsManager cốt lõi sẽ kiểm tra một 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 người dùng cần thiết. Quy tắc giữ 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ó nội dung nào trong mô-đun đó trực tiếp sử dụng VideoEventTracker. 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 khởi tạo thành công lớp đó.

Phản ánh để truy cập vào các thành viên riêng tư

Việc sử dụng tính năng phản chiếu để truy cập vào 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 đề nghiêm trọng. Mã như vậy có thể thay đổi mà không cần thông báo, đ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ể khiến bạn không cập nhật được 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 về sự cố hoặc các bản cập nhật bảo mật thiết yếu.

Các hoạt động tối ưu hoá và phản chiếu R8

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

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

Một thư viện không thuộc quyền sở hữu của bạn 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 để ngăn R8 xoá trường riêng tư:

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

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 xử lý 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 đề), nhưng tệp mặc định proguard-android-optimize.txt bao gồm quy tắc sau để duy trì hoạt động của các lệnh gọi xuống. Quy tắc này giúp ngăn chặn việc các phương thức gốc 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 Java (JNI)

Khi ứng dụng của bạn sử dụng JNI để thực hiện các lệnh gọi ngược từ mã gốc (C/C++) sang 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ó các tham chiếu trực tiếp đến những phương thức này trong ứng dụng của bạn, thì R8 sẽ giả định không chính xác rằng những phương thức này không được dùng và xoá chúng, 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ự kiến được gọi từ một thư viện gốc. Thư viện gốc khởi tạo một loại ứng dụng và truyề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ý dưới dạng tham số hoặc kiểu 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 lưu giữ sau đây 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 là loại tham số của phương thức này.

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: Lệnh này chỉ giữ lại các thành viên cụ thể của một lớp nếu lớp đó được khởi tạo trước trong mã Kotlin hoặc Java. Lệnh này cho R8 biết rằng ứng dụng đang dùng lớp đó và R8 nên giữ lại các thành viê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 bản sao trước trong mã Kotlin hoặc Java. Quy tắc này cho R8 biết rằng ứng dụng đang 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: Thao tác này nhắm đến chính xác lớp chứa trường.
    • includedescriptorclasses: Trình sửa đổi này cũng giữ lại mọi lớp có trong chữ ký hoặc bộ mô tả của phương thức. Trong trường hợp này, quy tắc này sẽ 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);: Chú thích này chỉ định chữ ký chính xác của phương thức Java 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 đều cần có quy tắc giữ lại riêng.
    • -keep class NativeData: Thao tác này nhắm đến lớp có tên là NativeData và khối này chỉ định những thành viê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. Thao tác này xác định duy nhất hàm khởi tạo lấy hai tham số: tham số đầu tiên là Integer và tham số thứ hai là String.

Cuộc gọi gián tiếp đến nền tảng

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

Khung Android dùng chức năng phản chiếu để tạo các thực thể của đố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 chế độ 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 một 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 nào được đề xuất. Trình bổ trợ kotlin-parcelizeGradle tự động tạo các quy tắc giữ lại bắt buộc cho các lớp mà bạn chú thích bằng @Parcelize. Nó xử lý sự phức tạp cho bạn, đảm bảo rằng CREATOR và các hàm khởi tạo đượ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 tự viết một lớp Parcelable theo cách thủ công trong Kotlin mà không dùng @Parcelize, bạn có trách nhiệm giữ trường CREATOR và hàm khởi tạo chấp nhận một Parcel. Nếu quên làm như vậy, ứng dụng của bạn sẽ gặp sự cố khi hệ thống cố gắng chuyển đổi đối tượng của bạn thành đối tượng có thể đọc được. 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 sẽ 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 hoạt động đúng cách.
  • Nhà phát triển ứng dụng phải xác minh rằng tất cả các quy tắc keep bắt buộc đều có mặt, đặc biệt là đối với mọi hoạt động triển khai tuỳ chỉnh hoặc các phần phụ thuộc của bên thứ ba.