เก็บกรณีการใช้งานและตัวอย่างกฎ

ตัวอย่างต่อไปนี้อิงตามสถานการณ์ทั่วไปที่คุณใช้ R8 เพื่อเพิ่มประสิทธิภาพ แต่ต้องมีคำแนะนำขั้นสูงในการร่างกฎการเก็บรักษา

การสะท้อน

โดยทั่วไป เราไม่แนะนำให้ใช้การสะท้อนเพื่อประสิทธิภาพสูงสุด อย่างไรก็ตาม ในบางสถานการณ์ก็อาจหลีกเลี่ยงไม่ได้ ตัวอย่างต่อไปนี้ให้คำแนะนำเกี่ยวกับกฎการเก็บรักษาในสถานการณ์ทั่วไปที่ใช้การสะท้อน

หากการสะท้อนอยู่ในโค้ดแอปหรือไลบรารีของคุณเอง คุณสามารถใช้ คำอธิบายประกอบการเก็บรักษา แทนการเขียนกฎการเก็บรักษาด้วยตนเองได้ ปลั๊กอิน Gradle จะสร้างกฎการเก็บรักษาจากคำอธิบายประกอบในซอร์สโค้ด ซึ่งรวมถึงกฎการเก็บรักษาของผู้ใช้สำหรับไลบรารี

การสะท้อนด้วยคลาสที่โหลดตามชื่อ

ไลบรารีมักจะโหลดคลาสแบบไดนามิกโดยใช้ชื่อคลาสเป็น String อย่างไรก็ตาม R8 ไม่สามารถตรวจหาคลาสที่โหลดด้วยวิธีนี้ และอาจนำคลาสที่พิจารณาว่าไม่ได้ใช้ออก

ตัวอย่างเช่น ลองพิจารณาสถานการณ์สมมติต่อไปนี้ซึ่งคุณมีไลบรารีและแอปที่ใช้ไลบรารี โดยโค้ดจะแสดงตัวโหลดไลบรารีที่สร้างอินสแตนซ์อินเทอร์เฟซ StartupTask ที่แอปใช้

โค้ดไลบรารีมีลักษณะดังนี้

// 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()
    }
}

แอปที่ใช้ไลบรารีมีโค้ดดังนี้

// 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")
}

ในสถานการณ์สมมตินี้ ไลบรารีของคุณควรมีไฟล์กฎการเก็บรักษาของผู้ใช้ที่มีกฎการเก็บรักษาต่อไปนี้

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

หากไม่มีกฎนี้ R8 จะนำ PreCacheTask ออกจากแอปเนื่องจากแอปไม่ได้ใช้คลาสโดยตรง ซึ่งจะทำให้การผสานรวมหยุดทำงาน กฎนี้จะค้นหาคลาสที่ใช้ StartupTask อินเทอร์เฟซของไลบรารีและเก็บคลาสเหล่านั้นไว้พร้อมกับคอนสตรักเตอร์แบบไม่มีอาร์กิวเมนต์ ซึ่งจะช่วยให้ไลบรารีสร้างอินสแตนซ์และเรียกใช้ PreCacheTask ได้สำเร็จ

การสะท้อนด้วย ::class.java

ไลบรารีสามารถโหลดคลาสได้โดยให้แอปส่งออบเจ็กต์ Class โดยตรง ซึ่งเป็นวิธีที่เชื่อถือได้มากกว่าการโหลดคลาสตามชื่อ วิธีนี้จะสร้างการอ้างอิงที่ชัดเจนไปยังคลาสที่ R8 ตรวจพบได้ อย่างไรก็ตาม แม้ว่าวิธีนี้จะป้องกันไม่ให้ R8 นำคลาสออก แต่คุณก็ยังต้องใช้กฎการเก็บรักษาเพื่อประกาศว่าคลาสได้รับการสร้างอินสแตนซ์แบบสะท้อนและเพื่อปกป้องสมาชิกที่เข้าถึงแบบสะท้อน เช่น คอนสตรักเตอร์

ตัวอย่างเช่น ลองพิจารณาสถานการณ์สมมติต่อไปนี้ซึ่งคุณมีไลบรารีและแอปที่ใช้ไลบรารี โดยตัวโหลดไลบรารีจะสร้างอินสแตนซ์อินเทอร์เฟซ StartupTask โดยส่งการอ้างอิงคลาสโดยตรง

