ตัวอย่างต่อไปนี้อิงตามสถานการณ์ทั่วไปที่คุณใช้ 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 ของบุคคลที่สาม