R8 มี 2 โหมด ได้แก่ โหมดความเข้ากันได้และโหมดเต็ม โหมดเต็มจะช่วยให้คุณเพิ่มประสิทธิภาพได้อย่างมีประสิทธิภาพ ซึ่งจะช่วยปรับปรุงประสิทธิภาพของแอป
คู่มือนี้มีไว้สำหรับนักพัฒนาแอป Android ที่ต้องการใช้การเพิ่มประสิทธิภาพที่มีประสิทธิภาพที่สุดของ R8 โดยจะอธิบายความแตกต่างที่สำคัญระหว่างโหมดความเข้ากันได้และโหมดเต็ม รวมถึงการกำหนดค่าที่ชัดเจนซึ่งจำเป็นสำหรับการย้ายข้อมูลโปรเจ็กต์อย่างปลอดภัยและหลีกเลี่ยงข้อขัดข้องที่พบบ่อยในรันไทม์
เปิดใช้โหมดเต็ม
หากต้องการเปิดใช้โหมดเต็ม ให้นำบรรทัดต่อไปนี้ออกจากไฟล์ gradle.properties
android.enableR8.fullMode=false // Remove this line to enable full mode
เก็บคลาสที่เชื่อมโยงกับแอตทริบิวต์ไว้
แอตทริบิวต์คือข้อมูลเมตาที่จัดเก็บไว้ในไฟล์คลาสที่คอมไพล์แล้ว ซึ่งไม่ได้เป็นส่วนหนึ่งของโค้ดที่เรียกใช้งานได้ อย่างไรก็ตาม แอตทริบิวต์อาจจำเป็นสำหรับการสะท้อนบางประเภท ตัวอย่างที่พบบ่อย ได้แก่ Signature (ซึ่งเก็บข้อมูลประเภททั่วไปไว้หลังจากลบประเภทแล้ว) InnerClasses และ EnclosingMethod (สำหรับการสะท้อนโครงสร้างคลาส) รวมถึงคำอธิบายประกอบที่มองเห็นได้ในรันไทม์
โค้ดต่อไปนี้แสดงลักษณะของแอตทริบิวต์ Signature สำหรับฟิลด์ในไบต์โค้ด สำหรับฟิลด์
List<User> users;
ไฟล์คลาสที่คอมไพล์แล้วจะมีไบต์โค้ดต่อไปนี้
.field public static final users:Ljava/util/List;
.annotation system Ldalvik/annotation/Signature;
value = {
"Ljava/util/List<",
"Lcom/example/package/User;",
">;"
}
.end annotation
.end field
ไลบรารีที่ใช้การสะท้อนอย่างหนัก (เช่น Gson) มักจะอาศัยแอตทริบิวต์เหล่านี้เพื่อตรวจสอบและทำความเข้าใจโครงสร้างโค้ดของคุณแบบไดนามิก โดยค่าเริ่มต้นในโหมดเต็มของ R8 ระบบจะเก็บแอตทริบิวต์ไว้ก็ต่อเมื่อมีการเก็บคลาส ฟิลด์ หรือเมธอดที่เชื่อมโยงไว้อย่างชัดเจนเท่านั้น
ตัวอย่างต่อไปนี้แสดงให้เห็นว่าทำไมแอตทริบิวต์จึงจำเป็น และคุณต้องเพิ่มกฎการเก็บรักษาใดบ้างเมื่อย้ายจากโหมดความเข้ากันได้ไปยังโหมดเต็ม นอกเหนือจากการเก็บคลาส ฟิลด์ หรือเมธอดที่สะท้อนแล้ว คุณยังต้องเก็บแอตทริบิวต์ที่คลาส ฟิลด์ หรือเมธอดเหล่านั้นอาศัยไว้อย่างชัดเจนด้วย
ลองพิจารณาตัวอย่างต่อไปนี้ที่เรายกเลิกการซีเรียลไลซ์รายการผู้ใช้โดยใช้ไลบรารี Gson
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
data class User(
@SerializedName("username")
var username: String? = null,
@SerializedName("age")
var age: Int = 0
)
fun GsonRemoteJsonListExample() {
val gson = Gson()
// 1. The JSON string for a list of users returned from remote
val jsonOutput = """[{"username":"alice","age":30}, {"username":"bob","age":25}]"""
// 2. Deserialize the JSON string into a List<User>
// We must use TypeToken for generic types like List
val listType = object : TypeToken<List<User>>() {}.type
val deserializedList: List<User> = gson.fromJson(jsonOutput, listType)
// Print the list
println("First user from list: ${deserializedList}")
}
ระหว่างการคอมไพล์ การลบประเภทของ Java จะนำอาร์กิวเมนต์ประเภททั่วไปออก ซึ่ง
หมายความว่าในรันไทม์ ทั้ง List<String> และ List<User> จะปรากฏเป็น
List แบบดิบ ดังนั้น ไลบรารีอย่าง Gson ซึ่งอาศัยการสะท้อนจึงไม่สามารถ
ระบุประเภทออบเจ็กต์ที่เฉพาะเจาะจงซึ่งมีการประกาศให้ List มีได้เมื่อ
ยกเลิกการซีเรียลไลซ์รายการ JSON ซึ่งอาจทำให้เกิดปัญหาในรันไทม์
Gson ใช้ TypeToken เพื่อเก็บข้อมูลประเภทไว้ การห่อ TypeToken จะเก็บข้อมูลการยกเลิกการซีเรียลไลซ์ที่จำเป็นไว้
นิพจน์ Kotlin object:TypeToken<List<User>>() {}.type จะสร้าง
คลาสภายในที่ไม่ระบุชื่อซึ่งขยาย TypeToken และเก็บข้อมูลประเภททั่วไป
ไว้ ในตัวอย่างนี้ คลาสที่ไม่ระบุชื่อมีชื่อว่า $GsonRemoteJsonListExample$listType$1
ภาษาโปรแกรม Java จะบันทึกลายเซ็นทั่วไปของคลาสซูเปอร์เป็นข้อมูลเมตาที่เรียกว่าแอตทริบิวต์ Signature ภายในไฟล์คลาสที่คอมไพล์แล้ว
จากนั้น TypeToken จะใช้ข้อมูลเมตา Signature นี้เพื่อกู้คืนประเภทในรันไทม์
ซึ่งช่วยให้ Gson ใช้การสะท้อนเพื่ออ่าน Signature และค้นพบประเภท List<User> ที่สมบูรณ์ซึ่งจำเป็นสำหรับการยกเลิกการซีเรียลไลซ์ได้สำเร็จ
เมื่อเปิดใช้ R8 ในโหมดความเข้ากันได้ ระบบจะเก็บแอตทริบิวต์ Signature ไว้สำหรับคลาส รวมถึงคลาสภายในที่ไม่ระบุชื่อ เช่น $GsonRemoteJsonListExample$listType$1 แม้ว่าจะไม่ได้กำหนดกฎการเก็บรักษาที่เฉพาะเจาะจงไว้อย่างชัดเจนก็ตาม ด้วยเหตุนี้ โหมดความเข้ากันได้ของ R8 จึงไม่จำเป็นต้องมีกฎการเก็บรักษาที่ชัดเจนเพิ่มเติมเพื่อให้ตัวอย่างนี้ทำงานได้ตามที่คาดไว้
// keep rule for compatibility mode
-keepattributes Signature
เมื่อเปิดใช้ R8 ในโหมดเต็ม ระบบจะนำแอตทริบิวต์ Signature ของคลาสภายในที่ไม่ระบุชื่อ $GsonRemoteJsonListExample$listType$1 ออก หากไม่มีข้อมูลประเภทนี้ใน Signature Gson จะไม่พบประเภทแอปพลิเคชันที่ถูกต้อง ซึ่งจะส่งผลให้เกิด IllegalStateException
หากคุณใช้ Gson เวอร์ชันเก่ากว่า 2.11.0 กฎการเก็บรักษาที่จำเป็นเพื่อป้องกันไม่ให้เกิดเหตุการณ์นี้มีดังนี้
// keep rule required for full mode
-keepattributes Signature
-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken { *; }
-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken
-keepattributes Signature: กฎนี้จะสั่งให้ R8 เก็บแอตทริบิวต์ที่ Gson ต้องอ่านไว้ ในโหมดเต็ม R8 จะเก็บแอตทริบิวต์Signatureไว้สำหรับคลาส ฟิลด์ หรือเมธอดที่ตรงกับกฎkeepอย่างชัดเจนเท่านั้น-keep,allowobfuscation,allowshrinking,allowoptimization class com.google.gson.reflect.TypeToken: กฎนี้จำเป็นเนื่องจากTypeTokenจะห่อประเภทออบเจ็กต์ที่กำลังยกเลิกการซีเรียลไลซ์ หลังจากลบประเภทแล้ว ระบบจะสร้างคลาสภายในที่ไม่ระบุชื่อเพื่อเก็บข้อมูลประเภททั่วไปไว้ หากไม่ได้เก็บcom.google.gson.reflect.TypeTokenไว้อย่างชัดเจน R8 ในโหมดเต็มจะไม่รวมประเภทคลาสนี้ไว้ในแอตทริบิวต์Signatureที่จำเป็นสำหรับการยกเลิกการซีเรียลไลซ์-keep,allowobfuscation,allowshrinking,allowoptimization class * extends com.google.gson.reflect.TypeToken: กฎนี้จะเก็บข้อมูลประเภทของคลาสที่ไม่ระบุชื่อซึ่งขยายTypeTokenไว้ เช่น$GsonRemoteJsonListExample$listType$1ในตัวอย่างนี้ หากไม่มีกฎนี้ R8 ในโหมดเต็มจะนำข้อมูลประเภทที่จำเป็นออก ทำให้การยกเลิกการซีเรียลไลซ์ล้มเหลว
คุณต้องเข้าใจว่ากฎที่แชร์ไว้ก่อนหน้านี้จะแก้ปัญหาการค้นหาประเภททั่วไป (เช่น List<User>) เท่านั้น R8 ยังเปลี่ยนชื่อฟิลด์ของคลาสด้วย หากคุณไม่ได้ใช้คำอธิบายประกอบ @SerializedName ในโมเดลข้อมูล Gson จะยกเลิกการซีเรียลไลซ์ JSON ไม่ได้เนื่องจากชื่อฟิลด์จะไม่ตรงกับคีย์ JSON อีกต่อไป
@SerializedName ในฟิลด์ คุณจึงไม่จำเป็นต้องระบุกฎการเก็บรักษาเพิ่มเติมสำหรับโมเดล
อย่างไรก็ตาม หากคุณใช้ Gson เวอร์ชันเก่ากว่า 2.11 หรือหากโมเดลไม่ได้ใช้คำอธิบายประกอบ @SerializedName คุณต้องเพิ่มกฎการเก็บรักษาที่ชัดเจนสำหรับโมเดลเหล่านั้น
เก็บตัวสร้างเริ่มต้นไว้
ในโหมดเต็มของ R8 ระบบจะไม่เก็บตัวสร้างที่ไม่มีอาร์กิวเมนต์/ตัวสร้างเริ่มต้นไว้โดยนัย แม้ว่าจะเก็บคลาสไว้ก็ตาม หากคุณสร้างอินสแตนซ์ของคลาสโดยใช้ class.getDeclaredConstructor().newInstance() หรือ class.newInstance() คุณต้องเก็บตัวสร้างที่ไม่มีอาร์กิวเมนต์ไว้ในโหมดเต็มอย่างชัดเจน ในทางตรงกันข้าม โหมดความเข้ากันได้จะเก็บตัวสร้างที่ไม่มีอาร์กิวเมนต์ไว้เสมอ
ลองพิจารณาตัวอย่างที่เราสร้างอินสแตนซ์ของ PrecacheTask โดยใช้การสะท้อนเพื่อเรียกเมธอด run แบบไดนามิก แม้ว่าสถานการณ์นี้จะไม่ต้องใช้กฎเพิ่มเติมในโหมดความเข้ากันได้ แต่ในโหมดเต็ม ระบบจะนำตัวสร้างเริ่มต้นของ PrecacheTask ออก ดังนั้นจึงต้องมีกฎการเก็บรักษาที่เฉพาะเจาะจง
// In library
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()
}
}
// In app
class PreCacheTask : StartupTask {
override fun run() {
Log.d("Pre cache task", "Warming up the cache...")
}
}
fun runTaskRunner() {
// The library is given a direct reference to the app's task class.
TaskRunner.execute(PreCacheTask::class.java)
}
# Full mode keep rule
# default constructor needs to be specified
-keep class com.example.fullmoder8.PreCacheTask {
<init>();
}
การแก้ไขการเข้าถึงเปิดใช้อยู่โดยค่าเริ่มต้น
ในโหมดความเข้ากันได้ R8 จะไม่เปลี่ยนระดับการเข้าถึงของเมธอดและฟิลด์ภายในคลาส อย่างไรก็ตาม ในโหมดเต็ม R8 จะเพิ่มประสิทธิภาพโดยการเปลี่ยนระดับการเข้าถึงของเมธอดและฟิลด์ เช่น จากส่วนตัวเป็นสาธารณะ ซึ่งจะช่วยให้เกิดการแทรกโค้ดมากขึ้น
การเพิ่มประสิทธิภาพนี้อาจทำให้เกิดปัญหาหากโค้ดของคุณใช้การสะท้อนที่อาศัยสมาชิกที่มีระดับการเข้าถึงที่เฉพาะเจาะจง R8 จะไม่รู้จักการใช้งานโดยอ้อมนี้ ซึ่งอาจทำให้แอปขัดข้อง คุณต้องเพิ่มกฎ -keep ที่เฉพาะเจาะจงเพื่อเก็บสมาชิกไว้ ซึ่งจะเก็บระดับการเข้าถึงเดิมของสมาชิกไว้ด้วย
ดูข้อมูลเพิ่มเติมได้ที่ตัวอย่างนี้เพื่อทำความเข้าใจว่าทำไมจึงไม่แนะนำให้เข้าถึงสมาชิกส่วนตัว โดยใช้การสะท้อน รวมถึงกฎการเก็บรักษาเพื่อเก็บฟิลด์/เมธอด เหล่านั้นไว้
ข้อมูลเมตาเฉพาะของ Kotlin
เมื่อคอมไพล์โค้ด Kotlin คอมไพเลอร์ Kotlin จะจัดเก็บข้อมูลเมตาเฉพาะภาษา (เช่น ความเป็น Null, ฟังก์ชันส่วนขยาย และลายเซ็นของโครูทีน) ไว้ในคำอธิบายประกอบ @kotlin.Metadata ในไฟล์คลาสแต่ละไฟล์
หากแอปหรือทรัพยากร Dependency ของแอปใช้การสะท้อน Kotlin (kotlin.reflect) ไลบรารีการสะท้อนจะแยกวิเคราะห์ข้อมูลเมตานี้ในรันไทม์เพื่อตรวจสอบโครงสร้างคลาส
ในโหมดเต็มของ R8 ระบบจะนำคำอธิบายประกอบออกโดยค่าเริ่มต้นหากไม่ได้เก็บคำอธิบายประกอบไว้อย่างชัดเจน นอกจากนี้ หาก R8 ลดขนาดหรือย่อคลาสโดยไม่ได้เก็บและอัปเดตข้อมูลเมตา การสะท้อน Kotlin จะล้มเหลวในรันไทม์ ซึ่งจะทำให้เกิดลักษณะการทำงานที่ไม่คาดคิดหรือข้อขัดข้อง (เช่น KotlinReflectionInternalError)
คุณต้องเก็บคำอธิบายประกอบที่มองเห็นได้ในรันไทม์และเก็บคลาส kotlin.Metadata ไว้โดยชัดเจนเพื่อป้องกันลักษณะการทำงานที่ไม่คาดคิดและให้แน่ใจว่าการสะท้อน Kotlin จะทำงานอย่างถูกต้องหลังจากการลดขนาด
# Preserve runtime-visible annotations required for inspecting metadata
-keepattributes RuntimeVisibleAnnotations
# Keep Kotlin metadata to ensure kotlin.reflect functions correctly
-keep class kotlin.Metadata { *; }