โค้ดไลบรารีมีลักษณะดังนี้

// 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()
    }
}

แอปที่ใช้ไลบรารีมีโค้ดดังนี้

// 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)
}

ในสถานการณ์สมมตินี้ ไลบรารีของคุณควรมีไฟล์กฎการเก็บรักษาของผู้ใช้ที่มีกฎการเก็บรักษาต่อไปนี้

# 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>();
}

กฎเหล่านี้ออกแบบมาให้ทำงานได้อย่างสมบูรณ์แบบกับการสะท้อนประเภทนี้ ซึ่งจะช่วยให้เพิ่มประสิทธิภาพได้สูงสุดพร้อมกับตรวจสอบว่าโค้ดทำงานอย่างถูกต้อง กฎเหล่านี้ช่วยให้ R8 ปรับชื่อคลาสให้ยากต่อการอ่าน (Obfuscate) และลดขนาดหรือนำการใช้งานคลาส StartupTask ออกหากแอปไม่ได้ใช้คลาสนี้ อย่างไรก็ตาม สำหรับการใช้งานใดๆ เช่น PrecacheTask ที่ใช้ในตัวอย่าง กฎเหล่านี้จะเก็บคอนสตรักเตอร์เริ่มต้น (<init>()) ที่ไลบรารีต้อง เรียก

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: กฎนี้กำหนดเป้าหมายไปยังคลาสที่ ใช้ StartupTask อินเทอร์เฟซของคุณ
    • -keep class * implements com.example.library.StartupTask: กฎนี้จะเก็บคลาส (*) ที่ใช้ อินเทอร์เฟซของคุณ
    • ,allowobfuscation: กฎนี้จะสั่งให้ R8 เปลี่ยนชื่อหรือปรับคลาสให้ยากต่อการอ่าน (Obfuscate) แม้ว่าจะเก็บคลาสไว้ก็ตาม การดำเนินการนี้ปลอดภัยเนื่องจากไลบรารีไม่ได้ขึ้นอยู่กับชื่อของคลาส แต่จะรับออบเจ็กต์ Class โดยตรง
    • ,allowshrinking: ตัวแก้ไขนี้จะสั่งให้ R8 นำคลาสออกได้หากไม่ได้ใช้ ซึ่งจะช่วยให้ R8 ลบการใช้งาน StartupTask ที่ไม่เคยส่งไปยัง TaskRunner.execute() ได้อย่างปลอดภัย กล่าวโดยย่อ กฎนี้หมายความว่า หากแอปใช้คลาสที่ใช้ StartupTask R8 จะเก็บคลาสนี้ไว้ R8 สามารถเปลี่ยนชื่อคลาสเพื่อลดขนาดและลบคลาสได้หากแอปไม่ได้ใช้
  • -keepclassmembers class * implements com.example.library.StartupTask { <init>(); }: กฎนี้กำหนดเป้าหมายไปยังสมาชิกที่เฉพาะเจาะจงของคลาสที่ระบุไว้ใน กฎแรก ซึ่งในกรณีนี้คือคอนสตรักเตอร์
    • -keepclassmembers class * implements com.example.library.StartupTask: กฎนี้จะเก็บสมาชิกที่เฉพาะเจาะจง (เมธอด, ฟิลด์) ของคลาสที่ใช้ StartupTask อินเทอร์เฟซ แต่จะเก็บไว้ก็ต่อเมื่อมีการเก็บคลาสที่ใช้เอง
    • { <init>(); }: นี่คือตัวเลือกสมาชิก <init> เป็นชื่อภายในพิเศษสำหรับคอนสตรักเตอร์ใน Java bytecode ส่วนนี้กำหนดเป้าหมายไปยังคอนสตรักเตอร์เริ่มต้นแบบไม่มีอาร์กิวเมนต์โดยเฉพาะ
    • กฎนี้มีความสำคัญเนื่องจากโค้ดของคุณเรียก getDeclaredConstructor().newInstance() โดยไม่มีอาร์กิวเมนต์ ซึ่งจะเรียกคอนสตรักเตอร์เริ่มต้นแบบสะท้อน หากไม่มีกฎนี้ R8 จะเห็นว่าไม่มีโค้ดใดเรียก new PreCacheTask() โดยตรง จึงสันนิษฐานว่าคอนสตรักเตอร์ไม่ได้ใช้และนำออก ซึ่งจะทำให้แอปขัดข้องขณะรันไทม์ด้วย InstantiationException

