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

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

เงาสะท้อน

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

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

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

ตัวอย่างเช่น พิจารณาสถานการณ์ต่อไปนี้ที่คุณมีไลบรารีและแอปที่ใช้ไลบรารีนั้น ตัวโหลดไลบรารีจะสร้างอินเทอร์เฟซ 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 สร้างความสับสนให้กับชื่อคลาสและย่อหรือนำการใช้งานของคลาส StartupTask ออกได้หากแอปไม่เคยใช้คลาสดังกล่าว อย่างไรก็ตาม สำหรับการใช้งานใดๆ เช่น PrecacheTask ที่ใช้ในตัวอย่าง จะยังคงมีตัวสร้างเริ่มต้น (<init>()) ที่ไลบรารีของคุณต้อง เรียกใช้

  • -keep,allowobfuscation,allowshrinking class * implements com.example.library.StartupTask: กฎนี้กำหนดเป้าหมายไปยังคลาสที่ใช้StartupTaskอินเทอร์เฟซของคุณ
    • -keep class * implements com.example.library.StartupTask: การดำเนินการนี้จะ รักษาคลาส (*) ที่ใช้การติดตั้งใช้งานอินเทอร์เฟซของคุณ
    • ,allowobfuscation: คำสั่งนี้จะบอก R8 ว่าแม้จะเก็บคลาสไว้ แต่ก็สามารถเปลี่ยนชื่อหรือทำให้คลาสดังกล่าวอ่านไม่ออกได้ วิธีนี้ปลอดภัยเนื่องจากไลบรารีไม่ได้อิงตามชื่อของคลาส แต่จะรับ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 ส่วนนี้ กำหนดเป้าหมายไปยังตัวสร้างเริ่มต้นที่ไม่มีอาร์กิวเมนต์โดยเฉพาะ
    • กฎนี้มีความสำคัญเนื่องจากโค้ดของคุณเรียกใช้ 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 ที่ไม่บังคับ

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

ไลบรารีหลักใช้การสะท้อน (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 { /* ... */ }
}

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

# 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 อาจมีปัญหาเมื่อทำงานกับ Upcall จากโค้ดเนทีฟ (C/C++ code) ไปยัง 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);: This specifies the exact Java signature of the method to preserve.
  • -keep class NativeData{<init>(java.lang.Integer, java.lang.String);}: แม้ว่า includedescriptorclasses จะตรวจสอบว่ามีการเก็บรักษาคลาส NativeData ไว้ แต่สมาชิก (ฟิลด์หรือเมธอด) ภายใน NativeData ที่เข้าถึงโดยตรงจากโค้ด JNI ดั้งเดิมจะต้องมีกฎ keep ของตัวเอง
    • -keep class NativeData: การดำเนินการนี้จะกำหนดเป้าหมายไปยังคลาสชื่อ NativeData และบล็อกจะระบุสมาชิกภายในคลาส NativeData ที่จะ เก็บไว้
    • <init>(java.lang.Integer, java.lang.String): นี่คือลายเซ็นของ คอนสตรัคเตอร์ โดยจะระบุตัวสร้างที่รับพารามิเตอร์ 2 รายการอย่างไม่ซ้ำกัน ซึ่งพารามิเตอร์แรกคือ Integer และพารามิเตอร์ที่ 2 คือ String

การเรียกใช้แพลตฟอร์มทางอ้อม

โอนข้อมูลด้วยการติดตั้งใช้งาน 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

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

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

โปรดคำนึงถึงสิ่งต่อไปนี้เมื่อใช้ปลั๊กอิน kotlin-parcelize

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