วิศวกรด้านประสิทธิภาพของ Android
แอป Android มักจะจัดส่งโดยไม่ได้รับการเพิ่มประสิทธิภาพอย่างเหมาะสมเนื่องจากกฎการเก็บรักษาที่เข้มงวดเกินไป ซึ่งมักมาจากไลบรารีของบุคคลที่สาม ส่งผลให้ APK มีขนาดใหญ่เกินไป, Cold Start ช้าลง และประสิทธิภาพรันไทม์ลดลง
เกณฑ์ประสิทธิภาพใหม่ของ Google Play สำหรับการเพิ่มประสิทธิภาพ DEX ทำให้ระดับการเข้าถึงของแอปขึ้นอยู่กับการเพิ่มประสิทธิภาพโดยตรง
การพูดคุยนี้จะแนะนำเวิร์กโฟลว์ R8 สมัยใหม่เพื่อจัดการกับกฎที่มากเกินไป คุณจะได้เรียนรู้วิธีต่อไปนี้
ข้อความถอดเสียง
สวัสดีทุกคน
ฉันชื่อ Breana จากทีม Android DevRel
และผม Chris Craik จากทีมประสิทธิภาพของ Android
ยินดีต้อนรับสู่กฎการเก็บรักษาที่ปลอดภัยในการกำหนดประเภทและการเพิ่มประสิทธิภาพ DEX
วันนี้เราจะมาพูดถึงความหมายของการเพิ่มประสิทธิภาพ DEX เหตุผลที่คุณควรให้ความสำคัญ และข้อมูลอัปเดตล่าสุดที่น่าสนใจ
เป็นเวลานานแล้วที่การเพิ่มประสิทธิภาพ DEX ถูกมองว่าเป็นสิ่งที่มีก็ดี
คุณจึงอาจกำหนดค่าเพียงครั้งเดียวแล้วปล่อยทิ้งไว้ หรืออาจกลับมาดูอีกครั้งเมื่อมีเวลาว่าง
อย่างไรก็ตาม เนื่องจากอุตสาหกรรมอุปกรณ์เคลื่อนที่กำลังเผชิญกับข้อจำกัดด้านการจัดหาฮาร์ดแวร์ที่สำคัญซึ่งส่งผลต่อความพร้อมใช้งานของหน่วยความจำของอุปกรณ์ DEX ที่เพิ่มประสิทธิภาพมาอย่างดีจึงเป็นสิ่งสำคัญที่จะช่วยให้แอปใช้หน่วยความจำน้อยลง เริ่มทำงานได้เร็วขึ้น ลด ANR และปรับปรุงประสิทธิภาพการแสดงผลและรันไทม์โดยรวม
Google Play ได้ประกาศข้อกำหนดใหม่ด้านคุณภาพแอปพร้อมเกณฑ์ประสิทธิภาพที่จะเริ่มบังคับใช้ในเดือนกุมภาพันธ์ เพื่อรักษาระบบนิเวศของ Android ให้มีประสิทธิภาพและเพื่อให้แอปของคุณมอบประสบการณ์การใช้งานระดับพรีเมียมต่อไป
ซึ่งรวมถึงเกณฑ์สำหรับการเพิ่มประสิทธิภาพโค้ด DEX
แอปต้องลดขนาด เพิ่มประสิทธิภาพ และปรับให้ยากต่อการอ่าน (Obfuscation) มากกว่า 25%
แล้วสิ่งนี้มีความหมายต่อคุณในฐานะนักพัฒนาแอป Android อย่างไร
โค้ด DEX ที่ไม่ได้รับการเพิ่มประสิทธิภาพไม่เพียงส่งผลเสียต่อประสบการณ์ของผู้ใช้เท่านั้น แต่ยังอาจส่งผลต่อระดับการมองเห็นและความสามารถในการเผยแพร่แอปใน Play Store ด้วย
อย่างไรก็ตาม คุณมาถูกที่แล้ว และเมื่อจบการพูดคุยนี้ คุณจะมีเครื่องมือที่ไม่เพียงแต่ช่วยให้คุณทำตามข้อกำหนดเหล่านี้ได้ แต่ยังช่วยให้คุณทำได้เกินกว่าข้อกำหนดด้วย
การเพิ่มประสิทธิภาพโค้ด DEX ทำได้ด้วย R8 ซึ่งเป็นเครื่องมือเพิ่มประสิทธิภาพแอปของ Android
และแม้ว่าเราจะไม่ได้บังคับให้ใช้ R8 หากเครื่องมือเพิ่มประสิทธิภาพแอปอื่นสามารถทำให้แอปของคุณมีประสิทธิภาพสูงกว่าเกณฑ์เหล่านี้ แต่ก็เป็นแนวทางที่แนะนำซึ่งสร้างขึ้นใน Toolchain ของ Android โดยตรง
และแม้ว่า DEX ที่เพิ่มประสิทธิภาพมาอย่างดีและ App Bundle จะใช้หน่วยความจำน้อยลง แต่ประโยชน์หลักๆ ในที่นี้ยังคงเกี่ยวข้องกับประสิทธิภาพ ซึ่งได้แก่ ANR ที่น้อยลง การเริ่มต้นระบบที่เร็วขึ้น และประสิทธิภาพรันไทม์ที่ดีขึ้น
และการเพิ่มประสิทธิภาพแอปจะให้ผลลัพธ์ที่ดีต่อแอปและผู้ใช้ของคุณอย่างแท้จริงและพิสูจน์ได้
ตัวอย่างเช่น การใช้โหมดเต็มรูปแบบของ R8 และปรับแต่งกฎการเก็บรักษาช่วยให้ Grab เพิ่มประสิทธิภาพได้ 78% ลดขนาดได้ 79% และสร้างความสับสนได้ 82% สำหรับแอปของตน
คุณอาจดูสไลด์นี้แล้วคิดว่า "โอเค ตัวเลขพวกนั้นค่อนข้างสูง แต่จริงๆ แล้วหมายความว่ายังไง"
ผลจากการปรับปรุงเหล่านี้เมื่อรวมกับโปรไฟล์พื้นฐานทำให้บริษัทลด ANR ลงได้ 32% และปรับปรุงการเริ่มต้นแอปได้ 35%
เมื่อแอปยอดนิยมเริ่มใช้ R8 เป็นครั้งแรก แอปเหล่านี้เห็นว่าขนาด DEX ลดลงโดยเฉลี่ย 38% และ ANR ที่ผู้ใช้รับรู้ลดลง 16%
และหากคุณใช้ R8 อยู่แล้ว เรามีข่าวดีมาบอก
สำหรับแอปที่เปิดใช้ R8 อยู่แล้ว และดำเนินการเพื่อปรับปรุงระดับการเพิ่มประสิทธิภาพ การลดขนาด และการสร้างความสับสนให้มากกว่า 50% จะเห็นว่าขนาด DEX ลดลงอีก 25% โดยเฉลี่ย เวลาเริ่มต้นลดลง 4% และ ANR ที่ผู้ใช้รับรู้ลดลงอีก 17% ซึ่งเป็นตัวเลขที่เพิ่มขึ้นจาก 16% ที่เราแสดงในสไลด์ก่อนหน้า
วันนี้เราจึงอยากแนะนำวิธีที่จะช่วยให้คุณบรรลุเป้าหมายตัวเลขเหล่านี้
เราจะพูดถึงเครื่องมือที่ช่วยเพิ่มประสิทธิภาพโค้ด DEX และเปลี่ยนจากกฎการเก็บรักษาที่กว้างเกินไปและกำหนดค่าไม่ถูกต้องเป็นกฎที่เฉพาะเจาะจงและแคบลง
ก่อนอื่น เราอยากพูดถึงด้วยว่า R8 มีมานานหลายปีแล้ว
และเราทราบดีว่าแม้ว่าการเพิ่มประสิทธิภาพจะมีผลอย่างมาก แต่ก็ยังต้องใช้ความระมัดระวังอย่างมากในการกำหนดค่า
การเรียกใช้แบบสะท้อนหมายความว่าอินพุตคาดการณ์ไม่ได้ และอาจทำให้เกิดข้อขัดข้องขณะรันไทม์
ซึ่งทำให้ทั้งทีมแอปและทีมไลบรารีในอดีตพยายามเพิ่มกฎการเก็บรักษาที่กว้างเกินไปเพื่อความปลอดภัย
เราทราบดีว่า 41% ของแอปยอดนิยมมีระดับการเพิ่มประสิทธิภาพต่ำกว่า 25% ตามที่ Google Play กำหนด แต่เราก็ทราบด้วยว่า 13% ของแอปยอดนิยมมีระดับการเพิ่มประสิทธิภาพสูงกว่าเกณฑ์ดังกล่าวถึง 3 เท่า
ซึ่งแอปที่สามารถบรรลุเป้าหมายนี้ได้มาจากอุตสาหกรรมและเส้นทางของผู้ใช้ที่หลากหลาย
และคุณสามารถทำให้แอปมีคะแนนการเพิ่มประสิทธิภาพสูงได้ ตราบใดที่คุณมีเครื่องมือที่เหมาะสมและระดับการเข้าถึงที่เหมาะสมในการจัดการทั้งกฎการเก็บรักษาของคุณเองและกฎที่รับค่ามาจากไลบรารี
และก่อนที่เราจะเจาะลึกเกี่ยวกับเครื่องมือและการปรับปรุงใหม่ๆ ที่จะช่วยในเรื่องนี้ เรายังต้องการไฮไลต์การปรับปรุง R8 อื่นๆ อีก 2-3 รายการจากปีที่ผ่านมาซึ่งช่วยให้การเพิ่มประสิทธิภาพนี้ง่ายขึ้นเล็กน้อยด้วย
ประการแรก เทมเพลตแอปใหม่จะมีขอบเขตแพ็กเกจ ซึ่งช่วยให้มั่นใจได้ว่าการเพิ่มประสิทธิภาพจะอยู่ในระดับขั้นต่ำตั้งแต่เริ่มต้น
ตอนนี้เครื่องมือวิเคราะห์การกำหนดค่า R8 ซึ่งเราจะพูดถึงในอีกสักครู่รองรับ AGP เวอร์ชันเก่าแล้ว
และยังรองรับ Bazel อย่างไม่เป็นทางการด้วย
และใน AGP 9.2 โครูทีนพื้นฐานจะทำงานเร็วขึ้น 2 เท่า
และจากการปรับปรุง R8 ที่ทำใน AGP 9.2 ทำให้ Tinder พบว่า ANR ลดลง 13% หลังจากอัปเกรดจากเวอร์ชัน 9.0.1
นอกจากนี้ AGP 9.3 ยังมาพร้อมกับ R8 API ที่ออกแบบใหม่ซึ่งมีการตั้งชื่อที่ชัดเจนขึ้น ค่าเริ่มต้นที่ง่ายขึ้นสำหรับกฎเริ่มต้นและการลดขนาดทรัพยากร รวมถึงการรองรับไดเรกทอรีต้นทาง AGP สำหรับกฎการเก็บรักษา
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}การอัปเดตที่เราจะพูดถึงในวันนี้ยังเป็นการปรับปรุงประสบการณ์ของนักพัฒนาแอปด้วย
เราจะเริ่มต้นด้วยวิธีใช้ประโยชน์จากเครื่องมือวิเคราะห์การกำหนดค่า R8 และทักษะ R8 เพื่อระบุโอเวอร์เฮดของกฎเดิมและกำหนดแนวทางในการปรับปรุง
จากนั้น Chris จะอธิบายวิธีเปลี่ยนจากกฎการเก็บรักษาแบบกว้างๆ ที่ใช้ไวลด์การ์ดไปเป็นคำอธิบายประกอบใหม่ที่สร้างขึ้นโดยอัตโนมัติ มีความแม่นยำ และมีความปลอดภัยในการกำหนดประเภท
และสุดท้าย เราจะดูวิธีใช้เครื่องมือตรวจสอบ CLI ใหม่เพื่อให้มั่นใจว่าการเพิ่มประสิทธิภาพเชิงรุกจะไม่ทำให้เกิดข้อขัดข้อง
ดังนั้น การเริ่มต้นด้วยเครื่องมือวิเคราะห์การกำหนดค่า R8 จึงเป็นจุดเริ่มต้นที่เหมาะอย่างยิ่งสำหรับการตรวจสอบการเพิ่มประสิทธิภาพ DEX
หากคุณไม่เคยใช้เครื่องมือนี้มาก่อน หน้าตาของเครื่องมือจะเป็นดังนี้
ซึ่งเป็นรายงาน HTML ที่แสดงข้อมูลเกี่ยวกับการกำหนดค่า R8 ของคุณ
รายงานนี้มีไว้สำหรับแอปของฉัน ซึ่งมีไลบรารีชื่อ Analytics Bloat
และอย่างที่คุณคาดเดาได้ นี่ไม่ใช่แอปจริงหรือไลบรารีจริง แต่จะไฮไลต์เป็นสีน้ำเงินที่ด้านบน
เครื่องมือวิเคราะห์การกำหนดค่าจะวัดสิ่งต่อไปนี้ การลดขนาด ซึ่งเป็นเปอร์เซ็นต์ของคลาส เมธอด และฟิลด์ที่ R8 ไม่ได้ถูกป้องกันไม่ให้ลดขนาดโดยกฎการเก็บรักษา
และจะคำนวณหลังจากที่ R8 ทำการลดขนาดครั้งแรก
นอกจากนี้ ยังวัดการเพิ่มประสิทธิภาพ ซึ่งเป็นเปอร์เซ็นต์ของโค้ดที่เปิดให้เพิ่มประสิทธิภาพทั้งโปรแกรม เช่น การแทรกเมธอดและการผสานคลาส
และการสร้างความสับสน ซึ่งเป็นเปอร์เซ็นต์ของสมาชิกที่สามารถสร้างความสับสนหรือเปลี่ยนชื่อเป็นตัวระบุที่สั้นลงได้
นอกจากนี้ เราขอแจ้งให้ทราบว่าเนื่องจากเครื่องมือวิเคราะห์การกำหนดค่า R8 จะคำนวณคะแนนเหล่านี้ก่อนการเพิ่มประสิทธิภาพและการคอมไพล์ DEX ขั้นสุดท้าย ค่าที่คุณเห็นในรายงานนี้สำหรับการเพิ่มประสิทธิภาพ การลดขนาด และการปรับให้ยากต่อการอ่าน (Obfuscation) จึงจะไม่ตรงกับค่าที่คุณเห็นใน Play Console ทุกประการ
เนื่องจาก Google Play จะวัดเอาต์พุต DEX สุดท้าย
และสาเหตุของความคลาดเคลื่อนนี้ก็เพื่อให้มั่นใจว่าเมื่อคุณกำลังพัฒนา คุณจะเข้าถึงรายงานความคิดเห็นในเครื่องได้อย่างรวดเร็ว
แต่คะแนนที่คุณเห็นในเครื่องมือวิเคราะห์การกำหนดค่า R8 ยังคงมีประโยชน์อย่างมากในการตรวจสอบศักยภาพในการเพิ่มประสิทธิภาพ DEX ก่อนที่จะอัปโหลดไปยัง Play Store
นอกจากนี้ รายงานยังมีตารางการวิเคราะห์กฎการเก็บรักษา ซึ่งเป็นมุมมองหลักที่คุณเห็นเมื่อเข้าสู่รายงาน
จากนั้น คุณสามารถคลิกเข้าไปในกฎใดก็ได้เพื่อดูจำนวนคลาส เมธอด และฟิลด์ที่กฎนั้นๆ เก็บไว้ เพื่อให้คุณเห็นว่ากฎใดเก็บข้อมูลไว้มากกว่าที่ควร
นอกจากนี้ยังมีกฎที่รวมอยู่ด้วย ซึ่งเป็นกฎที่ซูมเข้าที่นี่
และกฎเหล่านี้เป็นกฎที่ทับซ้อนกับกฎที่กว้างกว่า
และระบบจะไฮไลต์คีย์เวิร์ดเหล่านี้เพื่อให้คุณนำคีย์เวิร์ดแบบกว้างออกและเก็บเฉพาะคีย์เวิร์ดที่แคบกว่าไว้
นอกจากนี้ ยังแสดงกฎที่ไม่ได้ใช้ ซึ่งเป็นกฎที่ลบได้โดยไม่เสี่ยงต่อความเสถียรของแอป
ดังนั้น เครื่องมือวิเคราะห์การกำหนดค่าจึงได้รับการออกแบบมาเพื่อแก้ไขปัญหาบางอย่าง โดยเริ่มจากการสะสมกฎเดิม
ก่อนหน้านี้ การลบกฎเก่าค่อนข้างมีความเสี่ยงเนื่องจากไม่มีระดับการเข้าถึงต่อกฎ
ทีมจึงมักจะปล่อยไว้เฉยๆ หรือเพิ่มไวลด์การ์ดที่กว้างขึ้น
ประการที่ 2 คือช่วยให้คุณค้นพบจุดบอดของกฎไลบรารี
ดังนั้นเมื่อคุณเพิ่ม SDK ในอดีต จึงเป็นเรื่องยากที่จะบอกได้ว่ากฎที่รวมมากับไลบรารีนั้นกว้างเกินไปหรือไม่ หรือจำเป็นต้องใช้หรือไม่ เช่น ในเวอร์ชันที่ 10 หรือเวอร์ชันหลังจากนั้น
และเราจะพูดถึงไลบรารีเพิ่มเติมในอีกสักครู่
และประการที่ 3 คือช่วยให้คุณประเมินผลกระทบของการเพิ่มประสิทธิภาพได้
ก่อนหน้านี้ ไม่มีวิธีง่ายๆ ที่จะบอกได้ว่ากฎหนึ่งๆ เก็บรักษาชั้นเรียนไว้ 2 ชั้นเรียนหรือ 2,000 ชั้นเรียน
มาดูวิธีการทำงานของ R8 ในบริบทของแอปและไลบรารีที่เราดูไปก่อนหน้านี้กัน
ในที่นี้ฉันมีโมดูลแอป 1 รายการและไลบรารี 3 รายการ
และให้ความสนใจกับไลบรารี 3 ทางด้านขวา
นั่นคือไลบรารี Analytics Bloat ซึ่งมีกฎการเก็บรักษาทั้งแพ็กเกจ
ก่อนอื่น R8 จะระบุจุดแรกเข้าของรูทจากกฎการเก็บรักษาที่ผสานรวมทั้งหมด ซึ่งไฮไลต์เป็นสีเหลืองที่นี่
และโดยทั่วไปแล้ว ควรเป็นจุดแรกเข้าหลักของแอป เช่น คลาสในไฟล์ Manifest
และเนื่องจากกฎการเก็บไวลด์การ์ดของไลบรารี Analytics Bloat ทุกคลาสในไลบรารีจึงถือเป็นจุดแรกเข้า
จากนั้น R8 จะติดตามความสามารถในการเข้าถึงใน Call Graph จากรากเหล่านั้น
คลาสที่เข้าถึงได้จากรูทจะได้รับการทำเครื่องหมายเพื่อเก็บรักษาไว้เป็นสีเขียว ส่วนคลาสที่ไม่มีการอ้างอิงจะจัดประเภทเป็นโค้ดที่ไม่ได้ใช้และทำเครื่องหมายเป็นสีแดงที่นี่
จากนั้น R8 จะทำการทรีเชคกิ้งและตัดโค้ดที่ไม่ได้อ้างอิงออก
ดังนั้น หลังจากกระบวนการ Tree Shaking หรือการลดขนาดครั้งแรก สิ่งที่เหลืออยู่ก็คือโค้ดเหล่านี้
และแอปของเรามีคะแนนการหดตัวอยู่ที่ 42.5% ซึ่งสูงกว่าเกณฑ์ 25% จาก Google Play แต่ก็ยังไม่ถึงจุดที่เราต้องการ
และสาเหตุหลักมาจากกฎการเก็บรักษาทั้งแพ็กเกจจากไลบรารี Analytics
ซึ่งทำให้มีชั้นเรียนเพียง 17 ชั้นเรียนจาก 40 ชั้นเรียนที่เหลือที่สามารถลดขนาดลงได้อีก
ประเด็นที่เราต้องการเน้นย้ำคือ กฎการเก็บรักษาที่ไม่ถูกต้องเพียงข้อเดียวอาจทำให้เกิดปัญหาใหญ่ในการเพิ่มประสิทธิภาพแอป
เมื่อทราบเช่นนี้แล้ว เรามาพูดถึงไลบรารีกันอย่างรวดเร็ว
ในปีที่ผ่านมา ทีมของเราได้ทุ่มเทอย่างมากในการทำความเข้าใจว่ากฎการเก็บรักษาของผู้บริโภคในคลังส่งผลต่อการเพิ่มประสิทธิภาพสำหรับระบบนิเวศอย่างไร
ไลบรารีจำนวนมากจะมาพร้อมกับกฎที่กว้างเกินไปเพื่อความเสถียร
หรืออาจระบุกฎกว้างๆ ไว้ในเอกสารประกอบสำหรับนักพัฒนาแอปเพื่อเพิ่มลงในแอปของตนเอง
แต่การจัดส่งด้วยกฎแบบกว้างๆ ไม่จำเป็นต่อความเสถียร และส่งผลเสียต่อประสิทธิภาพ ซึ่งต้นทุนส่วนใหญ่จะเป็นภาระของคุณในฐานะนักพัฒนาแอป
เราพบกรณีที่ไลบรารีทั้งหมดได้รับการเก็บรักษาไว้โดยกฎการเก็บรักษาทั้งแพ็กเกจ เช่น ไลบรารี Bloat ของ Analytics และในกรณีของไลบรารีนั้น สิ่งนี้ส่งผลกระทบในวงกว้างต่อศักยภาพในการเพิ่มประสิทธิภาพของแอป
เรายังอยากพูดถึงด้วยว่าแม้แต่ SDK ของ Google เองก็ไม่รอดจากปัญหานี้ และไลบรารีของ Google มากกว่า 12 รายการได้แก้ไขกฎการเก็บรักษาที่กว้างเกินไปในปีที่ผ่านมา
ดังนั้นหากต้องการใช้เครื่องมือวิเคราะห์การกำหนดค่า เครื่องมือนี้จะผสานรวมเข้ากับ AGP 9.3 โดยตรง แต่ก็พร้อมใช้งานสำหรับเวอร์ชันเก่าด้วยหากคุณอัปเกรดเวอร์ชัน R8
ดังนั้นการเรียกใช้คำสั่งนี้จะสร้างรายงาน HTML ที่เราแสดง ซึ่งจะแสดงกฎการเก็บรักษา ผลกระทบต่อการลดขนาด การเพิ่มประสิทธิภาพ การสร้างความสับสนสำหรับกฎแต่ละข้อ และแอปโดยรวม
และหากคุณใช้บิลด์ที่เผยแพร่ R8 แบบเต็ม ระบบจะสร้างรายงานโดยอัตโนมัติและดูได้ที่เส้นทางไฟล์นี้
ประโยชน์ของงานแบบสแตนด์อโลนนี้คือใช้เวลาในการสร้างน้อยกว่าบิลด์แบบเต็ม
ทีม WHOOP ซึ่งอาจอยู่ที่นี่สักแห่งได้เรียกใช้เครื่องมือวิเคราะห์การกำหนดค่าและพบว่าการลดขนาดอยู่ที่ 74% การสร้างความสับสนอยู่ที่ 74% และการเพิ่มประสิทธิภาพอยู่ที่ 0
และอย่างที่คุณคาดเดาได้ ทีมรู้สึกประหลาดใจมากที่เห็นมูลค่าการเพิ่มประสิทธิภาพนี้
การมองเห็นจากเครื่องมือวิเคราะห์การกำหนดค่าช่วยให้ทีมค้นพบว่าบิลด์ของตนใช้ไฟล์ ProGuard เดิมที่ปิดใช้การเพิ่มประสิทธิภาพ ดังนั้นเมื่อเปลี่ยนไปใช้เวอร์ชันที่อัปเดต ทีมจึงเปิดใช้การเพิ่มประสิทธิภาพสำหรับแอปได้
ซึ่งส่งผลให้เพิ่มประสิทธิภาพได้ 55% และลดขนาด APK ได้ 4%
เมื่อใช้ร่วมกับโปรไฟล์พื้นฐานแล้ว พวกเขาสามารถปรับปรุงเวลาเริ่มต้นแบบ Cold Start ได้ 38% และลดเฟรมที่ค้างมากเกินไปได้ 28%
หน้าจอทางด้านซ้ายไม่มีโปรไฟล์พื้นฐาน ส่วนหน้าจอทางด้านขวามีโปรไฟล์พื้นฐาน และอย่างที่คุณเห็น การเริ่มต้นแอปจะเร็วขึ้นเมื่อเพิ่มโปรไฟล์พื้นฐาน
Let that play one more time.
เยี่ยมเลย
ลิงก์นี้จะนำคุณไปยังคู่มือสำหรับเครื่องมือวิเคราะห์การกำหนดค่า และการตรวจสอบรายงานด้วยตนเองก็เป็นวิธีที่ดี แต่คุณคงนึกภาพออกว่าการตรวจสอบอาจใช้เวลานาน ทั้งนี้ขึ้นอยู่กับสถานะการกำหนดค่า R8 ของแอป
ดังนั้นเพื่อเร่งกระบวนการให้เร็วขึ้น เราจึงมีทักษะ R8 Analyzer
$ android skills list
$ android skills add r8-analyzerคุณสามารถขอให้ Agent วิเคราะห์การกำหนดค่า R8 ของคุณได้ และทักษะนี้ไม่เพียงแต่จะสร้างรายงานเครื่องมือวิเคราะห์การกำหนดค่า R8 เท่านั้น แต่ยังชี้ให้เห็นกฎที่มีผลมากที่สุดด้วย
นี่คือตัวอย่างเอาต์พุต และเพื่อระบุรายละเอียดบางอย่าง โปรดทราบว่าคะแนนการเพิ่มประสิทธิภาพ 42% หมายความว่า 52%-- 58% ของฐานของโค้ดไม่สามารถเพิ่มประสิทธิภาพโดย R8 ได้
และหากเราดูที่ส่วนกฎการเก็บรักษาโดยเฉพาะ กฎการเก็บรักษาทั้งแพ็กเกจสำหรับไลบรารี Analytics Bloat จะป้องกันไม่ให้เพิ่มประสิทธิภาพฐานของโค้ด 22% และ Skill ยังแนะนำให้เราปรับแต่งกฎการเก็บรักษานี้ด้วย เนื่องจากเรารู้อยู่แล้วว่ากฎนี้กว้างเกินไป
ตอนนี้คุณจึงสามารถวิเคราะห์สาเหตุที่ทำให้แอปมีขนาดใหญ่เกินไปและบล็อกการเพิ่มประสิทธิภาพ DEX โดยใช้เครื่องมือวิเคราะห์การกำหนดค่า R8 และทักษะได้แล้ว
แต่เมื่อพบกฎการเก็บรักษาที่กว้างเกินไปแล้ว คุณจะแทนที่กฎดังกล่าวอย่างปลอดภัยโดยไม่ทำให้เกิดข้อขัดข้องที่ไม่คาดคิดได้อย่างไร
เราจะส่งต่อให้ Chris ซึ่งจะแสดงวิธีเปลี่ยนจากกฎการเก็บรักษาเดิมไปเป็นคำอธิบายประกอบการเก็บรักษาที่สร้างโดยอัตโนมัติและมีความปลอดภัยในการกำหนดประเภทแบบใหม่
ขอบคุณ Breana
เอาล่ะ มาพูดถึงที่มาที่ไปกันก่อนดีกว่า
ด้วยไลบรารี Analytics ที่มีขนาดใหญ่เกินไปนี้ พร้อมด้วยกฎการเก็บรักษาทั้งหมดเหล่านี้ ซึ่งก็คือด้วยกฎการเก็บรักษานี้ที่เก็บทุกอย่างไว้ ปัญหาหลักที่เราพบคือการสะท้อน
เรามาดูโค้ดแบบสะท้อนเพื่อเป็นการทบทวนกันสักเล็กน้อย
fun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}นี่คือฟังก์ชันการสร้างอินสแตนซ์ออบเจ็กต์ที่เพียงแค่ขยายออบเจ็กต์หนึ่งๆ และทำเช่นนั้นแบบสะท้อนกลับ
ข้อเสียของวิธีนี้ในบริบทของการเพิ่มประสิทธิภาพ DEX คือจะทำให้การเรียกแบบคงที่ทั้งหมดเหล่านี้ล่าช้าไปจนถึงรันไทม์
เพียงแค่ดูโค้ด เครื่องมือเพิ่มประสิทธิภาพ R8 ของเราจะไม่เห็นสิ่งที่เกิดขึ้นที่นี่ ทั้งนี้ขึ้นอยู่กับว่าชื่อคลาสนี้มาจากไหน
สถานการณ์เหล่านี้จะทำให้การเพิ่มประสิทธิภาพ Dex เสียไปโดยค่าเริ่มต้น ดังนั้นโดยปกติแล้วคุณจะต้องเพิ่มกฎ Keep
ซึ่งคุณควรหลีกเลี่ยงปัญหานี้
ตามหลักการแล้ว เราควรหลีกเลี่ยงการใช้ Reflection โดยสิ้นเชิง โดยเฉพาะอย่างยิ่งในปี 2026 นี้ที่เรามีเครื่องมือสร้างโค้ดที่ยอดเยี่ยมสำหรับการแปลงเป็นอนุกรมและการแทรกทรัพยากร Dependency ซึ่งสามารถขจัดปัญหาประเภทนี้ออกไปได้อย่างสิ้นเชิง
แต่ก็ไม่ใช่ทั้งหมดใช่ไหม
เรายังคงต้องใช้การสะท้อนในบางกรณี
มาเจาะลึกกันว่าเหตุการณ์นี้เกิดขึ้นได้อย่างไรในไลบรารีที่บวมนี้
ตัวอย่างหนึ่งที่พบบ่อยคือทรัพยากร Dependency ที่ไม่บังคับ
// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}ในที่นี้ เรากำลังขยายประเภทที่เฉพาะเจาะจงซึ่งเข้าถึงไม่ได้ใน classpath ของไลบรารี ดังนั้นเราจึงต้องดำเนินการด้วยสตริง
เรากำลังอ้างอิงถึงคลาสนี้โดยเฉพาะซึ่งอาจมีหรือไม่มีอยู่ในขณะที่สร้างไลบรารี ดังนั้นไลบรารีนี้จึงใช้การสะท้อนเพื่อค้นหาสมาชิกภายในไลบรารีที่เกี่ยวข้องอย่างสมเหตุสมผล
อีกกรณีหนึ่งที่เรามีคือการสร้างอินสแตนซ์คลาสย่อยแบบไดนามิก
ดังนั้น โดยเฉพาะอย่างยิ่งหากเป็นสิ่งที่มาจากไฟล์ Manifest หรือสิ่งที่ส่งมาจากฝั่งเซิร์ฟเวอร์ เราจะแจ้งให้แอปในเครื่องทราบว่า "เฮ้ โปรดขยายสิ่งนี้ สร้างสิ่งนี้ในภายหลัง"
และหมวดหมู่ที่ 3 ที่เราเห็นที่นี่คือการทำให้เป็นอนุกรมที่น่าอาย
คือฉันมีการจัดรูปแบบ JSON ที่เกิดขึ้นโดยการทำซ้ำผ่านสมาชิกทั้งหมด
ซึ่งเป็นเรื่องที่น่ารังเกียจอย่างยิ่ง แต่คุณก็รู้ว่าบางครั้งเรื่องแบบนี้ก็ยังคงมีอยู่
แต่ก็ไม่ได้แก้ไขทันทีที่จำเป็นเสมอไป
แต่เรามาพูดถึงผลลัพธ์ของเรื่องนี้กันดีกว่า
เมื่อการเรียกใช้แบบสะท้อนเหล่านี้เพิ่มขึ้นเรื่อยๆ คุณก็จะมีแรงจูงใจมากขึ้นที่จะทำสิ่งที่ง่าย และเพิ่มประสิทธิภาพสำหรับประสบการณ์ของนักพัฒนาแอปมากกว่าประสบการณ์ของผู้ใช้
ซึ่งก็รู้สึกแย่มาก ฉันเลยจะเพิ่มสิ่งที่ต้องทำ แล้วทุกอย่างก็จะเรียบร้อย
# TODO: clean this up
-keep com.example.analytics.bloat.** { * };สักวันหนึ่งฉันจะกลับมาแก้ไขให้ถูกต้อง
เรามาพูดถึงสิ่งที่เราควรทำ สิ่งที่เราควรทำอย่างน้อยก่อนวันนี้ วิธีเขียนกฎการเก็บรักษาที่เหมาะสมที่นี่
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}เอาล่ะ เตรียมตัวให้พร้อม
ดังนั้น ส่วนแรกที่นี่คือเราต้องการเก็บคลาสเฉพาะที่เราแสดงผลจากฟังก์ชันการสะท้อนนี้
### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}แต่เดี๋ยวก่อน
รู้ไหมว่า
ซึ่งไม่ถูกต้อง
นี่คือคลาส Kotlin ฉันต้องตรวจสอบว่านี่เป็นประเภทสตริง ขอใส่ KT ลงไปก่อน
### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}และที่จริงแล้ว สิ่งสำคัญที่นี่คือฉันไม่ได้แสดงผล EventFilter แต่ฉันแสดงผลคลาสย่อยของ EventFilter ดังนั้นฉันจึงต้องเก็บคลาสที่ขยาย EventFilter ไว้เพื่อให้สามารถขยายคลาสนี้ได้อย่างถูกต้อง
### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}และจริงๆ แล้ว หากฉันต้องการทำสิ่งนี้ให้มีประสิทธิภาพมากขึ้น ตอนนี้ก็ทำได้แล้ว แต่เพื่อให้มีประสิทธิภาพมากขึ้น ในฐานะนักพัฒนาไลบรารีที่ดี ฉันควรทำอะไรเพิ่มเติมอีก
ฉันควรพูดว่า -keepclasseswithmembers และเก็บเฉพาะคลาสที่มีเครื่องมือสร้าง <init> นี้ที่ฉันใช้เพื่อขยายออบเจ็กต์
### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}และถ้าอยากให้ดีจริงๆ ผมจะเพิ่ม allowaccessmodification เพื่อให้เพิ่มประสิทธิภาพได้มากขึ้นใน API ทั้งหมดที่กฎ Keep นี้กำหนดเป้าหมายไว้ เพราะผมไม่อยากป้องกันการเพิ่มประสิทธิภาพเพิ่มเติมจาก R8 ผมอยากเป็นพลเมืองที่ดี
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}และขั้นตอนสุดท้ายที่แทบไม่มีใครทำเพราะเป็นงานที่เพิ่มเข้ามา แต่เหมาะอย่างยิ่งสำหรับไลบรารี คือการทำให้เป็นแบบมีเงื่อนไข
### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}ในทางแนวคิด คุณไม่จำเป็นต้องเก็บคลาสย่อยทั้งหมดไว้ เว้นแต่คุณจะเรียกใช้ฟังก์ชันนี้จริงๆ และนั่นคือสิ่งที่คำสั่ง if นี้ทำที่ด้านบนของกฎ Keep
แต่คุณก็รู้ว่าผมสามารถขอให้ยกมือได้ แต่ผมรู้ว่าแทบไม่มีใครทำแบบนี้ใช่ไหม
เช่น การเขียนโค้ดจริงๆ และทำให้เป็นแบบมีเงื่อนไข
แล้วถ้าเราทำให้สิ่งนี้ดีขึ้นอีกสักหน่อยล่ะ
ก่อนอื่นมาดูที่ See, Like, Okay กัน
บางทีอาจมีวิธีอื่นในการดำเนินการนี้
เราได้พูดคุยกันไปแล้วว่าจะเป็นอย่างไรหากฉันเขียนกฎการเก็บรักษาที่แคบและเขียนด้วยตนเอง
ในทางเทคนิคแล้วทำได้ แต่เขียนและอ่านยาก รวมถึงดูแลรักษายากมาก เนื่องจากมีบิตที่พิมพ์เป็นสตริงจำนวนมากซึ่งอาจเสียหายได้ง่ายเมื่อเกิดเหตุการณ์บางอย่างขึ้น
ใต้สิ่งต่างๆ มีการเปลี่ยนแปลงเกิดขึ้น
และเรามีทางเลือกอื่น
เรามีคำอธิบายประกอบ เช่น @Keep
ตอนนี้ @Keep อยู่ในแพ็กเกจคำอธิบายประกอบ AndroidX มาเป็นเวลานานแล้ว แต่จริงๆ แล้วปัจจุบันไม่แนะนำให้ใช้
@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }ซึ่งไม่ใช่โซลูชันที่ยอดเยี่ยมสำหรับนักพัฒนาไลบรารี และจริงๆ แล้วเราก็แบนวิธีนี้เป็นการภายในด้วย เพราะแม้ว่าจะดูแลรักษาได้ง่ายกว่า แต่ก็ไม่ยืดหยุ่นเลย และไม่มีการเพิ่มประสิทธิภาพบางอย่างที่ผมพูดถึงก่อนหน้านี้
ก็ทำไม่ได้อยู่แล้ว
ซึ่งไม่อนุญาตให้ทำการวิเคราะห์เว็บไซต์การโทร
ทีนี้ ถ้าเราดูที่กรณีเฉพาะที่เรามีก่อนหน้านี้ "อ๋อ ฉันก็แค่ใส่ @Keep ไว้ใน EventFilter ของฉัน ฉันก็สบายแล้วใช่ไหม"
@Keep
class EventFilter { … }อืมมมม ไม่ได้ล่ะ
@Keep ไม่ได้กำหนดเป้าหมายคลาสย่อยจริงๆ แต่จะกำหนดเป้าหมายเฉพาะคลาสนั้นเท่านั้น
@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }ดังนั้นจริงๆ แล้วฉันต้องกำหนดเป้าหมายคลาสย่อยทั้งหมด และตอนนี้ฉันกำลังเขียนคำอธิบายประกอบ @Keep มากกว่าที่ฉันจะเขียนด้วยกฎ Keep ข้อเดียวที่เขียนยาก
ดังนั้นทั้ง 2 วิธีนี้จึงมีปัญหาใช่ไหม
เนื่องจากคุณอาจลืมได้ง่าย ทราบข้อมูลช้า และโดยค่าเริ่มต้นแล้วจะไม่ขึ้นอยู่กับผู้โทร
โดยเฉพาะอย่างยิ่งสำหรับไลบรารี เราต้องการวิธีที่ดีกว่า
เราต้องการให้มีวิธีที่ง่ายขึ้นในการรับสิ่งต่างๆ ที่พร้อมใช้งานทันที
และปัญหาใหญ่ประการหนึ่งที่เราเห็นคือสิ่งเหล่านี้อยู่ห่างไกลจากการสะท้อนที่เรากำลังป้องกันอยู่จริงๆ
ท้ายที่สุดแล้ว โซลูชันที่เราต้องการคือสิ่งที่อยู่ตรงข้ามกับโค้ดการสะท้อนที่ไม่เหมาะสมซึ่งทำให้สิ่งต่างๆ ที่กำหนดเป้าหมายเพิ่มขึ้น
เรามาเริ่มพูดถึง Jetpack Keep ซึ่งเป็นไลบรารีและปลั๊กอินใหม่จาก AndroidX ที่ออกแบบมาเพื่อช่วยแก้ปัญหานี้โดยเฉพาะกันดีกว่า
ดังนั้น เพื่อใช้-- เป้าหมายทั้งหมดของ Jetpack Keep คือการเริ่มต้นด้วยการสร้างกฎ Keep จากคำอธิบายประกอบ แต่ยิ่งไปกว่านั้นคือการสร้างคำอธิบายประกอบโดยตรงจากการสะท้อนที่มีอยู่ในฐานของโค้ดอยู่แล้ว
การตั้งค่าจึงง่ายมาก
plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}ซึ่งเป็นปลั๊กอิน Gradle และไลบรารีคำอธิบายประกอบ
แต่เรามาดูวิธีการทำงานจริงของโค้ดนี้ในเวลาบิลด์กัน
ดังนั้น หากฉันมีตัวอย่างก่อนหน้านี้ที่ฉันกำลังขยายคลาสย่อยของประเภทที่รู้จัก ฉันต้องทำอย่างไร
ฉันแค่เรียกใช้ lintFix
./gradlew analyticsbloat:lintFixLint จะวิเคราะห์การสะท้อนทั้งหมดที่เกิดขึ้นภายในเมธอดนี้ และแจ้งว่า "คุณต้องใช้คำอธิบายประกอบนี้"
@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}คำอธิบายประกอบนี้แสดงให้เห็นอย่างชัดเจนว่าการสะท้อนภายในสายนี้ทำอะไรอยู่
ซึ่งระบุว่า "ฉันกำลังขยายคลาสประเภทนี้"
ในกรณีนี้ EventFilter เป็นคลาสที่รู้จัก
ฉันไม่ได้ใช้สตริงเพื่ออ้างอิงเนื่องจากไม่จำเป็นต้องใช้
ซึ่งจะช่วยเพิ่มความปลอดภัย และฉันยังเข้ารหัส parameterTypes เพื่อให้ระบุได้อย่างชัดเจนว่ากำลังเก็บคอนสตรัคเตอร์ใดไว้
แต่การดำเนินการนี้ทำอะไรได้บ้าง
เช่น ข้อมูลนี้จะช่วยให้เราสร้าง R8 ได้อย่างไร
ระบบจะสร้างกฎการเก็บรักษา และหากคุณเห็นก่อนหน้านี้ นี่คือกฎการเก็บรักษาที่เหมาะสมแบบเดียวกับที่เรามีก่อนหน้านี้
# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}โดยจะกำหนดเป้าหมายไปยังคลาสและคลาสย่อยเดียวกันทุกประการ และขึ้นอยู่กับจุดสะท้อนนี้
และฉันไม่จำเป็นต้องดูแลรักษา-- ฉันไม่จำเป็นต้องกังวลเกี่ยวกับการใส่คำอธิบายประกอบคลาสย่อยทั้งหมด
ฉันไม่จำเป็นต้องเก็บไฟล์กฎการเก็บรักษาแยกต่างหากไว้ที่อื่น
วิธีนี้จะช่วยให้คุณจัดการกฎการเก็บรักษาได้ง่ายขึ้นมาก
ลองย้อนกลับไปดูตัวอย่างทรัพยากร Dependency ที่ไม่บังคับ
@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}ตอนนี้ คลาสไม่ได้อยู่ในคลาสพาธ แต่ฉันยังคงอ้างอิงคลาสนั้นได้
ฉันใช้สตริง แต่สำหรับสิ่งอื่นๆ อย่างประเภทพารามิเตอร์จะอ้างอิงตามการอ้างอิงคลาส ฉันจึงทำเช่นนั้น
และเราจะสร้างกฎการเก็บรักษาที่เหมาะสมสำหรับกรณีนี้อีกครั้ง
# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}ซึ่งเป็นแบบมีเงื่อนไข เฉพาะเจาะจง และตรงเป้าหมาย รวมถึงช่วยให้เพิ่มประสิทธิภาพได้ทุกเมื่อที่เป็นไปได้ในคลาสที่กำหนดเป้าหมายเหล่านั้น
นี่คือการสะท้อนฟิลด์ และในกรณีนี้ ฉันกำลังทำซ้ำฟิลด์ทั้งหมดภายในออบเจ็กต์ แต่ในที่นี้ เราก็จัดการได้ดีด้วยการกำหนดเป้าหมายฟิลด์ทั้งหมดด้วยโอเปอเรเตอร์ดาวนี้
// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}ซึ่งช่วยให้ฉันกำหนดเป้าหมายฟิลด์ทั้งหมดได้ และดูสิ มันสร้างกฎการเก็บรักษาที่ตรงกับที่คุณคาดหวังไว้สำหรับเรื่องนี้ ซึ่งเป็นภาพสะท้อนที่โชคร้ายอย่างไม่น่าเชื่อซึ่งไม่ควรอยู่ในแอปของฉัน แต่ฉันจะกลับมาล้างข้อมูลนั้น ฉันสัญญา
# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}แล้ว Jetpack Keep ทำอะไรได้บ้าง
ซึ่งช่วยลดการดูแลรักษากฎการเก็บรักษาได้อย่างมาก
สำหรับทุกอย่างที่คุณสร้างขึ้นโดยอัตโนมัติได้ ระบบจะนำสิ่งนั้นออกจากพื้นที่ที่คุณกังวลและพื้นที่ที่มีความเสี่ยงในการรักษากฎการเก็บรักษา
นอกจากนี้ยังให้ความแน่นอนของ Lint ด้วย
ซึ่งไม่เพียงแต่แก้ไขสิ่งต่างๆ ให้คุณในขณะที่คุณกำลังทำซ้ำและพยายามย้ายข้อมูลเท่านั้น แต่ยังตรวจจับการสะท้อนใหม่ๆ และตรวจจับการเปลี่ยนแปลงที่สะท้อนกลับทุกครั้งที่โค้ดมีการเปลี่ยนแปลง และความปลอดภัยนั้นสำคัญอย่างยิ่ง
ฉันจะใช้ฟีเจอร์นี้ได้ไหม
แล้วการขึ้นต่อกันในที่นี้คืออะไร
ปลั๊กอินและ Lint ต้องใช้ AGP 8.7
ซึ่งน่าจะครอบคลุมเกือบทุกคน
แต่ก็มีความยืดหยุ่นสูงมากในเรื่องตำแหน่งที่คุณจะใช้ได้
คุณใช้ฟีเจอร์นี้ภายในแอปได้
คุณสามารถใช้ฟีเจอร์นี้ภายในไลบรารี Android, AAR, ไลบรารี KMP และ JAR ซึ่งในกรณีนี้จะต้องมีการเชื่อมต่อด้วยตนเองเล็กน้อย
แต่ฟีเจอร์หลักในที่นี้คือฟีเจอร์นี้ปลอดภัยที่จะใช้ในฐานะนักพัฒนาไลบรารี
แม้ว่าจะมีคนใช้ R8 เวอร์ชันเก่าหรือแม้แต่เครื่องมือของบุคคลที่สามที่ใช้ไลบรารีของคุณ เราก็ยังคงส่งออกกฎผู้ใช้มาตรฐานจริงเพื่อให้ทุกคนยังคงใช้คุณได้และไม่ต้องกังวลเกี่ยวกับการใช้รูปแบบใหม่ที่เข้ากันไม่ได้
หลังจากที่ได้พูดถึงสิ่งที่ Jetpack Keep สามารถทำได้เพื่อช่วยคุณเพิ่มประสิทธิภาพและช่วยให้คุณกังวลน้อยลงเกี่ยวกับการปกป้องการเรียกใช้โค้ดแบบสะท้อนแล้ว เรามาพูดถึงฟีเจอร์ใหม่ที่เราเพิ่งเปิดตัวใน Android CLI เพื่อตรวจสอบอีกครั้งในขั้นตอนการเผยแพร่ครั้งล่าสุดกัน
Breana ได้พูดถึงก่อนหน้านี้ว่ามีเครื่องมือที่ Play Findings จะบอกข้อมูลเกี่ยวกับข้อมูลการปรับขนาด ขนาด DEX และเปอร์เซ็นต์การเพิ่มประสิทธิภาพ
ตอนนี้เราได้เริ่มเปิดให้ใช้งานผ่าน Android CLI ด้วยเช่นกัน
โดยใช้ตรรกะเดียวกันกับ Play Store ดังนั้นคุณจึงสามารถวิเคราะห์ Bundle ใน CLI ได้โดยไม่ต้องอัปโหลด Bundle ไปยัง Play Store
$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>และคุณจะได้รับข้อมูลเดียวกันนี้ที่รายงานถึงคุณโดยตรงและทันทีพร้อมการวิเคราะห์ในพื้นที่
คุณจะเห็นได้จากกรณีนี้ว่าแอปนี้ได้รับคำเตือนว่ามีอัตราการเพิ่มประสิทธิภาพต่ำมาก และยังได้รับแจ้งว่า "นี่คือขนาด DEX ของคุณ และนี่คือเปอร์เซ็นต์การเพิ่มประสิทธิภาพ" เหมือนกับที่ Play Store จะบอกคุณ
แต่เรายังมีเอาต์พุต JSON ที่เป็นประโยชน์ด้วย เพื่อให้คุณเขียนสคริปต์เพื่อทำการทดสอบการถดถอยโดยอัตโนมัติได้หากค่าเหล่านี้ลดลงอย่างมาก เพราะค่าเหล่านี้เป็นตัวเลขที่ Play บังคับใช้ คุณจึงต้องติดตามค่าเหล่านี้ ซึ่งทำได้ง่ายๆ ด้วยค่า JSON เหล่านี้หรือรายงาน JSON เหล่านี้
{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}เราจะเห็นตัวเลขเดียวกันนี้ปรากฏอยู่ด้วย
นี่ไม่ใช่ฟีเจอร์เดียวของ android analyze
android analyze ยังทำงานในไลบรารีได้ด้วย
$ android analyze <library.aar>
$ android analyze <library.jar>และอย่างที่ Breana กล่าวไว้ มีปัญหามากมายเกี่ยวกับไลบรารีที่ทำงานร่วมกับการจัดส่งซึ่งมีกฎการเก็บรักษาที่ไม่เหมาะสมอยู่ภายใน
เรามาใช้เครื่องมือนี้เพื่อดูปัญหาเหล่านี้กัน
ดังนั้นเมื่อฉันเรียกใช้เครื่องมือนี้กับ AAR ที่มีอยู่ ฉันจะเห็นว่าไลบรารีนี้มีปัญหาหลายอย่างที่ไฮไลต์ไว้ที่ด้านบนสุด
คลังนี้มีอัตราส่วนการเก็บรักษาสูง ซึ่งหมายความว่ามีการเก็บรักษาข้อมูลมากเกินไปเนื่องจากมีกฎการเก็บรักษาแบบไม่จำกัด
ซึ่งมีกฎการเก็บรักษาแบบกว้างๆ ที่ป้องกันไม่ให้มีการเพิ่มประสิทธิภาพ และมีตัวเลขด้านล่างที่บอกให้คุณทราบว่าแย่แค่ไหน และยังมีกฎการเก็บรักษาที่อาจน่ากลัวที่สุด ซึ่งไม่ได้กำหนดเป้าหมายใดๆ ซึ่งอาจหมายความว่ากฎเหล่านี้กำหนดเป้าหมายไปยังผู้อื่นในลักษณะที่ไม่มีขอบเขต
โค้ดที่อันตรายมาก
คุณจึงตรวจสอบทั้งหมดนั้นได้ใน AAR โดยเฉพาะอย่างยิ่งในฐานะผู้เผยแพร่ไลบรารี วิธีนี้เป็นวิธีที่ยอดเยี่ยมในการตรวจสอบว่าคุณไม่ได้จัดส่งกฎการเก็บรักษาที่ไม่ถูกต้อง และคุณไม่ได้ปล่อยให้กฎเหล่านั้นรั่วไหลเข้ามาเมื่อมีคนทำงานในไลบรารีซึ่งอาจไม่เชี่ยวชาญในการเขียนกฎการเก็บรักษา
คุณจะเห็นว่าเราแสดงอัตราส่วนการเก็บรักษาด้วย
วิธีนี้เป็นวิธีที่ดีในการพิจารณาคุณภาพโดยรวมของคลัง
หากฉันเพิ่มประสิทธิภาพไลบรารีแต่ละรายการแบบสแตนด์อโลนโดยสมบูรณ์ ไม่มีข้อมูลอ้างอิง ด้วย R8 ฉันควรเห็นว่ามีขนาด 0 ไบต์ใช่ไหม
ซึ่งควรจะว่างเปล่าเนื่องจากไม่มีการเก็บรักษาข้อมูลใดๆ ไว้ภายใน
ซึ่งนั่นเป็นสิ่งที่เหมาะสมที่สุด
ในกรณีนี้ ไม่ใช่
คุณจะเห็นขนาด R8 และ D8 โดยตรง
และเรายังไฮไลต์กฎการเก็บรักษาที่แย่ที่สุดบางข้อเพื่อให้คุณเห็นได้ชัดเจนยิ่งขึ้นด้วย
ซึ่งมีรูปแบบ JSON ด้วยเช่นกัน ดังนั้นไม่ว่าคุณจะเป็นนักพัฒนาแอปที่พยายามควบคุมการอัปเดตไลบรารี หรือเป็นนักพัฒนาไลบรารีที่พยายามป้องกันการถดถอยครั้งใหญ่ในคุณภาพของไลบรารี คุณก็สามารถทำการวิเคราะห์แบบเดียวกันและป้องกันไม่ให้ปัญหาเหล่านี้เกิดขึ้นได้ด้วยสคริปต์ง่ายๆ ที่รวม Android CLI ไว้
{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}ดังนั้น หากพบปัญหาเกี่ยวกับไลบรารี โปรดลองอัปเดตไลบรารีก่อน
อย่างที่ Breana ได้กล่าวไปก่อนหน้านี้ เราได้แก้ไขไลบรารีของ Google มากกว่า 12 รายการเพื่อแก้ไขกฎการเก็บรักษาที่น่าเสียดาย และยังมีไลบรารีอื่นๆ ที่ได้รับการแก้ไขแล้ว แต่แอปที่ใช้ไลบรารีเหล่านั้นยังไม่ได้รับการอัปเดต
นี่เป็นขั้นตอนแรกที่ยอดเยี่ยม
ประการที่ 2 คุณจะมองหาปัญหาและติดต่อนักพัฒนาไลบรารีได้ และอาจแนบรายงานเครื่องมือวิเคราะห์ที่บอกว่า "คุณเก็บข้อมูลไว้มากเกินไป
โปรดช่วยฉันเพิ่มประสิทธิภาพแอป"
สรุปก็คือ เราได้พูดถึงเครื่องมือวิเคราะห์การกำหนดค่า R8 ซึ่งช่วยให้คุณดูทุกสิ่งที่เกิดขึ้นในบิลด์ R8 ของคุณ ทุกสิ่งที่บอกว่าอะไรเพิ่มประสิทธิภาพได้และอะไรเพิ่มประสิทธิภาพไม่ได้
เราได้พูดถึง Jetpack Keep ซึ่งเป็นไลบรารีใหม่ที่ช่วยให้คุณเขียนกฎ Keep ได้โดยอัตโนมัติโดยไม่ต้องทำอะไรเลย
และ Android CLI ซึ่งเป็นเครื่องมือใหม่ที่จะช่วยคุณตรวจสอบและตรวจสอบซ้ำอีกครั้งในขั้นตอนสุดท้าย เพื่อให้แน่ใจว่าคุณ ตัวแทน หรือวิศวกรระดับเริ่มต้นในทีมไม่ได้ทำลายสิ่งใดๆ สำหรับผู้บริโภค
หากต้องการดูข้อมูลเพิ่มเติม โปรดดูเอกสารประกอบสำหรับ R8, เครื่องมือวิเคราะห์การกำหนดค่า และ Jetpack Keep
และมีเอกสารประกอบเกี่ยวกับ Android CLI ที่พูดถึงคำสั่ง analyze และวิธีใช้
ขอบคุณมากครับ
android {
buildTypes {
release {
optimization {
// no need to specify default
// rules and resource shrinking
enabled = true
}
}
}
...
}android {
buildTypes {
release {
// Enables code optimizations.
isMinifyEnabled = true
// Enables resource shrinking.
isShrinkResources = true
proguardFiles(
// Default rules.
getDefaultProguardFile(
"proguard-android-optimize.txt"
),
... // list out each file with keep rules
)
...
}
}
...
}$ android skills list
$ android skills add r8-analyzerfun createWithReflection(className: String): MyObject {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as MyObject
}// Problem 1 - optional dependency
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}
// Problem 2 - dynamically instantiate subclass
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}
// Problem 3 - serialization
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# TODO: clean this up
-keep com.example.analytics.bloat.** { * };fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}### Keep the EventFilter
-keep class com.example.analytics.bloat.EventFilter {
*
}### Keep the actual EventFilter
-keep class com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses
-keep class ** extends com.example.analytics.bloat.EventFilterKt {
*
}### Keep the EventFilter subclasses, if they have this constructor
-keepclasseswithmembers class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}### -if makes this conditional, critical for libraries
-if class com.example.analytics.bloat.EventToolsKt {
class com.example.analytics.bloat.EventFilterKt createEventFilter(java.lang.String);
}
### Keep the EventFilter subclasses, if they have this constructor, allow more optimization
-keepclasseswithmembers,allowaccessmodification class ** extends com.example.analytics.bloat.EventFilterKt {
void <init>();
}@androidx.annotation.Keep
class EventFilter { … }
@androidx.annotation.Keep
class SizeBasedEventFilter { … }
@androidx.annotation.Keep
class DropLogsEventFilter { … }@Keep
class EventFilter { … }@Keep
class EventFilter { … }
@Keep
class SizeBasedEventFilter { … }
@Keep
class DropLogsEventFilter { … }plugins {
id("com.android.library")
id("androidx.annotation.keep")
}
dependencies {
implementation("androidx.annotation.keep:keep")
...
}./gradlew analyticsbloat:lintFix@UsesReflectionToConstruct(
classConstant = EventFilter::class,
parameterTypes = []
)
fun createEventFilter(className: String): EventFilter {
return Class.forName(className)
.getDeclaredConstructor()
.newInstance() as? EventFilter
}# Generated Keep Rule
-if class com.example.analytics.bloat.EventTools {
class com.example.analytics.bloat.EventFilter createEventFilter(java.lang.String);
}
-keepclasseswithmembers,allowaccessmodification class ** extends class com.example.analytics.bloat.EventFilter {
void <init>();
}@UsesReflectionToConstruct(
className = "com.example.analytics.bloat.crash.CrashReporterTransport",
parameterTypes = [Context::class]
)
fun tryInitCrashReporter(context: Context): EventTransport? {
return runCatching {
Class.forName("com.example.analytics.bloat.crash.CrashReporterTransport")
.getDeclaredConstructor(Context::class.java)
.newInstance(context) as? EventTransport
}.getOrNull()
}# Generated Keep Rule
-if class com.example.analytics.bloat.CrashConnection {
class com.example.analytics.bloat.EventTransport tryInitCrashReporter(android.content.Context);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.crash.CrashReporterTransport {
void <init>(android.content.Context);
}// TODO: migrate to Kotlinx Serialization or Moshi codegen.
@UsesReflectionToAccessField(
classConstant = UserActionEvent::class,
fieldName = "*"
)
fun serializeEvent(event: UserActionEvent): JSONObject {
val json = JSONObject()
for (field in event.javaClass.declaredFields) {
field.isAccessible = true
json.put(field.name, field.get(event))
}
return json
}# Generated Keep Rule
-if class com.example.analytics.bloat.serialization.EventSerializer {
class org.json.JSONObject serializeEvent(com.example.analytics.bloat.model.UserActionEvent);
}
-keepclasseswithmembers,allowaccessmodification class com.example.analytics.bloat.model.UserActionEvent {
<fields>;
}$ android analyze <mybundle.aab>
$ android analyze <mybundle.apk>{
"optimization": {
"bundleAnalysisResult": {
"agpVersion": "9.3.2",
"r8Json": {
"version": "9.3.16",
"options": {
"isRepackageClassesEnabled": true,
"isProguardCompatibilityModeEnabled": false
},
"stats": {
"shrinkingRatio": 0.2306,
"optimizationRatio": 0.2351,
"obfuscationRatio": 0.2322
},
...
}
"mappingObfuscationRatio": 0.21631891,
"dexHeuristicObfuscationRatio": 0.25118626,
"uncompressedDexSizeBytes": "16198152"
}$ android analyze <library.aar>
$ android analyze <library.jar>{
"optimization": {
"libraryAnalysisResult": {
"dex": {
"d8CompileResult": {
"dexSizeBytes": 4523352
},
"r8CompileResult": {
"dexSizeBytes": 3266596,
"configuration": {
"usedPackageWideKeepRules": [
{
"source": "-keep class com.example.analytics.bloat.** { *; }",
"keptItemCount": 125579,
},
...
],
"unusedPackageWideKeepRules": [
{
"source": "-keep class some.other.library.** { *; }",
},
...
]
},
...
}