การสะท้อนตามคำอธิบายประกอบเมธอด

ไลบรารีมักจะกำหนดคำอธิบายประกอบที่นักพัฒนาแอปใช้เพื่อติดแท็กเมธอดหรือฟิลด์ จากนั้นไลบรารีจะใช้การสะท้อนเพื่อค้นหาสมาชิกที่มีคำอธิบายประกอบเหล่านี้ขณะรันไทม์ ตัวอย่างเช่น คำอธิบายประกอบ @OnLifecycleEvent ใช้เพื่อค้นหาเมธอดที่จำเป็นขณะรันไทม์

ตัวอย่างเช่น ลองพิจารณาสถานการณ์สมมติต่อไปนี้ซึ่งคุณมีไลบรารีและแอปที่ใช้ไลบรารี โดยตัวอย่างจะแสดง Event Bus ที่ค้นหาและเรียกเมธอดที่มีคำอธิบายประกอบ @OnEvent

โค้ดไลบรารีมีลักษณะดังนี้

@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) { /* ... */ }
            }
        }
    }
}

แอปที่ใช้ไลบรารีมีโค้ดดังนี้

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)
}

ไลบรารีควรมีไฟล์กฎการเก็บรักษาของผู้ใช้ที่เก็บเมธอดใดๆ ที่ใช้คำอธิบายประกอบโดยอัตโนมัติ

-keepattributes RuntimeVisibleAnnotations
-keep @interface com.example.library.OnEvent;
-keepclassmembers class * {
    @com.example.library.OnEvent <methods>;
}
  • -keepattributes RuntimeVisibleAnnotations: กฎนี้จะเก็บ คำอธิบายประกอบที่ออกแบบมาให้อ่านขณะรันไทม์
  • -keep @interface com.example.library.OnEvent: กฎนี้จะเก็บคลาสคำอธิบายประกอบ OnEvent ไว้
  • -keepclassmembers class * {@com.example.library.OnEvent <methods>;}: กฎนี้จะเก็บคลาสและสมาชิกที่เฉพาะเจาะจงไว้ก็ต่อเมื่อมีการใช้คลาสและคลาสมีสมาชิกเหล่านั้น
    • -keepclassmembers: กฎนี้จะเก็บคลาสและสมาชิกที่เฉพาะเจาะจงไว้ก็ต่อเมื่อมีการใช้คลาสและคลาสมีสมาชิกเหล่านั้น
    • class *: กฎนี้ใช้กับคลาสใดก็ได้
    • @com.example.library.OnEvent <methods>;: กฎนี้จะเก็บคลาสใดก็ได้ ที่มีเมธอดอย่างน้อย 1 รายการ (<methods>) ที่มีคำอธิบายประกอบ @com.example.library.OnEvent และเก็บเมธอดที่มีคำอธิบายประกอบไว้ด้วย

การสะท้อนตามคำอธิบายประกอบคลาส

ไลบรารีสามารถใช้การสะท้อนเพื่อสแกนหาคลาสที่มีคำอธิบายประกอบที่เฉพาะเจาะจง ในกรณีนี้ คลาส Task Runner จะค้นหาคลาสทั้งหมดที่มีคำอธิบายประกอบ ReflectiveExecutor โดยใช้การสะท้อนและเรียกใช้เมธอด execute

ตัวอย่างเช่น ลองพิจารณาสถานการณ์สมมติต่อไปนี้ซึ่งคุณมีไลบรารีและแอปที่ใช้ไลบรารี

ไลบรารีมีโค้ดดังนี้

@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)
        }
    }
}

แอปที่ใช้ไลบรารีมีโค้ดดังนี้

// 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)
}

เนื่องจากไลบรารีใช้การสะท้อนแบบสะท้อนเพื่อรับคลาสที่เฉพาะเจาะจง ไลบรารีจึงควรมีไฟล์กฎการเก็บรักษาของผู้ใช้ที่มีกฎการเก็บรักษาต่อไปนี้

# 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();
}

การกำหนดค่านี้มีประสิทธิภาพสูงเนื่องจากจะบอก R8 อย่างชัดเจนว่าจะเก็บอะไรไว้

