ปลดล็อกศักยภาพอย่างเต็มรูปแบบของเครื่องมือเพิ่มประสิทธิภาพ R8

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 ที่เฉพาะเจาะจงเพื่อรักษาการเป็นสมาชิก ซึ่งจะรักษาการมองเห็นเดิมของสมาชิกไว้ด้วย

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