Jetpack Keep ช่วยให้คุณประกาศการใช้การสะท้อนและ Java Native Interface (JNI) ด้วยคำอธิบายประกอบในซอร์สโค้ด Kotlin หรือ Java ได้โดยตรง แทนที่จะประกาศในไฟล์กฎการเก็บรักษาแยกต่างหาก ปลั๊กอิน Jetpack Keep Gradle จะอ่านคำอธิบายประกอบเหล่านี้ในเวลาบิลด์และสร้างกฎการเก็บรักษาที่ R8 ต้องการ
Jetpack Keep ช่วยให้การเพิ่มประสิทธิภาพ R8 ยังคงมีประสิทธิภาพเมื่อฐานของโค้ดมีการเปลี่ยนแปลง ดังนี้
กฎ Keep จะอยู่ถัดจากโค้ดที่ต้องใช้กฎดังกล่าว เมื่อคุณเปลี่ยนชื่อหรือลบโค้ดที่ใช้การสะท้อน คุณจะอัปเดตคำอธิบายประกอบในตำแหน่งเดียวกัน จึงไม่มีไฟล์กฎแยกต่างหากที่ต้องซิงค์ให้ตรงกัน
กฎที่สร้างขึ้นเป็นแบบมีเงื่อนไข R8 จะเก็บเป้าหมายของการเรียกแบบสะท้อนไว้ก็ต่อเมื่อเก็บโค้ดที่ทำการเรียกไว้ด้วย หากไม่ได้ใช้โค้ดการเรียกและ R8 นำโค้ดออก (มักเป็นกรณีในฟีเจอร์ไลบรารีที่ไม่บังคับ) R8 ก็สามารถนำเป้าหมายออกได้ด้วย ดังนั้นกฎจากฟีเจอร์ที่ไม่ได้ใช้จะไม่เพิ่มภาระ
Lint จะค้นหาการสะท้อนให้คุณ ไลบรารีประกอบด้วยการตรวจสอบ Lint ที่แจ้งว่ามีการเรียกใช้การสะท้อนที่ไม่มีคำอธิบายประกอบ และสามารถเพิ่มคำอธิบายประกอบจำนวนมากโดยอัตโนมัติ
วิธีการทำงานของ Jetpack Keep
พิจารณาฟังก์ชันที่สร้างอินสแตนซ์คลาสตามชื่อ หาก R8 ไม่ทราบว่าโค้ดของคุณใช้คลาสนี้ ก็จะนำคอนสตรัคเตอร์ของคลาสออก และแอปจะขัดข้องขณะรันไทม์ เพื่อป้องกันไม่ให้เกิดข้อขัดข้อง คุณสามารถเพิ่มกฎต่อไปนี้ลงในไฟล์กฎ Keep
# Unconditional: R8 keeps this constructor even if it removes the reflection that invokes it.
-keep class com.example.ResourceLocatorImpl {
<init>();
}
เมื่อใช้ Jetpack Keep คุณจะใส่คำอธิบายประกอบฟังก์ชันที่ทำการรีเฟล็กชันแทน ดังนี้
// ResourceLocatorImpl isn't on this module's compile classpath, so both the
// annotation and the code refer to it by name.
@UsesReflectionToConstruct(
className = "com.example.ResourceLocatorImpl",
// Omitting parameterTypes keeps every constructor. An empty array keeps
// only the no-argument constructor, which the code calls.
parameterTypes = [],
)
fun loadResourceLocator(): ResourceLocator {
val implClass = Class.forName("com.example.ResourceLocatorImpl")
return implClass.getDeclaredConstructor().newInstance() as ResourceLocator
}
เมื่อสร้าง ปลั๊กอิน Gradle จะสร้างกฎการเก็บรักษาสำหรับคอนสตรัคเตอร์
กฎที่สร้างขึ้นจะมีเงื่อนไข ซึ่งแตกต่างจากกฎที่เขียนด้วยตนเองก่อนหน้านี้ โดย R8 จะเก็บคอนสตรัคเตอร์ไว้ก็ต่อเมื่อ loadResourceLocator ยังคงอยู่ในแอปที่เพิ่มประสิทธิภาพแล้วเท่านั้น
ข้อกำหนด
หากต้องการใช้ Jetpack Keep โปรเจ็กต์ของคุณต้องใช้ปลั๊กอิน Android Gradle (AGP) 8.7 ขึ้นไป
ปลั๊กอิน Gradle รองรับโมดูลประเภทต่อไปนี้
| ประเภทโมดูล | ปลั๊กอินที่โมดูลใช้ | การตั้งค่าเพิ่มเติม |
|---|---|---|
| แอป Android | com.android.application |
ไม่มี |
| ไลบรารี Android | com.android.library |
ไม่มี |
| ไลบรารี Kotlin Multiplatform ที่มีเป้าหมายเป็น Android | com.android.kotlin.multiplatform.library |
ไม่มี |
| ไลบรารี JVM (Java หรือ Kotlin) | java-library หรือ org.jetbrains.kotlin.jvm |
ลงทะเบียนการเปลี่ยนรูปแบบ JAR |
ตั้งค่า Jetpack Keep
หากต้องการใช้ Jetpack Keep ให้เพิ่มทั้งไลบรารีและปลั๊กอิน Gradle ลงในแต่ละโมดูลที่ใช้การสะท้อนหรือ JNI ไลบรารีจะให้คำอธิบายประกอบและการตรวจสอบ Lint และปลั๊กอินจะเปลี่ยนคำอธิบายประกอบเป็นกฎการเก็บรักษา
เพิ่มไลบรารีและปลั๊กอินในไฟล์
libs.versions.tomlไลบรารีและปลั๊กอินจะใช้เวอร์ชันเดียวกัน ดังนี้[versions] # The library and the plugin ship together. Use one version for both so # that they stay in sync when you upgrade. annotationKeep = "1.0.0-alpha01" [libraries] androidx-annotation-keep = { group = "androidx.annotation.keep", name = "keep", version.ref = "annotationKeep" } [plugins] androidx-annotation-keep = { id = "androidx.annotation.keep", version.ref = "annotationKeep" }Gradle จะดาวน์โหลดปลั๊กอินจากที่เก็บ Maven ของ Google ดังนั้นโปรดตรวจสอบว่าที่เก็บ
pluginManagementในไฟล์settings.gradle.ktsมีgoogle()ในไฟล์
build.gradle.ktsระดับรูท ให้ประกาศปลั๊กอินควบคู่ไปกับ AGP โดยไม่ต้องใช้ปลั๊กอินplugins { alias(libs.plugins.android.application) apply false // Other plugins. // The plugin relies on AGP classes that it doesn't bundle, so declare // it next to AGP. Declaring it here also gives every module the same // plugin version. alias(libs.plugins.androidx.annotation.keep) apply false }ในไฟล์
build.gradle.ktsของแต่ละโมดูลที่ใช้การสะท้อนหรือ JNI ให้ใช้ปลั๊กอินและเพิ่มทรัพยากร Dependency ของไลบรารีplugins { alias(libs.plugins.android.application) // The plugin reads annotations only from this module's own code, so // apply it in every module that uses Jetpack Keep. alias(libs.plugins.androidx.annotation.keep) } dependencies { implementation(libs.androidx.annotation.keep) }ซิงค์โปรเจ็กต์กับไฟล์ Gradle
สำหรับตัวแปรบิลด์แต่ละรายการของโมดูลแอป Android ปลั๊กอินจะลงทะเบียนงาน <variant>ExtractKeepRules เช่น releaseExtractKeepRules และส่งกฎที่สร้างขึ้นไปยัง R8 สำหรับตัวแปรนั้น สำหรับโมดูลไลบรารี ปลั๊กอินจะแพ็กเกจกฎที่สร้างขึ้นเป็นกฎการเก็บรักษาของผู้ใช้ในเอาต์พุตของไลบรารี
โมดูลไลบรารี JVM ต้องมีการตั้งค่าเพิ่มเติม
ค้นหาการสะท้อนความรู้สึกด้วย Lint
Jetpack Keep มีการตรวจสอบ Lint ต่อไปนี้
ReflectionWithoutKeepAnnotations- แจ้งว่าการเรียกใช้แบบสะท้อน เช่น
Class.forName,getDeclaredConstructor,getDeclaredMethodและgetDeclaredFieldไม่มีคำอธิบายประกอบ Keep ที่ตรงกัน MissingKeepAnnotationPlugin- แจ้งว่าโมดูลที่ขึ้นอยู่กับ
androidx.annotation.keep:keepแต่ไม่ได้ใช้ปลั๊กอิน Gradleandroidx.annotation.keepหากไม่มีปลั๊กอิน บิลด์ของคุณจะไม่สร้างกฎการเก็บรักษาจากคำอธิบายประกอบ
หากต้องการใช้ Lint เพื่อกำกับเนื้อหาโมดูล Android ให้ทำตามขั้นตอนต่อไปนี้
เรียกใช้เครื่องมือวิเคราะห์ซอร์สโค้ดในโมดูลเพื่อค้นหาการเรียกแบบสะท้อนที่ต้องใช้คำอธิบายประกอบ
./gradlew :app:lintใช้การแก้ไขที่ Lint ทำได้โดยอัตโนมัติ:
./gradlew :app:lintFixLint สามารถเพิ่มคำอธิบายประกอบโดยอัตโนมัติเมื่อระบุได้ว่าโค้ดแสดงถึงคลาสและสมาชิกใด
ตรวจสอบคำอธิบายประกอบที่เพิ่มแต่ละรายการเพื่อยืนยันว่าตรงกับสิ่งที่โค้ดทำ เช่น ตรวจสอบว่าประเภทพารามิเตอร์ตรงกับคอนสตรักเตอร์หรือเมธอดที่โค้ดเรียกใช้
แก้ไขผลการตรวจสอบที่เหลือใน Android Studio เมื่อ Lint ไม่สามารถระบุคลาสเป้าหมายได้ การแก้ไขด่วนจะแทรกคำอธิบายประกอบพร้อมตัวยึดตำแหน่ง
TODOเพื่อให้คุณดำเนินการให้เสร็จสมบูรณ์ นอกจากนี้ คุณยังเพิ่มหมายเหตุด้วยตนเองได้ด้วยตัวอย่างเช่น ฟังก์ชันต่อไปนี้ได้รับชื่อคลาสเป็นพารามิเตอร์ ดังนั้น Lint จึงระบุไม่ได้ว่าสร้างคลาสใด การแก้ไขด่วนจะเพิ่มคำอธิบายประกอบพร้อมตัวยึดตำแหน่ง ดังนี้
@UsesReflectionToConstruct( className = TODO(), parameterTypes = [] ) fun createInstance(className: String): Any = Class.forName(className).getDeclaredConstructor().newInstance()แทนที่ตัวยึดตำแหน่งด้วยคลาสที่โค้ดของคุณสร้างขึ้น หากคลาสพร้อมใช้งานในเวลาคอมไพล์ ให้ใช้
classConstantแทนclassNameใน Kotlin โค้ดจะไม่คอมไพล์จนกว่าคุณจะแทนที่TODO()ตรวจสอบกฎการเก็บรักษาที่ปลั๊กอินสร้างขึ้นจากคำอธิบายประกอบก่อนที่จะนำกฎที่มีอยู่ออก สำหรับโมดูลแอป ให้เรียกใช้งานการแยกสำหรับตัวแปรของรุ่น ดังนี้
./gradlew :app:releaseExtractKeepRulesปลั๊กอินจะเขียนกฎลงใน
app/build/generated/release/androidx.annotation.keep.rules.proสำหรับโมดูลไลบรารี โปรดดูหัวข้อยืนยันกฎที่สร้างขึ้นนำกฎการเก็บรักษาที่คำอธิบายประกอบใหม่จะแทนที่ออกจากไฟล์กฎการเก็บรักษา กฎอาจครอบคลุมการสะท้อนที่ Lint ไม่ได้แจ้งว่าไม่เหมาะสมด้วย เช่น การสะท้อนในโมดูลอื่นหรือในไลบรารี ดังนั้นโปรดตรวจสอบกฎแต่ละข้อก่อนที่จะนำออก
- สำหรับกฎทั้งแพ็กเกจ เช่น
-keep class com.example.** { *; }โปรดตรวจสอบว่าคำอธิบายประกอบหรือกฎอื่นๆ ครอบคลุมการสะท้อนทั้งหมดในคลาสของแพ็กเกจ - สำหรับกฎที่เก็บคลาสหรือสมาชิกที่เฉพาะเจาะจงไว้ ให้ตรวจสอบว่ากฎดังกล่าวตรงกับกฎที่สร้างขึ้นอย่างใกล้ชิด หรือไม่มีการสะท้อนอื่นๆ ที่ขึ้นอยู่กับกฎนั้น
- สำหรับกฎทั้งแพ็กเกจ เช่น
ทดสอบบิลด์ที่เผยแพร่
ใส่คำอธิบายประกอบโค้ด
เพิ่มคำอธิบายประกอบ Jetpack Keep ลงในโค้ดที่ทำการรีเฟลกชัน ซึ่งได้แก่ฟังก์ชัน, ตัวสร้าง, ฟิลด์ หรือคลาสที่มีการเรียกใช้รีเฟลกชัน คำอธิบายประกอบจะอธิบายเป้าหมายของการสะท้อน และกฎที่สร้างขึ้นจะเก็บเป้าหมายนั้นไว้ก็ต่อเมื่อ R8 เก็บโค้ดที่มีคำอธิบายประกอบไว้เท่านั้น
เลือกคำอธิบายประกอบตามสิ่งที่โค้ดทำ ดังนี้
| หมายเหตุ | ใช้เมื่อโค้ดของคุณ |
|---|---|
@UsesReflectionToConstruct |
เรียกใช้คอนสตรัคเตอร์ผ่านการสะท้อน เช่น ด้วย getDeclaredConstructor().newInstance() |
@UsesReflectionToAccessMethod |
เรียกใช้เมธอดผ่านการสะท้อน เช่น ด้วย getMethod หรือ getDeclaredMethod |
@UsesReflectionToAccessField |
อ่านหรือเขียนฟิลด์ผ่านการสะท้อน เช่น ด้วย getField หรือ getDeclaredField |
@UnconditionallyKeep |
มีการเรียกจากภายนอกโค้ด Kotlin หรือ Java ของแอป เช่น จากโค้ดแบบเนทีฟผ่าน JNI ใช้คำอธิบายประกอบนี้เฉพาะเมื่อคุณไม่สามารถใช้คำอธิบายประกอบ @UsesReflection* ซึ่งสร้างกฎแบบมีเงื่อนไขได้ |
คำอธิบายประกอบ @UsesReflection* ทั้งหมดเป็นไปตามรูปแบบต่อไปนี้
- ระบุคลาสเป้าหมายด้วย
classConstantหรือclassNameหากคลาสพร้อมใช้งานที่เวลาคอมไพล์ ให้ใช้classConstantคอมไพเลอร์จะตรวจสอบการอ้างอิง และการเปลี่ยนโครงสร้างภายในโค้ดของ IDE เช่น การเปลี่ยนชื่อจะอัปเดตการอ้างอิงดังกล่าว ใช้classNameเฉพาะเมื่อคลาสไม่พร้อมใช้งานในเวลาคอมไพล์ เช่น คลาสในการขึ้นต่อกันที่ไม่บังคับ พารามิเตอร์ 2 รายการนี้แยกกันอย่างสิ้นเชิง - กำหนดเป้าหมายคลาสที่เจาะจงที่สุด กฎที่สร้างขึ้นจะเก็บสมาชิกที่ตรงกันในคลาสที่คุณระบุและในคลาสย่อยหรือการใช้งานอินเทอร์เฟซ ยิ่งคลาสเป็นแบบทั่วไปมากเท่าใด R8 ก็จะเก็บโค้ดไว้มากขึ้นเท่านั้น
- ระบุประเภทพารามิเตอร์เพื่อเก็บโอเวอร์โหลดเดียว หากคุณไม่ได้ตั้งค่า
parameterTypesหรือparameterTypeNamesกฎจะเก็บตัวสร้างทั้งหมดหรือการโอเวอร์โหลดทั้งหมดของเมธอดไว้ หากต้องการเก็บเฉพาะคอนสตรักเตอร์หรือเมธอดที่ไม่มีอาร์กิวเมนต์ ให้ส่งอาร์เรย์ที่ว่างเปล่า - ทำซ้ำคำอธิบายประกอบสำหรับเป้าหมายหลายรายการ หากฟังก์ชันหนึ่งแสดงผลในคลาสหรือสมาชิกมากกว่า 1 รายการ ให้เพิ่มคำอธิบายประกอบ 1 รายการสำหรับแต่ละเป้าหมาย
สร้างวัตถุด้วยการสะท้อน
ใช้ @UsesReflectionToConstruct เมื่อโค้ดสร้างอินสแตนซ์คลาสผ่านการสะท้อน ในตัวอย่างต่อไปนี้ ตัวรันงานจะสร้างอินสแตนซ์การใช้งานอินเทอร์เฟซ StartupTask ตามชื่อ เนื่องจากคำอธิบายประกอบระบุอินเทอร์เฟซ กฎที่สร้างขึ้นจึงเก็บคอนสตรัคเตอร์ที่ไม่มีอาร์กิวเมนต์ของการติดตั้งใช้งาน StartupTask ทุกรายการไว้ ตราบใดที่ R8 ยังคงเก็บ execute ไว้
interface StartupTask {
fun run()
}
object TaskRunner {
// The task class is known only at runtime, so target the interface that
// every task implements.
@UsesReflectionToConstruct(classConstant = StartupTask::class, parameterTypes = [])
fun execute(className: String) {
val task = Class.forName(className)
.getDeclaredConstructor()
.newInstance() as StartupTask
task.run()
}
}
เรียกใช้เมธอดด้วยการสะท้อน
ใช้ @UsesReflectionToAccessMethod เมื่อโค้ดเรียกใช้เมธอดผ่านการสะท้อน ตั้งค่า methodName เป็นชื่อของเมธอด ในตัวอย่างต่อไปนี้ โค้ดจะเรียกใช้เมธอดแบบคงที่ในการขึ้นต่อกันที่ไม่บังคับ คลาสนี้ไม่พร้อมใช้งานในเวลาคอมไพล์ ดังนั้นคำอธิบายประกอบจึงใช้ className แทน classConstant ดังนี้
object CrashReporting {
@UsesReflectionToAccessMethod(
className = "com.example.crashlib.CrashReporter",
methodName = "log",
// Omitting parameterTypes keeps every log overload. Keep only the one
// that the code calls.
parameterTypes = [String::class],
)
fun log(message: String) {
val reporterClass = try {
Class.forName("com.example.crashlib.CrashReporter")
} catch (e: ClassNotFoundException) {
// Crash reporting is optional, so skip it instead of failing.
return
}
reporterClass.getMethod("log", String::class.java).invoke(null, message)
}
}
เข้าถึงฟิลด์ด้วยการสะท้อน
ใช้ @UsesReflectionToAccessField เมื่อโค้ดของคุณอ่านหรือเขียนฟิลด์ผ่านการสะท้อน ตั้งค่า fieldName เป็นชื่อของช่อง ดังนี้
class UserProfileFetcher {
// User doesn't expose internalId, so reading it requires reflection.
@UsesReflectionToAccessField(classConstant = User::class, fieldName = "internalId")
fun readInternalId(user: User): Any? {
val field = User::class.java.getDeclaredField("internalId")
field.isAccessible = true
return field.get(user)
}
}
เก็บโค้ดที่เรียกจากภายนอกแอปไว้
ใช้ @UnconditionallyKeep สำหรับโค้ดที่เรียกจากภายนอกโค้ด Kotlin หรือ Java ของแอป เช่น เมธอดที่โค้ดแบบเนทีฟเรียกผ่าน JNI ใส่คำอธิบายประกอบองค์ประกอบที่เรียกใช้ ไม่ใช่โค้ดที่เรียกใช้องค์ประกอบนั้น
class NativeBridge {
// Only native code calls this method, through a JNI lookup by name that
// R8 can't see. Keep the method and its name unconditionally.
@UnconditionallyKeep
fun onNativeEvent(message: String) {
Log.d("NativeBridge", "Received from native code: $message")
}
// The default proguard-android-optimize.txt file already keeps native
// method names, so this method doesn't need an annotation.
external fun startNativeProcess()
}
กฎที่สร้างขึ้นจะเก็บเมธอดที่มีคำอธิบายประกอบและคลาสที่มีเมธอดดังกล่าวไว้โดยใช้ชื่อเดิม แต่จะไม่เก็บคลาสอื่นๆ ที่ซิกเนเจอร์ของเมธอดอ้างอิงไว้ หากโค้ดแบบเนทีฟขึ้นอยู่กับคลาสเหล่านั้น ให้ใส่คำอธิบายประกอบด้วย หรือเพิ่มกฎการเก็บรักษาสำหรับคลาสเหล่านั้น ดูข้อมูลเพิ่มเติมเกี่ยวกับ JNI และ R8 ได้ที่การโต้ตอบกับโค้ดแบบเนทีฟผ่าน JNI
โดยค่าเริ่มต้น @UnconditionallyKeep จะคงชื่อขององค์ประกอบไว้ หากผู้เรียกภายนอกไม่ได้ขึ้นอยู่กับชื่อ ให้ตั้งค่า shouldPreserveName = false เพื่อให้ R8 เปลี่ยนชื่อองค์ประกอบได้
ยืนยันกฎที่สร้างขึ้น
ส่วนนี้จะอธิบายวิธีตรวจสอบกฎที่สร้างขึ้นสำหรับโมดูลแต่ละประเภท สำหรับโมดูลแอป ปลั๊กอินจะส่งกฎไปยัง R8 โดยตรง สำหรับโมดูลไลบรารี ปลั๊กอินจะแพ็กเกจกฎเป็นกฎการเก็บรักษาของผู้ใช้ในเอาต์พุตของไลบรารี แอปที่ขึ้นอยู่กับไลบรารีของคุณจะใช้กฎเหล่านี้โดยอัตโนมัติเมื่อเรียกใช้ R8 แม้ว่าแอปจะไม่ได้ใช้ Jetpack Keep ก็ตาม ดูข้อมูลเพิ่มเติมเกี่ยวกับกฎการเก็บรักษาของผู้ใช้ได้ที่การเพิ่มประสิทธิภาพสำหรับผู้เขียนไลบรารี
แอป Android
หากต้องการตรวจสอบว่าปลั๊กอินสร้างกฎการเก็บรักษาใดสำหรับโมดูลแอป ให้เรียกใช้งานการแยกสำหรับตัวแปรบิลด์ ดังนี้
./gradlew :app:releaseExtractKeepRules
ปลั๊กอินจะเขียนกฎสำหรับแต่ละตัวแปรลงใน app/build/generated/<variant>/androidx.annotation.keep.rules.pro ความคิดเห็น # context: ก่อนกฎแต่ละข้อจะระบุโค้ดที่มีคำอธิบายประกอบซึ่งสร้างกฎนั้นขึ้นมา
หากต้องการตรวจสอบว่ากฎที่สร้างขึ้นส่งผลต่อการเพิ่มประสิทธิภาพร่วมกับกฎการเก็บรักษาอื่นๆ อย่างไร ให้ใช้เครื่องมือวิเคราะห์การกำหนดค่า R8
ไลบรารี Android
โมดูลไลบรารี Android ไม่จำเป็นต้องมีการตั้งค่าเพิ่มเติม เมื่อคุณสร้าง AAR ปลั๊กอินจะเพิ่มกฎที่สร้างขึ้นลงในไฟล์ proguard.txt ของ AAR หลังจากกฎการเก็บรักษาของผู้ใช้ที่ AAR มีอยู่แล้ว ปลั๊กอินจะลงทะเบียนงาน <variant>KeepRulesTransformAar สำหรับตัวแปรบิลด์แต่ละรายการเพื่อดำเนินการนี้
หากต้องการยืนยันกฎ ให้สร้าง AAR เช่น ด้วย ./gradlew
:mylibrary:assembleRelease และเปิด proguard.txt ใน AAR ที่สร้างขึ้น กฎที่สร้างขึ้นจะปรากฏในส่วนที่มีส่วนหัวต่อไปนี้
#################################################################
# Rules generated from androidx.annotation.keep annotations #
#################################################################
ใน macOS และ Linux คุณยังพิมพ์กฎจากบรรทัดคำสั่งได้ด้วย โดยทำดังนี้
unzip -p mylibrary/build/intermediates/aar/release/releaseKeepRulesTransformAar/mylibrary-release.aar proguard.txt
ไลบรารี Kotlin Multiplatform
โมดูลไลบรารี Kotlin Multiplatform ที่มีเป้าหมายเป็น Android ไม่จำเป็นต้องมีการตั้งค่าเพิ่มเติมเช่นกัน เมื่อคุณสร้าง AAR สำหรับเป้าหมาย Android ปลั๊กอินจะเพิ่มกฎที่สร้างขึ้นลงในไฟล์ proguard.txt ของ AAR ในลักษณะเดียวกับไลบรารี Android ปลั๊กอินจะลงทะเบียนงาน androidMainKeepRulesTransformAar เพื่อดำเนินการนี้
หากต้องการยืนยันกฎ ให้เรียกใช้งานนั้นและเปิด proguard.txt ใน AAR ที่สร้างขึ้น ใน macOS และ Linux คุณสามารถพิมพ์กฎจากบรรทัดคำสั่งได้โดยทำดังนี้
./gradlew :mylibrary:androidMainKeepRulesTransformAar
unzip -p mylibrary/build/intermediates/aar/androidMain/androidMainKeepRulesTransformAar/mylibrary.aar proguard.txt
ไลบรารี JVM
ก่อนที่จะยืนยันกฎสำหรับโมดูลไลบรารี JVM ได้ ให้กำหนดค่าโมดูลเพื่อให้ปลั๊กอินเพิ่มกฎที่สร้างขึ้นลงในสำเนาของ JAR จากนั้นเรียกใช้งานการเปลี่ยนรูปแบบที่คุณลงทะเบียนไว้และตรวจสอบไฟล์กฎใน JAR ที่สร้างขึ้น ตัวอย่างเช่น หากคุณตั้งชื่อภารกิจว่า keepRulesJar คุณจะพิมพ์กฎใน macOS และ Linux ได้ด้วยคำสั่งต่อไปนี้
./gradlew :mylibrary:keepRulesJar
unzip -p mylibrary/build/intermediates/annotation-keep/keepRulesJar/mylibrary-keep-rules.jar \
"META-INF/com.android.tools/*" "META-INF/proguard/*"
กำหนดค่าไลบรารี JVM
สำหรับโมดูลไลบรารี JVM ที่สร้าง JAR ปลั๊กอินจะไม่แก้ไข JAR โดยอัตโนมัติ แต่ให้ใช้ฟังก์ชัน annotationKeep.registerJavaArchiveTransform เพื่อลงทะเบียนงานที่สร้างสำเนาของ JAR ด้วยกฎที่สร้างขึ้น แล้วเผยแพร่ JAR นั้นแทน เพิ่มโค้ดต่อไปนี้ลงในไฟล์ build.gradle.kts ของโมดูลไลบรารี
plugins {
id("java-library")
alias(libs.plugins.jetbrains.kotlin.jvm)
alias(libs.plugins.androidx.annotation.keep)
}
val jarTask = tasks.named<Jar>("jar")
// Apps run R8 on this library's code, so the generated rules must be inside
// the JAR. The plugin handles this automatically only for Android modules.
val keepRulesJarTask = annotationKeep.registerJavaArchiveTransform(
taskName = "keepRulesJar",
input = jarTask.flatMap { it.archiveFile },
fileName = jarTask.flatMap { it.archiveFileName }.map { it.replace(".jar", "-keep-rules.jar") },
)
// Consumers of this library resolve apiElements and runtimeElements, so add
// the JAR that contains the generated rules to them.
configurations.configureEach {
if (name == "apiElements" || name == "runtimeElements") {
outgoing.artifact(keepRulesJarTask.flatMap { it.outputJar })
}
}
dependencies {
implementation(libs.androidx.annotation.keep)
}
การเปลี่ยนรูปแบบจะเพิ่มกฎที่สร้างขึ้นไปยังตำแหน่งกฎ R8 ที่กำหนดเป้าหมายของ JAR
META-INF/com.android.tools/r8/proguard.txt หาก JAR มีกฎใน META-INF/com.android.tools/ หรือ META-INF/proguard/ อยู่แล้ว การเปลี่ยนรูปแบบจะเพิ่มกฎที่สร้างขึ้นต่อท้ายไฟล์กฎที่มีอยู่แทน ดูข้อมูลเพิ่มเติมเกี่ยวกับตำแหน่งเหล่านี้ได้ที่รองรับ R8 เวอร์ชันต่างๆ
หลังจากลงทะเบียนการเปลี่ยนรูปแบบแล้ว ให้ยืนยันกฎใน JAR
ตัวอย่าง: โหลดไลบรารีส่วนเสริมที่ไม่บังคับ
ไลบรารีมักใช้การสะท้อนเพื่อโหลดส่วนเสริมที่ไม่บังคับ เช่น Analytics SDK อาจจัดส่งไลบรารีหลักและส่วนเสริมวิดีโอ ส่วนเสริมจะขึ้นอยู่กับไลบรารีหลัก ดังนั้นไลบรารีหลักจึงไม่สามารถอ้างอิงคลาสของส่วนเสริมได้โดยตรง แต่ไลบรารีหลักจะค้นหาคลาสของส่วนเสริมตามชื่อที่รันไทม์และเปิดใช้การวิเคราะห์วิดีโอเฉพาะในกรณีที่พบคลาสเท่านั้น
เนื่องจากไลบรารีหลักใช้คลาสของส่วนเสริมผ่านการสะท้อนเท่านั้น R8 จึงอาจนำคลาสหรือคอนสตรักเตอร์ออกจากบิลด์ที่เผยแพร่ของแอป ซึ่งทำให้การค้นหาไม่สำเร็จ ความล้มเหลวนี้เกิดขึ้นเฉพาะในบิลด์ที่เผยแพร่ของแอปที่มีส่วนเสริม จึงตรวจจับได้ยากในการทดสอบ
หากไม่มี Jetpack Keep ส่วนเสริมจะจัดส่งกฎ Keep ของผู้บริโภคสำหรับคลาสของตน ดังที่แสดงในการสะท้อนเพื่อรองรับทรัพยากร Dependency ที่ไม่บังคับ
# In the add-on's consumer keep rules file.
-keep class com.example.analytics.video.VideoEventTracker {
<init>();
}
กฎนี้อยู่ในไลบรารีที่แตกต่างจากการค้นหาที่ต้องการกฎนี้ ดังนั้นทั้งสองจึงอาจไม่ซิงค์กัน เมื่อใช้ Jetpack Keep คุณจะใส่คำอธิบายประกอบการค้นหาในไลบรารีหลักแทน ดังนี้
// In the core library.
interface AnalyticsPlugin {
fun start()
}
object AnalyticsManager {
// The annotation and the lookup share this constant so that they can't
// refer to different classes.
private const val VIDEO_TRACKER_CLASS =
"com.example.analytics.video.VideoEventTracker"
@UsesReflectionToConstruct(
className = VIDEO_TRACKER_CLASS,
parameterTypes = [],
)
fun initialize() {
val tracker = try {
Class.forName(VIDEO_TRACKER_CLASS)
.getDeclaredConstructor()
.newInstance() as AnalyticsPlugin
} catch (e: ClassNotFoundException) {
// Apps that don't play video leave out the add-on, so a missing
// class isn't an error.
return
}
// This is an ordinary interface call, not a reflective one, so R8
// keeps start() without an annotation.
tracker.start()
}
}
คำอธิบายประกอบเป็นไปตามหลักเกณฑ์ต่อไปนี้
- ใส่คำอธิบายประกอบโค้ดที่ทำการสะท้อน คำอธิบายประกอบจะอยู่ใน
initializeซึ่งเรียกใช้Class.forNameไม่ใช่ในคลาสของส่วนเสริม - เลือกคำอธิบายประกอบตามสิ่งที่โค้ดทำ การค้นหาจะสร้างอินสแตนซ์ของคลาส ดังนั้นโค้ดจึงใช้
@UsesReflectionToConstruct - ใช้
classNameสำหรับคลาสที่คุณอ้างอิงไม่ได้ คลาสของส่วนเสริมไม่ได้อยู่ในคลาสพาธการคอมไพล์ของไลบรารีหลัก ดังนั้นคำอธิบายประกอบจึงใช้classConstantไม่ได้ - เก็บเฉพาะคอนสตรัคเตอร์ที่โค้ดเรียกใช้ การค้นหาจะเรียกใช้คอนสตรัคเตอร์ที่ไม่มีอาร์กิวเมนต์ ดังนั้น
parameterTypesจึงเป็นอาร์เรย์ว่างเปล่า
เมื่อคุณสร้าง AAR ของไลบรารีหลัก ปลั๊กอินจะเพิ่มกฎที่เก็บ VideoEventTracker และคอนสตรัคเตอร์ที่ไม่มีอาร์กิวเมนต์ไว้ทุกครั้งที่ R8 เก็บ initialize ไว้ แอปที่ใช้ SDK จะใช้กฎนี้โดยอัตโนมัติ แม้ว่าจะไม่ได้ใช้ Jetpack Keep ก็ตาม ดังนั้นส่วนเสริมจึงไม่จำเป็นต้องมีกฎของตัวเอง
เมื่อเทียบกับกฎของส่วนเสริม วิธีนี้มีข้อดีดังต่อไปนี้
- กฎจะอยู่ถัดจากการค้นหา ไลบรารีหลักเป็นเจ้าของทั้งการค้นหาและกฎ คุณจึงอัปเดตทั้ง 2 อย่างพร้อมกันได้แทนที่จะต้องประสานงานการเปลี่ยนแปลงในไลบรารี 2 รายการ
- R8 ยังคงนำโค้ดที่ไม่ได้ใช้ออกได้ หากแอปมีส่วนเสริมแต่ไม่เคยเรียกใช้
initializeกฎที่สร้างขึ้นจะไม่มีผล และ R8 จะนำVideoEventTrackerออกได้ กฎของส่วนเสริมจะเก็บส่วนเสริมไว้ในทุกแอปที่มีส่วนเสริมนั้น - Lint จะตรวจจับการค้นหาที่ไม่มีคำอธิบายประกอบ หากไลบรารีหลักเพิ่มการค้นหาแบบสะท้อนอีกรายการโดยไม่มีคำอธิบายประกอบในภายหลัง Lint จะแจ้งว่ามีปัญหาเพื่อให้คุณแก้ไขก่อนเผยแพร่ไลบรารี
หากต้องการยืนยันผลลัพธ์ ให้ตรวจสอบว่ากฎที่สร้างขึ้นปรากฏในไฟล์ proguard.txt ของ AAR ตามที่อธิบายไว้ในไลบรารี Android จากนั้นทดสอบบิลด์ที่เผยแพร่ของแอปที่มีส่วนเสริม เนื่องจากกฎที่ขาดหายไปจะทำให้การวิเคราะห์วิดีโอใช้งานไม่ได้
ข้อจำกัด
Jetpack Keep ยังตรวจไม่พบการสะท้อนที่ค้นหาคลาสและสมาชิกตามคำอธิบายประกอบ หรือสร้างกฎการเก็บไว้สำหรับคลาสและสมาชิกดังกล่าว เราวางแผนเปิดตัวฟีเจอร์นี้แล้ว
จนกว่าจะถึงตอนนั้น ให้ใช้กฎการเก็บรักษาสำหรับการพิจารณาประเภทนี้ ตัวอย่างเช่น กฎต่อไปนี้จะเก็บสมาชิกทุกรายที่มีคำอธิบายประกอบเป็น @SomeAnnotation ไว้ในคลาสที่ R8 เก็บไว้
-keepclassmembers class * {
@com.example.annotation.SomeAnnotation <methods>;
@com.example.annotation.SomeAnnotation <fields>;
}
หาก Jetpack Keep ไม่เหมาะกับกรณีการใช้งานของคุณ หรือหากคุณมีความคิดเห็นอื่นๆ โปรดแจ้งปัญหา
แหล่งข้อมูลเพิ่มเติม
- ตัวอย่าง Jetpack Keep: โปรเจ็กต์ Android Studio ที่ใช้ Jetpack Keep ในแอป, ไลบรารี Android, ไลบรารี Kotlin Multiplatform และไลบรารี JVM
- กรณีการใช้งานและตัวอย่างของกฎ Keep: กฎ Keep สำหรับสถานการณ์การสะท้อนและ JNI ทั่วไป
- ทำตามแนวทางปฏิบัติแนะนำ: คำแนะนำในการเขียนกฎการเก็บรักษาที่มีขอบเขตแคบ