การสะท้อนเพื่อรองรับทรัพยากร Dependency ที่ไม่บังคับ

กรณีการใช้งานทั่วไปสำหรับการสะท้อนคือการสร้างทรัพยากร Dependency แบบยืดหยุ่นระหว่างไลบรารีหลักกับไลบรารีส่วนเสริมที่ไม่บังคับ ไลบรารีหลักสามารถตรวจสอบว่ามีการรวมไลบรารีส่วนเสริมไว้ในแอปหรือไม่ และหากรวมไว้ ก็สามารถเปิดใช้ฟีเจอร์เพิ่มเติมได้ ซึ่งจะช่วยให้คุณจัดส่งโมดูลส่วนเสริมได้โดยไม่ต้องบังคับให้ไลบรารีหลักมีทรัพยากร Dependency โดยตรงกับโมดูลเหล่านั้น

ไลบรารีหลักใช้การสะท้อน (Class.forName) เพื่อค้นหาคลาสที่เฉพาะเจาะจงตามชื่อ หากพบคลาส ระบบจะเปิดใช้ฟีเจอร์ หากไม่พบ ระบบจะทำงานล้มเหลวอย่างราบรื่น

ตัวอย่างเช่น ลองพิจารณาโค้ดต่อไปนี้ซึ่ง AnalyticsManager หลักจะตรวจสอบคลาส VideoEventTracker ที่ไม่บังคับเพื่อเปิดใช้การวิเคราะห์วิดีโอ

ไลบรารีหลักมีโค้ดดังนี้

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())
        }
    }
}

ไลบรารีวิดีโอที่ไม่บังคับมีโค้ดดังนี้

package com.example.analytics.video

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

นักพัฒนาแอปของไลบรารีที่ไม่บังคับมีหน้าที่รับผิดชอบในการจัดเตรียมกฎการเก็บรักษาของผู้ใช้ที่จำเป็น กฎการเก็บรักษานี้จะช่วยให้แอปใดๆ ที่ใช้ไลบรารีที่ไม่บังคับเก็บโค้ดที่ไลบรารีหลักต้องค้นหาไว้

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

หากไม่มีกฎนี้ R8 มีแนวโน้มที่จะนำ VideoEventTracker ออกจากไลบรารีที่ไม่บังคับเนื่องจากไม่มีสิ่งใดในโมดูลนั้นใช้คลาสนี้โดยตรง กฎการเก็บรักษาจะเก็บคลาสและคอนสตรักเตอร์ไว้ ซึ่งจะช่วยให้ไลบรารีหลักสร้างอินสแตนซ์คลาสนี้ได้สำเร็จ

การสะท้อนเพื่อเข้าถึงสมาชิกส่วนตัว

การใช้การสะท้อนเพื่อเข้าถึงโค้ดส่วนตัวหรือโค้ดที่ได้รับการปกป้องซึ่งไม่ได้เป็นส่วนหนึ่งของ API สาธารณะของไลบรารีอาจทำให้เกิดปัญหาสำคัญ โค้ดดังกล่าวอาจมีการเปลี่ยนแปลงโดยไม่ต้องแจ้งให้ทราบ ซึ่งอาจนำไปสู่ลักษณะการทำงานที่ไม่คาดคิดหรือการขัดข้องในแอปพลิเคชัน

เมื่อคุณใช้การสะท้อนสำหรับ API ที่ไม่ใช่สาธารณะ คุณอาจพบปัญหาต่อไปนี้

  • การอัปเดตถูกบล็อก: การเปลี่ยนแปลงในโค้ดส่วนตัวหรือโค้ดที่ได้รับการปกป้องอาจทำให้คุณอัปเดตเป็นไลบรารีเวอร์ชันที่สูงขึ้นไม่ได้
  • พลาดสิทธิประโยชน์: คุณอาจพลาดฟังก์ชันการทำงานใหม่ๆ การแก้ไขข้อบกพร่องที่ทำให้เกิดการขัดข้องที่สำคัญ หรือการอัปเดตความปลอดภัยที่จำเป็น

การเพิ่มประสิทธิภาพของ R8 และการสะท้อน

