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 กฎการเก็บรักษาที่จำเป็นต่อการป้องกันปัญหานี้มีดังนี้
// 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 ในโหมดเต็มจะลบข้อมูลประเภทที่จำเป็นออก ทำให้ การยกเลิกการซีเรียลไลซ์ล้มเหลว
ตั้งแต่ Gson เวอร์ชัน 2.11.0 เป็นต้นไป ไลบรารีจะรวมกฎ keep ที่จำเป็นซึ่งจำเป็นสำหรับการดีซีเรียลไลซ์ในโหมดเต็ม เมื่อคุณสร้าง แอปโดยเปิดใช้ R8 แล้ว R8 จะค้นหาและใช้กฎเหล่านี้จาก ไลบรารีโดยอัตโนมัติ ซึ่งจะช่วยให้แอปได้รับการปกป้องที่จำเป็นโดยที่คุณไม่ต้องเพิ่มหรือดูแลรักษากฎที่เฉพาะเจาะจงเหล่านี้ในโปรเจ็กต์ด้วยตนเอง
คุณควรทราบว่ากฎที่แชร์ไว้ก่อนหน้านี้
แก้ปัญหาการค้นหาประเภททั่วไปเท่านั้น (เช่น List<User>)
R8 ยังเปลี่ยนชื่อฟิลด์ของคลาสด้วย หากคุณไม่ได้ใช้@SerializedName
คำอธิบายประกอบในโมเดลข้อมูล Gson จะไม่สามารถยกเลิกการซีเรียลไลซ์ JSON ได้เนื่องจาก
ชื่อฟิลด์จะไม่ตรงกับคีย์ JSON อีกต่อไป
อย่างไรก็ตาม หากคุณใช้ 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 ที่เฉพาะเจาะจงเพื่อรักษาการเป็นสมาชิก ซึ่งจะรักษาการมองเห็นเดิมของสมาชิกไว้ด้วย
ดูข้อมูลเพิ่มเติมได้ที่ตัวอย่างนี้เพื่อทำความเข้าใจว่าเหตุใดจึงไม่แนะนำให้เข้าถึงสมาชิกส่วนตัวโดยใช้การสะท้อน และกฎการเก็บเพื่อเก็บฟิลด์/เมธอดเหล่านั้น