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 thảo 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.

Nếu hoạt động phản chiếu nằm trong mã ứng dụng hoặc thư viện của riêng bạn, thì bạn có thể sử dụng chú giải giữ lại thay vì viết quy tắc giữ lại theo cách thủ công. Trình bổ trợ Gradle tạo quy tắc giữ lại từ chú giải trong mã nguồn của bạn, bao gồm cả quy tắc giữ lại của đối tượng sử dụng cho thư viện.

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 độ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à 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 đó. 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.

Mã thư viện có dạng như sau:

// 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ã như 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 khởi tạo 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 đượ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 khởi tạo giao diện StartupTask bằng cách chuyển trực tiếp tham chiếu lớp.

Mã thư viện có dạng như sau:

// 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ã như 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 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á việc 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 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 của bạn.
    • -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 hoạt động 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 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ú giải phương thức

Thư viện thường xác định các chú giải mà nhà phát triển sử dụng để gắn thẻ 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ú giải này trong thời gian chạy. Ví dụ: chú giải @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ú giải bằng @OnEvent.

Mã thư viện có dạng như sau:

@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ã như 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ú giải 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ú giải đượ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ú giải OnEvent.
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: Quy tắc này giữ lại một lớp và các thành phần cụ thể chỉ khi lớp đó đang được sử dụng và lớp đó chứa các thành phần đó.
    • -keepclassmembers: Quy tắc này giữ lại một lớp và các thành phần cụ thể chỉ khi lớp đó đang được sử dụng và lớp đó 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ú giải bằng @com.example.library.OnEvent, đồng thời giữ lại chính các phương thức được chú giải.

Phản chiếu dựa trên chú giải 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ú giải cụ thể. Trong trường hợp này, lớp trình chạy tác vụ tìm tất cả các lớp được chú giải bằng ReflectiveExecutor bằ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ã như 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ã như 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 một cách 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 gói bổ sung có được đưa vào ứng dụng 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 phân phối 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 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ã như 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ã như 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 của đố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 khởi tạo 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ó 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.

Thư viện mà bạn không sở hữu có mã như sau:

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

Ứng dụng của bạn có mã như 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 giữ lại các thành phần cụ thể của một lớp chỉ khi 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 khởi tạo một 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 lệnh gọi phương thức từ mã gốc sử dụng các lớp của riêng bạn trong chữ ký làm tham số hoặc kiểu dữ liệ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 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 giữ lại các thành phần cụ thể của một lớp chỉ khi lớp đó được khởi tạo trong mã Kotlin hoặc Java trước – 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 giữ lại các thành phần cụ thể của một lớp chỉ khi lớp đó được khởi tạo trước 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), thì 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.

Bạn cũng có thể chú giải các phương thức mà mã gốc gọi bằng chú giải @UnconditionallyKeep từ Jetpack Keep thay vì viết quy tắc giữ lại cho các phương thức đó. Chú giải này không giữ lại các lớp trong chữ ký của phương thức, chẳng hạn như NativeData, vì vậy, bạn vẫn cần giữ lại các lớp đó riêng biệt.

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 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 hoạt động 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 bắt buộc cho các lớp mà bạn chú giải 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, thì bạn chịu 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 việc này sẽ khiến ứng dụng của bạn gặp sự cố khi hệ thống cố gắng giải 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 bắt buộc đều có, đặc biệt là đối với mọi hoạt động 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 động 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, thì ứ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à giải 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ú giải, 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

Các trường được đánh dấu bằng đối tượng sửa đổi transient sẽ bị Gson bỏ qua trong quá trình tuần tự hoá và giải 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 kết nối mạng kiểm tra các phương thức giao diện dịch vụ được chú giải bằng chú giải HTTP (chẳng hạn như @GET hoặc @POST) bằng tính năng phản chiếu để tạo các yêu cầu mạng và chuyển đổi phản hồi.

Retrofit tạo động các hoạt động 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 kiểu dữ liệu 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ú giải phương thức và chú giải 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 kiểu dữ liệu trả về, phần tiếp tục 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 động 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ú giải, 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 kiểu dữ liệu trả về chung

Retrofit kiểm tra chữ ký chung của kiểu dữ liệu trả về (ví dụ: Observable<Data>) để giải 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 khởi tạo bằng null.

Để ngăn chế độ đầy đủ chức năng của R8 loại bỏ chữ ký chung của các kiểu dữ liệu 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 tạo một cách phản chiếu bởi trình chuyển đổi (chẳng hạn như Gson).

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 tính năng 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