หากคุณต้องสะท้อนโค้ดส่วนตัวหรือโค้ดที่ได้รับการปกป้องของไลบรารี โปรดให้ความสนใจกับการเพิ่มประสิทธิภาพของ R8 หากไม่มีการอ้างอิงโดยตรงไปยังสมาชิกเหล่านี้ R8 อาจสันนิษฐานว่าสมาชิกเหล่านี้ไม่ได้ใช้และนำออกหรือเปลี่ยนชื่อในภายหลัง ซึ่งอาจนำไปสู่การขัดข้องขณะรันไทม์ โดยมักจะมีข้อความแสดงข้อผิดพลาดที่ทำให้เข้าใจผิด เช่น NoSuchMethodException หรือ NoSuchFieldException

ตัวอย่างเช่น ลองพิจารณาสถานการณ์สมมติต่อไปนี้ซึ่งแสดงวิธีเข้าถึงฟิลด์ส่วนตัวจากคลาสไลบรารี

ไลบรารีที่คุณไม่ได้เป็นเจ้าของมีโค้ดดังนี้

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

แอปของคุณมีโค้ดดังนี้

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
}

เพิ่มกฎ -keep ในแอปเพื่อป้องกันไม่ให้ R8 นำฟิลด์ส่วนตัวออก

-keepclassmembers class com.example.LibraryClass {
    private java.lang.String secretMessage;
}
  • -keepclassmembers: กฎนี้จะเก็บสมาชิกที่เฉพาะเจาะจงของคลาสไว้ก็ต่อเมื่อมีการเก็บคลาสไว้
  • class com.example.LibraryClass: กฎนี้กำหนดเป้าหมายไปยังคลาสที่แน่นอนซึ่งมีฟิลด์
  • private java.lang.String secretMessage;: กฎนี้จะระบุฟิลด์ส่วนตัวที่เฉพาะเจาะจงตามชื่อและประเภท

Java Native Interface (JNI)

การเพิ่มประสิทธิภาพของ R8 อาจทำให้เกิดปัญหาเมื่อทำงานกับการเรียกกลับจากโค้ดแบบเนทีฟ (C/C++) ไปยัง Java หรือ Kotlin แม้ว่าการเรียกกลับจาก Java หรือ Kotlin ไปยังโค้ดแบบเนทีฟก็อาจทำให้เกิดปัญหาเช่นกัน แต่ไฟล์เริ่มต้น proguard-android-optimize.txt จะมีกฎต่อไปนี้เพื่อเก็บการเรียกกลับไว้ กฎนี้จะป้องกันไม่ให้มีการตัดแต่งเมธอดแบบเนทีฟ

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

การโต้ตอบกับโค้ดแบบเนทีฟผ่าน Java Native Interface (JNI)

เมื่อแอปใช้ JNI เพื่อทำการเรียกกลับจากโค้ดแบบเนทีฟ (C/C++) ไปยัง Java หรือ Kotlin, R8 จะมองไม่เห็นว่ามีการเรียกเมธอดใดจากโค้ดแบบเนทีฟ หากไม่มีการอ้างอิงโดยตรงไปยังเมธอดเหล่านี้ในแอป R8 จะสันนิษฐานอย่างไม่ถูกต้องว่าเมธอดเหล่านี้ไม่ได้ใช้และนำออก ซึ่งจะทำให้แอปขัดข้อง

ตัวอย่างต่อไปนี้แสดงคลาส Kotlin ที่มีเมธอดที่ออกแบบมาให้เรียกจากไลบรารีแบบเนทีฟ ไลบรารีแบบเนทีฟจะสร้างอินสแตนซ์ประเภทแอปพลิเคชันและส่งข้อมูลจากโค้ดแบบเนทีฟไปยังโค้ด 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")
        }
    }
}

ในกรณีนี้ คุณต้องแจ้งให้ R8 ทราบเพื่อป้องกันไม่ให้มีการเพิ่มประสิทธิภาพประเภทแอปพลิเคชัน นอกจากนี้ หากเมธอดที่เรียกจากโค้ดแบบเนทีฟใช้คลาสของคุณเองในลายเซ็นเป็นพารามิเตอร์หรือประเภทการแสดงผล คุณต้องตรวจสอบด้วยว่าคลาสเหล่านั้นไม่ได้เปลี่ยนชื่อ

เพิ่มกฎการเก็บรักษาต่อไปนี้ลงในแอป

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

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

กฎการเก็บรักษาเหล่านี้จะป้องกันไม่ให้ R8 นำเมธอด onNativeEvent และประเภทพารามิเตอร์ของเมธอดออกหรือเปลี่ยนชื่อ

  • -keepclassmembers,includedescriptorclasses class com.example.JniBridge{ public void onNativeEvent(com.example.model.NativeData);}: กฎนี้จะเก็บสมาชิกที่เฉพาะเจาะจงของคลาสไว้ก็ต่อเมื่อมีการสร้างอินสแตนซ์คลาส ในโค้ด Kotlin หรือ Java ก่อน ซึ่งจะบอก R8 ว่าแอปกำลังใช้คลาสและ ควรเก็บสมาชิกที่เฉพาะเจาะจงของคลาสไว้
    • -keepclassmembers: กฎนี้จะเก็บสมาชิกที่เฉพาะเจาะจงของคลาสไว้ก็ต่อเมื่อมีการสร้างอินสแตนซ์คลาสในโค้ด Kotlin หรือ Java ก่อน ซึ่งจะบอก R8 ว่าแอปกำลังใช้คลาสและควรเก็บสมาชิกที่เฉพาะเจาะจงของคลาสไว้
    • class com.example.JniBridge: กฎนี้กำหนดเป้าหมายไปยังคลาสที่แน่นอนซึ่งมีฟิลด์
    • includedescriptorclasses: ตัวแก้ไขนี้จะเก็บคลาสใดๆ ที่พบในลายเซ็นหรือตัวอธิบายของเมธอดไว้ด้วย ในกรณีนี้ ตัวแก้ไขจะป้องกันไม่ให้ R8 เปลี่ยนชื่อหรือนำคลาส com.example.models.NativeData ซึ่งใช้เป็นพารามิเตอร์ออก หากเปลี่ยนชื่อ NativeData (เช่น เป็น a.a) ลายเซ็นเมธอดจะไม่ตรงกับสิ่งที่โค้ดแบบเนทีฟคาดหวังอีกต่อไป ซึ่งจะทำให้เกิดการขัดข้อง
    • public void onNativeEvent(com.example.models.NativeData);: กฎนี้จะระบุลายเซ็น Java ที่แน่นอนของเมธอดที่จะเก็บไว้
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: แม้ว่า includedescriptorclasses จะช่วยให้มั่นใจได้ว่าคลาส NativeData จะถูกเก็บไว้ แต่สมาชิก (ฟิลด์หรือเมธอด) ใดๆ ภายใน NativeData ที่เข้าถึงโดยตรงจากโค้ด JNI แบบเนทีฟจะต้องมีกฎการเก็บรักษาของตัวเอง
    • -keep class NativeData: กฎนี้กำหนดเป้าหมายไปยังคลาสที่ชื่อ NativeData และบล็อกจะระบุสมาชิกที่จะเก็บไว้ภายในคลาส NativeData class to keep.
    • <init>(java.lang.Integer, java.lang.String): นี่คือ ลายเซ็นของคอนสตรักเตอร์ ลายเซ็นนี้จะระบุคอนสตรักเตอร์ที่ใช้พารามิเตอร์ 2 รายการอย่างไม่ซ้ำกัน โดยพารามิเตอร์แรกคือ Integer และพารามิเตอร์ที่ 2 คือ String

นอกจากนี้ คุณยังใส่คำอธิบายประกอบเมธอดที่โค้ดแบบเนทีฟเรียกด้วยคำอธิบายประกอบ @UnconditionallyKeep จาก Jetpack Keep แทนการเขียนกฎการเก็บรักษา สำหรับเมธอดเหล่านั้นได้ด้วย คำอธิบายประกอบจะไม่เก็บคลาสไว้ในลายเซ็นของเมธอด เช่น NativeData ดังนั้นคุณจึงยังต้องเก็บคลาสเหล่านั้นแยกต่างหาก

การเรียกแพลตฟอร์มโดยอ้อม

โอนข้อมูลด้วยการใช้งาน Parcelable

เฟรมเวิร์ก Android ใช้การสะท้อนเพื่อสร้างอินสแตนซ์ออบเจ็กต์ Parcelable ในการพัฒนา Kotlin สมัยใหม่ คุณควรใช้ปลั๊กอิน kotlin-parcelize ซึ่งจะสร้างการใช้งาน Parcelable ที่จำเป็นโดยอัตโนมัติ ซึ่งรวมถึงฟิลด์ CREATOR และเมธอดที่เฟรมเวิร์กต้องการ

ตัวอย่างเช่น ลองพิจารณาตัวอย่างต่อไปนี้ซึ่งใช้ปลั๊กอิน kotlin-parcelize เพื่อสร้างคลาส 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

ในสถานการณ์สมมตินี้ ไม่มีกฎการเก็บรักษาที่แนะนำ ปลั๊กอิน Gradle kotlin-parcelize จะสร้างกฎการเก็บรักษาที่จำเป็นสำหรับคลาสที่คุณใส่คำอธิบายประกอบ @Parcelize โดยอัตโนมัติ ปลั๊กอินจะจัดการความซับซ้อนให้คุณ โดยตรวจสอบว่ามีการเก็บ CREATOR และคอนสตรักเตอร์ที่สร้างขึ้นไว้สำหรับการเรียกการสะท้อนของเฟรมเวิร์ก Android

หากคุณเขียนคลาส Parcelable ด้วยตนเองใน Kotlin โดยไม่ใช้ @Parcelize, คุณมีหน้าที่รับผิดชอบในการเก็บฟิลด์ CREATOR และคอนสตรักเตอร์ที่ รับ Parcel หากลืมดำเนินการดังกล่าว แอปจะขัดข้องเมื่อระบบพยายามยกเลิกการซีเรียลไลซ์ออบเจ็กต์ การใช้ @Parcelize เป็นแนวทางปฏิบัติมาตรฐานที่ปลอดภัยกว่า

เมื่อใช้ปลั๊กอิน kotlin-parcelize โปรดทราบสิ่งต่อไปนี้

  • ปลั๊กอินจะสร้างฟิลด์ CREATOR โดยอัตโนมัติระหว่างการคอมไพล์
  • ไฟล์ proguard-android-optimize.txt มีกฎ keep ที่จำเป็นเพื่อเก็บฟิลด์เหล่านี้ไว้เพื่อให้ทำงานได้อย่างถูกต้อง
  • นักพัฒนาแอปต้องตรวจสอบว่ามีกฎ keep ที่จำเป็นทั้งหมด โดยเฉพาะอย่างยิ่งสำหรับการใช้งานที่กำหนดเองหรือทรัพยากร Dependency ของบุคคลที่สาม

ไลบรารีที่ใช้การสะท้อนหรือการแปลง bytecode จะเข้าถึงโค้ดแบบไดนามิกขณะรันไทม์ หาก R8 นำคลาส ฟิลด์ หรือเมธอดที่เข้าถึงด้วยวิธีนี้ออกหรือเปลี่ยนชื่อ แอปอาจขัดข้อง

อย่างไรก็ตาม ไลบรารีของบุคคลที่สามยอดนิยม (เช่น Gson, Retrofit และ Kotlinx Serialization) จะรวมกฎการเก็บรักษาของผู้ใช้ R8 ไว้โดยอัตโนมัติ เมื่อใช้ไลบรารีเวอร์ชันล่าสุด คุณไม่จำเป็นต้องเพิ่มกฎการเก็บรักษาด้วยตนเองลงในโปรเจ็กต์

Gson

Gson เป็นไลบรารีการซีเรียลไลซ์และการยกเลิกการซีเรียลไลซ์ JSON ที่ใช้การสะท้อนอย่างมาก เมื่อคุณใช้ โหมดเต็ม เพื่อเพิ่มประสิทธิภาพแอป ไลบรารีจะนำลายเซ็นประเภททั่วไป คอนสตรัคเตอร์เริ่มต้น และฟิลด์ที่ไม่มีคำอธิบายประกอบออก เว้นแต่ จะได้รับคำสั่งอย่างชัดเจน

หากต้องการให้ Gson ทำงานอย่างถูกต้อง ให้เพิ่มกฎที่เฉพาะเจาะจงเพื่อเก็บฟิลด์ที่ไม่ใช่ชั่วคราวไว้ในคลาสโมเดลข้อมูลและเก็บลำดับชั้น 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 จะไม่สนใจฟิลด์ที่มีตัวแก้ไข transient ระหว่างการ ซีเรียลไลซ์และการยกเลิกการซีเรียลไลซ์ ซึ่งเป็นเหตุผลที่กฎการเก็บรักษากำหนดเป้าหมายไปยังฟิลด์ที่ไม่ใช่ชั่วคราว (!transient) โดยเฉพาะ

Retrofit

Retrofit เป็นไลบรารีเครือข่ายที่ตรวจสอบเมธอดอินเทอร์เฟซบริการที่มีคำอธิบายประกอบ HTTP (เช่น @GET หรือ @POST) โดยใช้การสะท้อนเพื่อสร้างคำขอเครือข่ายและแปลงการตอบกลับ

Retrofit จะสร้างการใช้งานอินเทอร์เฟซ API แบบไดนามิกขณะรันไทม์โดยใช้ Proxy.newProxyInstance() เนื่องจาก R8 ไม่เห็นคลาสใดที่ใช้ อินเทอร์เฟซเหล่านี้แบบคงที่ จึงอาจนำเมธอดหรือประเภทการแสดงผลทั่วไปของเมธอดออก

กฎการเก็บรักษาที่รวมไว้

Retrofit ใช้การสะท้อนขณะรันไทม์เพื่อตรวจสอบพารามิเตอร์ทั่วไป คำอธิบายประกอบเมธอด และคำอธิบายประกอบพารามิเตอร์ หากไม่มีการกำหนดค่าที่เหมาะสม โหมดเต็มของ R8 อาจนำลายเซ็นทั่วไปออกจากประเภทการแสดงผล การดำเนินการต่อของ Kotlin และคลาสการตอบกลับออกอย่างละเอียด หรือแม้แต่แทนที่ค่าอินเทอร์เฟซด้วยค่า Null เนื่องจากอินเทอร์เฟซ Retrofit ได้รับการสร้างอินสแตนซ์แบบไดนามิกด้วยพร็อกซี

ตั้งแต่ Retrofit 2.10.0 เป็นต้นไป ไลบรารีจะรวมกฎการเก็บรักษาอย่างเป็นทางการที่จำเป็นเพื่อเก็บค่าเริ่มต้นของคำอธิบายประกอบ พารามิเตอร์เมธอดบริการ และข้อมูลเมตาของคลาสที่จำเป็นไว้โดยอัตโนมัติ ดูข้อมูลเพิ่มเติมได้ที่ กฎที่ Retrofit ใช้

เก็บประเภทการแสดงผลทั่วไปไว้

Retrofit จะตรวจสอบลายเซ็นทั่วไปของประเภทการแสดงผล (เช่น Observable<Data>) เพื่อยกเลิกการซีเรียลไลซ์การตอบกลับเครือข่ายอย่างถูกต้อง หาก R8 นำลายเซ็นทั่วไปออก Retrofit จะแทนที่ออบเจ็กต์ที่สร้างอินสแตนซ์ด้วย null

หากต้องการป้องกันไม่ให้โหมดเต็มของ R8 นำลายเซ็นทั่วไปของประเภทการแสดงผลออก ให้ใช้กฎแบบมีเงื่อนไขต่อไปนี้

# 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>

คลาสโมเดลข้อมูลจริงที่แสดงผล (เช่น Data ใน Observable<Data>) ต้องถูกเก็บไว้ด้วย เนื่องจาก ตัวแปลง (เช่น Gson) จะสร้างคลาสนี้แบบสะท้อน

โครูทีน

เมื่อคุณใช้โครูทีน Kotlin คอมไพเลอร์ Kotlin จะแปลงฟังก์ชัน suspend โดยเพิ่มพารามิเตอร์ Continuation ลงในลายเซ็นเมธอดที่คอมไพล์

เมื่อไลบรารี เช่น Retrofit อ่านลายเซ็นทั่วไปของฟังก์ชัน suspend แบบสะท้อน ไลบรารีจะใช้พารามิเตอร์ Continuation นั้น เมื่อใช้โหมดเต็ม ระบบจะเก็บแอตทริบิวต์ Signature ไว้สำหรับคลาสที่เก็บไว้อย่างชัดเจนเท่านั้น เนื่องจาก Continuation เป็นพารามิเตอร์สังเคราะห์ R8 จึงนำลายเซ็นออกโดยค่าเริ่มต้น ซึ่งจะทำให้การสะท้อนหยุดทำงาน

หากต้องการป้องกันการนำลายเซ็นออกและรับประกันความเข้ากันได้ขณะรันไทม์ในโหมดเต็ม ให้รวมกฎต่อไปนี้

# Keep the signature attribute globally
-keepattributes Signature

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