การแสดงผล UI คือการสร้างเฟรมจากแอปและแสดงเฟรมนั้นบนหน้าจอ แอปของคุณต้องแสดงผลเฟรมภายใน 16 มิลลิวินาทีเพื่อให้ได้ 60 เฟรมต่อวินาที (fps) เพื่อให้การโต้ตอบของผู้ใช้กับแอปเป็นไปอย่างราบรื่น หากต้องการทราบว่าเหตุใดจึงควรใช้ 60 FPS โปรดดูรูปแบบประสิทธิภาพของ Android: ทำไมต้อง 60 FPS หากคุณพยายาม ให้ได้ 90 FPS หน้าต่างนี้จะลดลงเหลือ 11 มิลลิวินาที และสำหรับ 120 FPS จะเป็น 8 มิลลิวินาที
หากคุณใช้เวลาเกินกว่ากรอบเวลานี้ไป 1 มิลลิวินาที ไม่ได้หมายความว่าเฟรมจะแสดงช้าไป 1 มิลลิวินาที แต่Choreographer
จะทิ้งเฟรมไปเลย หากแอปของคุณแสดงผล UI ช้า ระบบจะบังคับให้ข้ามเฟรมและผู้ใช้จะรับรู้ว่าแอปของคุณกระตุก
ซึ่งเรียกว่าความหน่วง หน้านี้แสดงวิธีวินิจฉัยและแก้ไขอาการกระตุก
หากคุณกำลังพัฒนาเกมที่ไม่ได้ใช้ระบบ
View คุณก็ไม่ต้องใช้
Choreographer ในกรณีนี้ Frame Pacing
Library จะช่วยให้เกม OpenGL และ Vulkan แสดงผลได้อย่างราบรื่นและมี Frame Pacing ที่ถูกต้องใน Android
ระบุการกระตุก
การค้นหาโค้ดในแอปที่ทำให้เกิดการกระตุกอาจเป็นเรื่องยาก ส่วนนี้ จะอธิบาย 3 วิธีในการระบุอาการกระตุก
การตรวจสอบด้วยสายตาช่วยให้คุณดู Use Case ทั้งหมดในแอปได้ภายในไม่กี่นาที แต่จะให้รายละเอียดไม่มากเท่า Systrace Systrace ให้รายละเอียดเพิ่มเติม แต่หากคุณเรียกใช้ Systrace สำหรับกรณีการใช้งานทั้งหมดในแอป คุณอาจได้รับข้อมูลจำนวนมากจนวิเคราะห์ได้ยาก ทั้งการตรวจสอบด้วยภาพและ Systrace จะตรวจพบ Jank ในอุปกรณ์ของคุณ หากทำซ้ำ อาการกระตุกในอุปกรณ์ในพื้นที่ไม่ได้ คุณสามารถสร้างการตรวจสอบประสิทธิภาพที่กำหนดเองเพื่อวัด ส่วนที่เฉพาะเจาะจงของแอปในอุปกรณ์ที่ทำงานในภาคสนามได้
การตรวจสอบด้วยสายตา
การตรวจสอบด้วยภาพช่วยให้คุณระบุ Use Case ที่ทำให้เกิดการกระตุกได้ หากต้องการ ตรวจสอบด้วยสายตา ให้เปิดแอปและไปยังส่วนต่างๆ ของแอปด้วยตนเอง แล้วมองหาอาการกระตุกใน UI
เคล็ดลับบางส่วนในการตรวจสอบด้วยสายตามีดังนี้
- เรียกใช้แอปเวอร์ชันที่เผยแพร่แล้ว หรืออย่างน้อยก็เวอร์ชันที่แก้ไขข้อบกพร่องไม่ได้ ART รันไทม์จะปิดใช้การเพิ่มประสิทธิภาพที่สำคัญหลายอย่างเพื่อรองรับฟีเจอร์การแก้ไขข้อบกพร่อง ดังนั้นโปรดตรวจสอบว่าคุณกำลังดูสิ่งที่คล้ายกับสิ่งที่ผู้ใช้ เห็น
- เปิดใช้การแสดงผล GPU ตามโปรไฟล์ การแสดงผล GPU ของโปรไฟล์จะแสดงแถบบนหน้าจอซึ่งแสดงภาพ ว่าต้องใช้เวลานานเท่าใดในการแสดงผลเฟรมของหน้าต่าง UI เมื่อเทียบกับเกณฑ์มาตรฐาน 16 มิลลิวินาทีต่อเฟรม แต่ละแท่งมีคอมโพเนนต์สี ที่เชื่อมโยงกับขั้นตอนในไปป์ไลน์การแสดงผล คุณจึงดูได้ว่าส่วนใด ใช้เวลานานที่สุด เช่น หากเฟรมใช้เวลานานในการจัดการอินพุต ให้ดูโค้ดของแอปที่จัดการข้อมูลจากผู้ใช้
- ดูคอมโพเนนต์ที่เป็นแหล่งที่มาทั่วไปของอาการกระตุก เช่น
RecyclerView - เปิดแอปจากการเริ่มต้น แบบเย็น
- เรียกใช้แอปในอุปกรณ์ที่ช้ากว่าเพื่อทำให้ปัญหาแย่ลง
เมื่อพบ Use Case ที่ทำให้เกิด Jank คุณอาจทราบดีว่าอะไรเป็นสาเหตุที่ทำให้เกิด Jank ในแอป หากต้องการข้อมูลเพิ่มเติม คุณสามารถใช้ Systrace เพื่อดูสาเหตุเพิ่มเติมได้
Systrace
แม้ว่า Systrace จะเป็นเครื่องมือที่แสดงสิ่งที่อุปกรณ์ทั้งหมดกำลังทำ แต่ก็มีประโยชน์ในการระบุอาการกระตุกในแอปของคุณ Systrace มีค่าใช้จ่ายเพิ่มเติมของระบบน้อยที่สุด คุณจึงพบอาการกระตุกที่สมจริงได้ในระหว่างการวัดประสิทธิภาพ
บันทึกการติดตามด้วย Systrace ขณะดำเนินการ กรณีการใช้งานที่กระตุกในอุปกรณ์ ดูวิธีการใช้ Systrace ได้ที่ บันทึกการติดตามระบบในบรรทัดคำสั่ง Systrace จะแบ่งตามกระบวนการและ เธรด มองหากระบวนการของแอปใน Systrace ซึ่งจะมีลักษณะคล้ายกับ รูปที่ 1
ตัวอย่าง Systrace ในรูปที่ 1 มีข้อมูลต่อไปนี้สำหรับ ระบุอาการกระตุก
- Systrace จะแสดงเมื่อมีการวาดแต่ละเฟรมและกำหนดรหัสสีให้กับแต่ละเฟรมเพื่อ ไฮไลต์เวลาในการแสดงผลที่ช้า ซึ่งจะช่วยให้คุณค้นหาเฟรมที่กระตุกแต่ละเฟรมได้แม่นยำกว่าการตรวจสอบด้วยสายตา ดูข้อมูลเพิ่มเติมได้ที่ ตรวจสอบเฟรมและการแจ้งเตือนของ UI
- Systrace ตรวจหาปัญหาในแอปและแสดงการแจ้งเตือนทั้งใน แต่ละเฟรมและแผงการแจ้งเตือน เราขอแนะนำให้ทำตามวิธีการในข้อความแจ้ง
- ส่วนต่างๆ ของเฟรมเวิร์กและไลบรารี Android เช่น
RecyclerViewมีเครื่องหมายการติดตาม ดังนั้นไทม์ไลน์ Systrace จึงแสดงเวลาที่เรียกใช้เมธอดเหล่านั้นใน UI Thread และระยะเวลาที่ใช้ในการเรียกใช้
หลังจากดูเอาต์พุต Systrace แล้ว คุณอาจพบเมธอดในแอปที่สงสัยว่าทำให้เกิดอาการกระตุก เช่น หากไทม์ไลน์แสดงว่าเฟรมช้าเกิดจากRecyclerViewใช้เวลานาน คุณสามารถเพิ่มเหตุการณ์การติดตามที่กำหนดเองลงในโค้ดที่เกี่ยวข้องและเรียกใช้ Systrace อีกครั้งเพื่อดูข้อมูลเพิ่มเติม ใน Systrace ใหม่ ไทม์ไลน์จะแสดง
เมื่อมีการเรียกใช้เมธอดของแอปและระยะเวลาที่ใช้ในการดำเนินการ
หาก Systrace ไม่แสดงรายละเอียดเกี่ยวกับสาเหตุที่การทำงานของเธรด UI ใช้เวลานาน ให้ใช้ เครื่องมือสร้างโปรไฟล์ CPU ของ Android เพื่อบันทึกร่องรอยเมธอดแบบสุ่มตัวอย่างหรือแบบวัดประสิทธิภาพ โดยทั่วไปแล้ว การติดตามเมธอดไม่เหมาะสำหรับ การระบุ Jank เนื่องจากทำให้เกิด Jank ที่เป็นผลบวกลวงเนื่องจากมี ค่าใช้จ่ายสูง และไม่สามารถดูได้ว่าเมื่อใดที่เธรดทำงานและเมื่อใดที่ถูกบล็อก แต่ การติดตามเมธอดจะช่วยให้คุณระบุเมธอดในแอปที่ใช้เวลานานที่สุดได้ หลังจากระบุเมธอดเหล่านี้แล้ว ให้เพิ่มเครื่องหมาย Trace แล้วเรียกใช้ Systrace อีกครั้งเพื่อดูว่าเมธอดเหล่านี้ทำให้เกิดอาการกระตุกหรือไม่
ดูข้อมูลเพิ่มเติมได้ที่ทำความเข้าใจ Systrace
การตรวจสอบประสิทธิภาพที่กำหนดเอง
หากจำลองอาการกระตุกในอุปกรณ์ในพื้นที่ไม่ได้ คุณสามารถสร้างการตรวจสอบประสิทธิภาพที่กำหนดเองลงในแอปเพื่อช่วยระบุแหล่งที่มาของอาการกระตุกในอุปกรณ์ภาคสนามได้
โดยให้รวบรวมเวลาในการแสดงผลเฟรมจากส่วนที่เฉพาะเจาะจงของแอปด้วย
FrameMetricsAggregator
และบันทึกและวิเคราะห์ข้อมูลโดยใช้ Firebase Performance
Monitoring
ดูข้อมูลเพิ่มเติมได้ที่เริ่มต้นใช้งาน Performance Monitoring สำหรับ Android
เฟรมที่หยุดทำงาน
เฟรมที่ค้างคือเฟรม UI ที่ใช้เวลาในการแสดงผลนานกว่า 700 มิลลิวินาที นี่เป็นปัญหาเนื่องจากแอปของคุณดูเหมือนจะค้างและไม่ตอบสนองต่อข้อมูลจากผู้ใช้ เป็นเวลาเกือบ 1 วินาทีขณะที่เฟรมกำลังแสดงผล เราขอแนะนำให้เพิ่มประสิทธิภาพ แอปให้แสดงผลเฟรมภายใน 16 มิลลิวินาทีเพื่อให้ UI ทำงานได้อย่างราบรื่น อย่างไรก็ตาม ในระหว่างการเริ่มต้นแอปหรือขณะเปลี่ยนไปหน้าจออื่น เป็นเรื่องปกติที่เฟรมเริ่มต้นจะใช้เวลานานกว่า 16 มิลลิวินาทีในการวาด เนื่องจากแอปต้องขยายมุมมอง จัดวางหน้าจอ และทำการวาดเริ่มต้นทั้งหมดตั้งแต่ต้น ด้วยเหตุนี้ Android จึงติดตามเฟรมที่ค้างแยกต่างหากจากการแสดงผลช้า เฟรมในแอปไม่ควรใช้เวลาในการแสดงผลนานกว่า 700 มิลลิวินาที
เฟรมค้างเป็นรูปแบบหนึ่งของการแสดงผลช้า ดังนั้นขั้นตอนการวินิจฉัยและแก้ไขปัญหาจึงเหมือนกัน
การติดตามความกระตุก
FrameTimeline ใน Perfetto ช่วยติดตามเฟรมที่ช้าหรือ หยุดนิ่งได้
ความสัมพันธ์ระหว่างเฟรมที่ช้า เฟรมที่ค้าง และ ANR
เฟรมช้า เฟรมหยุดนิ่ง และ ANR เป็นรูปแบบต่างๆ ของ Jank ที่แอปของคุณอาจพบ โปรดดูความแตกต่างในตารางด้านล่าง
| เฟรมที่ช้า | เฟรมที่หยุดทำงาน | ANR | |
|---|---|---|---|
| เวลาในการแสดงผล | ระหว่าง 16 มิลลิวินาทีถึง 700 มิลลิวินาที | ระหว่าง 700 มิลลิวินาทีถึง 5 วินาที | มากกว่า 5 วินาที |
| พื้นที่ที่ผู้ใช้ได้รับผลกระทบ |
|
|
|
ติดตามเฟรมที่ช้าและเฟรมที่ค้างแยกกัน
ในระหว่างการเริ่มต้นแอปหรือขณะเปลี่ยนไปใช้หน้าจออื่น เป็นเรื่องปกติที่เฟรมเริ่มต้นจะใช้เวลานานกว่า 16 มิลลิวินาทีในการวาด เนื่องจากแอปต้องขยายมุมมอง จัดวางหน้าจอ และทำการวาดเริ่มต้นตั้งแต่ต้น
แนวทางปฏิบัติแนะนำในการจัดลำดับความสำคัญและแก้ไขอาการกระตุก
โปรดคำนึงถึงแนวทางปฏิบัติแนะนำต่อไปนี้เมื่อต้องการแก้ไขการกระตุกในแอป
- ระบุและแก้ไขอินสแตนซ์ของ Jank ที่ทำซ้ำได้ง่ายที่สุด
- จัดลำดับความสำคัญของ ANR แม้ว่าเฟรมที่ช้าหรือหยุดนิ่งอาจทำให้แอปดู อืดอาด แต่ ANR จะทำให้แอปหยุดตอบสนอง
- การแสดงผลช้าเป็นปัญหาที่จำลองได้ยาก แต่คุณสามารถเริ่มต้นด้วยการหยุดเฟรมที่ค้างนาน 700 มิลลิวินาที ซึ่งมักเกิดขึ้นขณะที่แอปเริ่มต้นหรือเปลี่ยนหน้าจอ
การแก้ไขการกระตุก
หากต้องการแก้ไขอาการกระตุก ให้ตรวจสอบเฟรมที่ไม่เสร็จสมบูรณ์ใน 16 มิลลิวินาทีและดูว่ามีอะไรผิดปกติ ตรวจสอบว่า Record View#draw หรือ Layout ใช้เวลานานผิดปกติใน
บางเฟรมหรือไม่ ดูปัญหาเหล่านี้และปัญหาอื่นๆ ได้ที่แหล่งที่มาทั่วไปของอาการกระตุก
หากต้องการหลีกเลี่ยงอาการกระตุก ให้เรียกใช้งานที่ใช้เวลานานแบบไม่พร้อมกันนอกเทรด UI โปรดทราบเสมอว่าโค้ดของคุณทำงานในเธรดใด และใช้ความระมัดระวังเมื่อ โพสต์งานที่ไม่ใช่เรื่องเล็กน้อยไปยังเธรดหลัก
หากคุณมี UI หลักที่ซับซ้อนและสำคัญสำหรับแอป เช่น รายการเลื่อนตรงกลาง ให้ลองเขียนการทดสอบเครื่องมือที่ตรวจหาเวลาในการแสดงผลที่ช้าโดยอัตโนมัติและเรียกใช้การทดสอบบ่อยๆ เพื่อป้องกันการถดถอย
แหล่งที่มาทั่วไปของการกระตุก
ส่วนต่อไปนี้จะอธิบายแหล่งที่มาทั่วไปของการกระตุกในแอปที่ใช้View
ระบบและแนวทางปฏิบัติแนะนำในการแก้ไขปัญหาดังกล่าว ดูข้อมูลเกี่ยวกับการแก้ไขปัญหาด้านประสิทธิภาพด้วย Jetpack Compose ได้ที่ประสิทธิภาพของ Jetpack
Compose
รายการที่เลื่อนได้
ListView และโดยเฉพาะอย่างยิ่ง
RecyclerView มักใช้กับรายการเลื่อนที่ซับซ้อนซึ่งมีแนวโน้มที่จะเกิดอาการกระตุกมากที่สุด
ทั้ง 2 อย่างมีเครื่องหมาย Systrace คุณจึงใช้ Systrace
เพื่อดูว่าเครื่องหมายเหล่านี้ทำให้แอปเกิดอาการกระตุกหรือไม่ได้ ส่งอาร์กิวเมนต์บรรทัดคำสั่ง
-a
<your-package-name> เพื่อให้ส่วนการติดตามใน RecyclerView รวมถึงเครื่องหมายการติดตามที่คุณเพิ่มไว้ปรากฏขึ้น หากมี ให้ทำตามคำแนะนำของ
การแจ้งเตือนที่สร้างขึ้นในเอาต์พุต Systrace ภายใน Systrace คุณสามารถคลิกส่วนที่RecyclerViewติดตามเพื่อดูคำอธิบายเกี่ยวกับงานที่ RecyclerView
กำลังทำอยู่
RecyclerView: notifyDataSetChanged()
หากเห็นว่าทุกรายการใน RecyclerView มีการผูกใหม่ (และจัดวางใหม่
และวาดใหม่ในเฟรมเดียว) ให้ตรวจสอบว่าคุณไม่ได้เรียก
notifyDataSetChanged(),
setAdapter(Adapter),
หรือ swapAdapter(Adapter,
boolean)
สำหรับการอัปเดตเล็กๆ วิธีการเหล่านี้ส่งสัญญาณว่ามีการเปลี่ยนแปลงเนื้อหาในรายการทั้งหมด
และจะปรากฏใน Systrace เป็น RV FullInvalidate แต่ให้ใช้ SortedList หรือ DiffUtil เพื่อสร้างข้อมูลอัปเดตขั้นต่ำเมื่อมีการเปลี่ยนแปลงหรือเพิ่มเนื้อหา
ตัวอย่างเช่น ลองพิจารณาแอปที่ได้รับเนื้อหาข่าวสารเวอร์ชันใหม่จากเซิร์ฟเวอร์
เมื่อโพสต์ข้อมูลนี้ไปยังอแดปเตอร์ คุณจะเรียกใช้ notifyDataSetChanged() ได้ตามตัวอย่างต่อไปนี้
Kotlin
fun onNewDataArrived(news: List<News>) { myAdapter.news = news myAdapter.notifyDataSetChanged() }
Java
void onNewDataArrived(List<News> news) { myAdapter.setNews(news); myAdapter.notifyDataSetChanged(); }
ข้อเสียของวิธีนี้คือหากมีการเปลี่ยนแปลงเล็กน้อย เช่น มีการเพิ่มรายการเดียว
ที่ด้านบน RecyclerView จะไม่ทราบ ดังนั้น ระบบจึงสั่งให้ทิ้งสถานะของรายการที่แคชไว้ทั้งหมด และต้องผูกทุกอย่างใหม่
เราขอแนะนำให้คุณใช้ DiffUtil ซึ่งจะคำนวณและส่งการอัปเดตขั้นต่ำ
ให้คุณ
Kotlin
fun onNewDataArrived(news: List<News>) { val oldNews = myAdapter.items val result = DiffUtil.calculateDiff(MyCallback(oldNews, news)) myAdapter.news = news result.dispatchUpdatesTo(myAdapter) }
Java
void onNewDataArrived(List<News> news) { List<News> oldNews = myAdapter.getItems(); DiffResult result = DiffUtil.calculateDiff(new MyCallback(oldNews, news)); myAdapter.setNews(news); result.dispatchUpdatesTo(myAdapter); }
หากต้องการแจ้งให้ DiffUtil ทราบวิธีตรวจสอบรายการของคุณ ให้กำหนด MyCallback เป็นการติดตั้งใช้งานCallback
RecyclerView: RecyclerView ที่ซ้อนกัน
การซ้อนอินสแตนซ์หลายรายการของ RecyclerView เป็นเรื่องปกติ โดยเฉพาะกับ
รายการแนวตั้งของรายการที่เลื่อนในแนวนอน ตัวอย่างของรูปแบบนี้คือตาราง
ของแอปในหน้าหลักของ Play Store วิธีนี้อาจได้ผลดี แต่ก็ทำให้มี
การย้ายมุมมองไปมาเป็นจำนวนมากด้วย
หากเห็นว่ามีการขยายรายการภายในจำนวนมากเมื่อเลื่อนลงไปที่หน้าเว็บเป็นครั้งแรก
คุณอาจต้องตรวจสอบว่าได้แชร์
RecyclerView.RecycledViewPool
ระหว่างอินสแตนซ์ภายใน (แนวนอน) ของ RecyclerView โดยค่าเริ่มต้น แต่ละ
RecyclerViewจะมีกลุ่มรายการของตัวเอง อย่างไรก็ตาม ในกรณีที่มี 12 รายการ
itemViewsบนหน้าจอพร้อมกัน จะเกิดปัญหาเมื่อรายการแนวนอนต่างๆ itemViewsไม่สามารถ
แชร์ได้ หากแถวทั้งหมดแสดงมุมมองประเภทที่คล้ายกัน
Kotlin
class OuterAdapter : RecyclerView.Adapter<OuterAdapter.ViewHolder>() { ... override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { // Inflate inner item, find innerRecyclerView by ID. val innerLLM = LinearLayoutManager(parent.context, LinearLayoutManager.HORIZONTAL, false) innerRv.apply { layoutManager = innerLLM recycledViewPool = sharedPool } return OuterAdapter.ViewHolder(innerRv) } ...
Java
class OuterAdapter extends RecyclerView.Adapter<OuterAdapter.ViewHolder> { RecyclerView.RecycledViewPool sharedPool = new RecyclerView.RecycledViewPool(); ... @Override public void onCreateViewHolder(ViewGroup parent, int viewType) { // Inflate inner item, find innerRecyclerView by ID. LinearLayoutManager innerLLM = new LinearLayoutManager(parent.getContext(), LinearLayoutManager.HORIZONTAL); innerRv.setLayoutManager(innerLLM); innerRv.setRecycledViewPool(sharedPool); return new OuterAdapter.ViewHolder(innerRv); } ...
หากต้องการเพิ่มประสิทธิภาพให้ดียิ่งขึ้น คุณยังเรียกใช้
setInitialPrefetchItemCount(int)
ใน
LinearLayoutManager
ของ RecyclerView ด้านในได้ด้วย เช่น หากคุณมีรายการที่มองเห็นได้ 3.5 รายการในแถวเสมอ ให้เรียกใช้ innerLLM.setInitialItemPrefetchCount(4) ซึ่งเป็นการส่งสัญญาณไปยัง
RecyclerView ว่าเมื่อแถวแนวนอนกำลังจะปรากฏบนหน้าจอ
จะต้องพยายามดึงข้อมูลล่วงหน้าสำหรับรายการภายในหากมีเวลาเหลือในเธรด UI
RecyclerView: การขยายมากเกินไปหรือการสร้างใช้เวลานานเกินไป
ในกรณีส่วนใหญ่ ฟีเจอร์การดึงข้อมูลล่วงหน้าใน RecyclerView จะช่วยลดผลกระทบจาก
ต้นทุนที่เพิ่มขึ้นได้ด้วยการทำงานล่วงหน้าขณะที่เธรด UI ไม่ได้ใช้งาน
หากเห็นว่าเฟรมเพิ่มขึ้นระหว่างเฟรมและไม่ได้อยู่ในส่วนที่มีป้ายกำกับว่า RV
Prefetch โปรดตรวจสอบว่าคุณกำลังทดสอบในอุปกรณ์ที่รองรับและใช้ไลบรารีการสนับสนุนเวอร์ชันล่าสุด
การดึงข้อมูลล่วงหน้ารองรับเฉพาะใน Android 5.0 API ระดับ 21 ขึ้นไป
หากเห็นว่าอัตราเงินเฟ้อทำให้เกิดอาการกระตุกบ่อยครั้งเมื่อมีรายการใหม่ๆ ปรากฏบนหน้าจอ ให้ตรวจสอบว่าคุณไม่ได้มีประเภทมุมมองมากกว่าที่จำเป็น ยิ่งมีประเภทมุมมองในเนื้อหาของ RecyclerView น้อยเท่าใด ก็ยิ่งต้องขยายขนาดน้อยลงเมื่อมีไอเทมประเภทใหม่ปรากฏบนหน้าจอ หากเป็นไปได้ ให้ผสานประเภทมุมมองเมื่อเหมาะสม หากมีการเปลี่ยนแปลงเฉพาะไอคอน สี หรือข้อความระหว่างประเภท คุณสามารถทำการเปลี่ยนแปลงนั้นในเวลาที่เชื่อมโยงและหลีกเลี่ยงการขยาย ซึ่งจะช่วยลดการใช้หน่วยความจำของแอปได้ในเวลาเดียวกัน
หากประเภทมุมมองดูดีแล้ว ให้พิจารณาลดต้นทุนของอัตราเงินเฟ้อ
การลดมุมมองคอนเทนเนอร์และโครงสร้างที่ไม่จำเป็นจะช่วยได้ ลองสร้างitemViewsด้วย ConstraintLayout
ซึ่งจะช่วยลดการดูแบบโครงสร้างได้
หากต้องการเพิ่มประสิทธิภาพให้ดียิ่งขึ้น และลำดับชั้นของรายการเป็นแบบ ง่ายๆ และคุณไม่ต้องการฟีเจอร์การจัดธีมและสไตล์ที่ซับซ้อน ให้ลองเรียก ตัวสร้างด้วยตนเอง แต่ก็มักจะไม่คุ้มค่าที่จะแลกกับความเรียบง่ายและฟีเจอร์ของ XML
RecyclerView: การเชื่อมโยงใช้เวลานานเกินไป
การเชื่อมโยง ซึ่งก็คือ onBindViewHolder(VH,
int) ต้องตรงไปตรงมาและใช้เวลาน้อยกว่า 1 มิลลิวินาทีสำหรับทุกรายการยกเว้นรายการที่ซับซ้อนที่สุด โดยจะต้องใช้ Plain Old Java Object (POJO)
จากข้อมูลรายการภายในของอแดปเตอร์และเรียกใช้ตัวตั้งค่าในมุมมองใน ViewHolder หาก RV OnBindView ใช้เวลานาน ให้ตรวจสอบว่าคุณ
ทำงานน้อยที่สุดในโค้ดการเชื่อมโยง
หากคุณใช้ออบเจ็กต์ POJO พื้นฐานเพื่อเก็บข้อมูลในอแดปเตอร์ คุณจะหลีกเลี่ยงการเขียนโค้ดการเชื่อมโยงใน onBindViewHolder ได้โดยสมบูรณ์โดยใช้ไลบรารี Data Binding
RecyclerView หรือ ListView: เลย์เอาต์หรือการวาดใช้เวลานานเกินไป
หากพบปัญหาเกี่ยวกับการวาดและเลย์เอาต์ โปรดดูส่วนประสิทธิภาพของเลย์เอาต์และประสิทธิภาพการแสดงผล
ListView: การขยาย
คุณอาจปิดใช้การรีไซเคิลโดยไม่ตั้งใจใน ListViewหากไม่ระมัดระวัง หากคุณเห็นการขยายทุกครั้งที่รายการปรากฏบนหน้าจอ ให้ตรวจสอบว่าการติดตั้งใช้งาน
Adapter.getView()
ของคุณกำลังใช้ การมิวส์ การเชื่อมโยงใหม่ และการส่งคืนพารามิเตอร์ convertView หากการติดตั้งใช้งาน
getView()ของคุณเพิ่มขึ้นเสมอ แอปจะไม่ได้รับประโยชน์จากการ
รีไซเคิลใน ListView โครงสร้างของ getView() ต้องคล้ายกับการติดตั้งใช้งานต่อไปนี้เกือบทุกครั้ง
Kotlin
fun getView(position: Int, convertView: View?, parent: ViewGroup): View { return (convertView ?: layoutInflater.inflate(R.layout.my_layout, parent, false)).apply { // Bind content from position to convertView. } }
Java
View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { // Only inflate if no convertView passed. convertView = layoutInflater.inflate(R.layout.my_layout, parent, false) } // Bind content from position to convertView. return convertView; }
ประสิทธิภาพของเลย์เอาต์
หาก Systrace แสดงว่าส่วนเลย์เอาต์ของ Choreographer#doFrame ทำงานมากเกินไปหรือบ่อยเกินไป แสดงว่าคุณกำลังพบปัญหาด้านประสิทธิภาพของเลย์เอาต์ ประสิทธิภาพเลย์เอาต์ของแอปขึ้นอยู่กับส่วนใดของลำดับชั้นการแสดงผลที่มีพารามิเตอร์หรืออินพุตเลย์เอาต์ที่เปลี่ยนแปลง
ประสิทธิภาพเลย์เอาต์: ต้นทุน
หากกลุ่มยาวกว่า 2-3 มิลลิวินาที คุณอาจ
พบประสิทธิภาพการซ้อนในกรณีที่แย่ที่สุดสำหรับ
RelativeLayouts หรือ
weighted-LinearLayouts เลย์เอาต์แต่ละรายการ
เหล่านี้สามารถทริกเกอร์การวัดและการส่งผ่านเลย์เอาต์หลายรายการขององค์ประกอบย่อยได้ ดังนั้น
การซ้อนเลย์เอาต์อาจทำให้เกิดลักษณะการทำงานของ O(n^2) ตามระดับการซ้อน
พยายามหลีกเลี่ยงRelativeLayoutหรือฟีเจอร์น้ำหนักของLinearLayoutในทุกโหนด ยกเว้น
โหนดล่างสุดของลำดับชั้น โดยคุณทำได้ดังนี้
- จัดระเบียบมุมมองโครงสร้างใหม่
- กำหนดตรรกะเลย์เอาต์ที่กำหนดเอง ดูตัวอย่างที่เฉพาะเจาะจงได้ที่เพิ่มประสิทธิภาพลำดับชั้นของการออกแบบ
คุณลองเปลี่ยนไปใช้
ConstraintLayoutซึ่งมี ฟีเจอร์คล้ายกันได้โดยไม่มีข้อเสียด้านประสิทธิภาพ
ประสิทธิภาพเลย์เอาต์: ความถี่
การจัดวางคาดว่าจะเกิดขึ้นเมื่อมีเนื้อหาใหม่ปรากฏบนหน้าจอ เช่น เมื่อรายการใหม่เลื่อนเข้ามาในมุมมองใน RecyclerView หากมีการจัดวางที่สำคัญในแต่ละเฟรม คุณอาจกำลังเคลื่อนไหวเลย์เอาต์ ซึ่งมีแนวโน้มที่จะทำให้เฟรมหลุด
โดยทั่วไปแล้ว ภาพเคลื่อนไหวต้องทำงานในพร็อพเพอร์ตี้การวาดของ View เช่น รายการต่อไปนี้
คุณสามารถเปลี่ยนการตั้งค่าเหล่านี้ได้ทั้งหมดในราคาที่ถูกกว่าพร็อพเพอร์ตี้เลย์เอาต์มาก เช่น
padding หรือ margins โดยทั่วไปแล้ว การเปลี่ยนพร็อพเพอร์ตี้การวาดของมุมมองโดยการเรียกใช้ Setter ซึ่งทริกเกอร์ invalidate() ตามด้วย draw(Canvas)
ในเฟรมถัดไปก็มีราคาถูกกว่ามากเช่นกัน ซึ่งจะบันทึกการวาดภาพใหม่สำหรับมุมมองที่
ไม่ถูกต้อง และโดยทั่วไปแล้วจะมีค่าใช้จ่ายน้อยกว่าเลย์เอาต์มาก
ประสิทธิภาพการแสดงผล
UI ของ Android ทำงานใน 2 เฟส ดังนี้
- Record View#draw ในเธรด UI ซึ่งจะทำงาน
draw(Canvas)ในทุกมุมมองที่ทำให้ไม่ถูกต้อง และเรียกใช้การโทรไปยังมุมมองที่กำหนดเองหรือไปยังโค้ดของคุณได้ - DrawFrame ใน
RenderThreadซึ่งทำงานบนRenderThreadแต่จะทำงานตามงานที่สร้างขึ้นในระยะ Record View#draw
ประสิทธิภาพการแสดงผล: เธรด UI
หากRecord View#draw ใช้เวลานาน มักเป็นเพราะมีการวาดบิตแมปในเทรด UI การวาดภาพลงในบิตแมปใช้การแสดงผล CPU ดังนั้น โดยทั่วไปแล้วควรหลีกเลี่ยงการดำเนินการนี้เมื่อเป็นไปได้ คุณสามารถใช้ร่องรอยเมธอดกับ เครื่องมือสร้างโปรไฟล์ CPU ของ Android เพื่อดูว่านี่เป็นปัญหาหรือไม่
การวาดลงในบิตแมปมักจะเกิดขึ้นเมื่อแอปต้องการตกแต่งบิตแมปก่อน แสดง ซึ่งบางครั้งอาจเป็นการตกแต่ง เช่น การเพิ่มมุมโค้ง
Kotlin
val paint = Paint().apply { isAntiAlias = true } Canvas(roundedOutputBitmap).apply { // Draw a round rect to define the shape: drawRoundRect( 0f, 0f, roundedOutputBitmap.width.toFloat(), roundedOutputBitmap.height.toFloat(), 20f, 20f, paint ) paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.MULTIPLY) // Multiply content on top to make it rounded. drawBitmap(sourceBitmap, 0f, 0f, paint) setBitmap(null) // Now roundedOutputBitmap has sourceBitmap inside, but as a circle. }
Java
Canvas bitmapCanvas = new Canvas(roundedOutputBitmap); Paint paint = new Paint(); paint.setAntiAlias(true); // Draw a round rect to define the shape: bitmapCanvas.drawRoundRect(0, 0, roundedOutputBitmap.getWidth(), roundedOutputBitmap.getHeight(), 20, 20, paint); paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); // Multiply content on top to make it rounded. bitmapCanvas.drawBitmap(sourceBitmap, 0, 0, paint); bitmapCanvas.setBitmap(null); // Now roundedOutputBitmap has sourceBitmap inside, but as a circle.
หากคุณกำลังทำงานประเภทนี้ในเธรด UI คุณสามารถทำสิ่งนี้ในเธรดการถอดรหัสในเบื้องหลังแทนได้ ในบางกรณี เช่น ตัวอย่างก่อนหน้า
คุณสามารถทำงานนี้ในเวลาที่วาดได้ด้วย ดังนั้น หากโค้ด
Drawable หรือ
View มีลักษณะดังนี้
Kotlin
fun setBitmap(bitmap: Bitmap) { mBitmap = bitmap invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawBitmap(mBitmap, null, paint) }
Java
void setBitmap(Bitmap bitmap) { mBitmap = bitmap; invalidate(); } void onDraw(Canvas canvas) { canvas.drawBitmap(mBitmap, null, paint); }
คุณแทนที่ด้วยข้อความต่อไปนี้ได้
Kotlin
fun setBitmap(bitmap: Bitmap) { shaderPaint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) invalidate() } override fun onDraw(canvas: Canvas) { canvas.drawRoundRect(0f, 0f, width, height, 20f, 20f, shaderPaint) }
Java
void setBitmap(Bitmap bitmap) { shaderPaint.setShader( new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); invalidate(); } void onDraw(Canvas canvas) { canvas.drawRoundRect(0, 0, width, height, 20, 20, shaderPaint); }
คุณยังทำเช่นนี้ได้เพื่อการป้องกันพื้นหลัง เช่น เมื่อวาดการไล่ระดับสี
บนบิตแมป และการกรองรูปภาพด้วย
ColorMatrixColorFilter ซึ่งเป็นอีก 2
การดำเนินการทั่วไปที่ใช้ในการแก้ไขบิตแมป
หากคุณวาดลงในบิตแมปด้วยเหตุผลอื่น ซึ่งอาจใช้เป็นแคช ให้ลองวาดลงใน Canvas ที่เร่งด้วยฮาร์ดแวร์ซึ่งส่งไปยัง View หรือ Drawable โดยตรง หากจำเป็น ให้พิจารณาเรียกใช้
setLayerType()
ด้วย
LAYER_TYPE_HARDWARE
เพื่อแคชเอาต์พุตการแสดงผลที่ซับซ้อนและยังคงใช้ประโยชน์จากการแสดงผลด้วย GPU
ประสิทธิภาพการแสดงผล: RenderThread
Canvas การดำเนินการบางอย่างบันทึกได้ในราคาถูก แต่ทำให้เกิดการคำนวณที่มีค่าใช้จ่ายสูงใน RenderThread โดยทั่วไปแล้ว Systrace จะแจ้งเตือนเกี่ยวกับปัญหาเหล่านี้
การทำให้เส้นทางขนาดใหญ่เคลื่อนไหว
เมื่อเรียกใช้
Canvas.drawPath() ใน Canvas ที่เร่งด้วยฮาร์ดแวร์ซึ่งส่งไปยัง View แล้ว Android จะวาดเส้นทางเหล่านี้บน CPU ก่อน แล้วอัปโหลดไปยัง GPU
หากมีเส้นทางขนาดใหญ่ ให้หลีกเลี่ยงการแก้ไขเส้นทางจากเฟรมหนึ่งไปยังอีกเฟรมหนึ่ง เพื่อให้ระบบแคชและวาดเส้นทางได้อย่างมีประสิทธิภาพ
drawPoints(),
drawLines() และ drawRect/Circle/Oval/RoundRect() มีประสิทธิภาพมากขึ้นและ
ใช้งานได้ดีกว่าแม้ว่าคุณจะใช้ Draw Call มากขึ้นก็ตาม
Canvas.clipPath
clipPath(Path)
ทําให้เกิดลักษณะการทำงานของการตัดที่ใช้ทรัพยากรมาก และโดยทั่วไปควรหลีกเลี่ยง หากเป็นไปได้ ให้เลือกวาดรูปร่างแทนการตัดให้เป็นรูปร่างที่ไม่ใช่สี่เหลี่ยม ซึ่งทำงานได้ดีกว่าและรองรับการป้องกันรอยหยัก ตัวอย่างเช่น การเรียกใช้ clipPath ต่อไปนี้อาจแสดงได้แตกต่างกัน
Kotlin
canvas.apply { save() clipPath(circlePath) drawBitmap(bitmap, 0f, 0f, paint) restore() }
Java
canvas.save(); canvas.clipPath(circlePath); canvas.drawBitmap(bitmap, 0f, 0f, paint); canvas.restore();
แต่ให้แสดงตัวอย่างก่อนหน้าดังนี้
Kotlin
paint.shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP) // At draw time: canvas.drawPath(circlePath, mPaint)
Java
// One time init: paint.setShader(new BitmapShader(bitmap, TileMode.CLAMP, TileMode.CLAMP)); // At draw time: canvas.drawPath(circlePath, mPaint);
การอัปโหลดบิตแมป
Android จะแสดงบิตแมปเป็นเท็กซ์เจอร์ OpenGL และเมื่อมีการแสดงบิตแมปในเฟรมเป็นครั้งแรก ระบบจะอัปโหลดบิตแมปลงใน GPU คุณจะเห็นข้อความนี้ใน Systrace เป็น อัปโหลดเท็กซ์เจอร์(รหัส) กว้าง x สูง ซึ่งอาจใช้เวลาหลายมิลลิวินาที ดังที่แสดงในรูปที่ 2 แต่จำเป็นต้องใช้เพื่อแสดงรูปภาพด้วย GPU
หากใช้เวลานาน ให้ตรวจสอบหมายเลขความกว้างและความสูงใน ร่องรอยก่อน ตรวจสอบว่าบิตแมปที่แสดงมีขนาดไม่ใหญ่กว่า พื้นที่บนหน้าจอที่แสดงอย่างเห็นได้ชัด ซึ่งจะทำให้เสียเวลาในการอัปโหลดและ หน่วยความจำ โดยทั่วไป ไลบรารีการโหลดบิตแมปจะมีวิธีการขอ บิตแมปที่มีขนาดเหมาะสม
ใน Android 7.0 โค้ดการโหลดบิตแมป ซึ่งโดยทั่วไปจะดำเนินการโดยไลบรารี สามารถเรียกใช้
prepareToDraw() เพื่อ
ทริกเกอร์การอัปโหลดก่อนเวลาที่จำเป็น ด้วยวิธีนี้ การอัปโหลดจะเกิดขึ้นตั้งแต่เนิ่นๆ
ขณะที่ RenderThread ไม่ได้ใช้งาน คุณทำได้หลังจากถอดรหัสหรือเมื่อเชื่อมโยง
บิตแมปกับมุมมอง ตราบใดที่คุณทราบบิตแมป โดยปกติแล้วไลบรารีการโหลดบิตแมป
จะจัดการให้คุณ แต่หากคุณจัดการเองหรือต้องการให้แน่ใจว่า
คุณจะไม่พบข้อจำกัดในการอัปโหลดในอุปกรณ์รุ่นใหม่ คุณสามารถเรียกใช้ prepareToDraw() ในโค้ดของคุณเองได้
prepareToDraw()การหน่วงเวลาการกำหนดเวลาของเธรด
ตัวกำหนดเวลาของเธรดเป็นส่วนหนึ่งของระบบปฏิบัติการ Android ที่มีหน้าที่ ตัดสินใจว่าเธรดใดในระบบที่ต้องทำงาน เมื่อใดที่เธรดทำงาน และนานเท่าใด
บางครั้ง Jank เกิดขึ้นเนื่องจาก UI Thread ของแอปถูกบล็อกหรือไม่ได้ทำงาน Systrace ใช้สีต่างๆ ดังที่แสดงในรูปที่ 3 เพื่อระบุว่าเมื่อใดที่เธรด หยุดทำงาน (สีเทา) พร้อมทำงาน (สีน้ำเงิน: ทำงานได้ แต่ตัวกำหนดเวลา ยังไม่ได้เลือกให้ทำงาน) ทำงานอยู่ (สีเขียว) หรือหยุดทำงานโดยไม่สามารถขัดจังหวะได้ (สีแดงหรือสีส้ม) ซึ่งมีประโยชน์อย่างยิ่งในการแก้ไขข้อบกพร่องของปัญหาการกระตุกที่เกิดจากความล่าช้าในการจัดกำหนดการของเทรด
โดยปกติแล้วการเรียก Binder ซึ่งเป็นกลไกการสื่อสารระหว่างกระบวนการ (IPC) ใน Android จะทำให้การดำเนินการของแอปหยุดชั่วคราวเป็นเวลานาน ใน Android เวอร์ชันที่ใหม่กว่า นี่เป็นสาเหตุที่พบบ่อยที่สุดอย่างหนึ่งที่ทำให้เทรด UI หยุดทำงาน โดยทั่วไปแล้ว วิธีแก้ไขคือหลีกเลี่ยงการเรียกฟังก์ชันที่ทำการเรียก Binder หากหลีกเลี่ยงไม่ได้ ให้แคชค่าหรือย้ายงานไปยังเธรดเบื้องหลัง เมื่อโค้ดเบสมีขนาดใหญ่ขึ้น คุณอาจเพิ่มการเรียก Binder โดยไม่ตั้งใจด้วยการเรียกใช้เมธอดระดับล่างบางอย่างหากไม่ระมัดระวัง แต่คุณสามารถค้นหาและแก้ไขได้ด้วยการติดตาม
หากมีธุรกรรม Binder คุณจะบันทึกสแต็กการเรียกของธุรกรรมเหล่านั้นได้ด้วยคำสั่ง adb ต่อไปนี้
$ adb shell am trace-ipc start
… use the app - scroll/animate ...
$ adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
$ adb pull /data/local/tmp/ipc-trace.txt
บางครั้งการเรียกที่ดูเหมือนไม่มีพิษมีภัย เช่น
getRefreshRate() อาจ
ทริกเกอร์ธุรกรรมของ Binder และทำให้เกิดปัญหาใหญ่เมื่อมีการเรียก
บ่อยๆ การติดตามเป็นระยะจะช่วยให้คุณค้นหาและแก้ไขปัญหาเหล่านี้ได้เมื่อเกิดขึ้น
trace-ipc เพื่อติดตามและนำการเรียก Binder ออกหากไม่เห็นกิจกรรม binder แต่ยังไม่เห็นเธรด UI ทํางาน โปรดตรวจสอบว่าคุณไม่ได้รอการล็อกหรือการดําเนินการอื่นๆ จากเทรดอื่น โดยปกติแล้ว เธรด UI ไม่จำเป็นต้องรอผลลัพธ์จาก Thread อื่น เธรดอื่นๆ ต้องโพสต์ข้อมูลไปยังเธรดนี้
การจัดสรรออบเจ็กต์และระบบจัดการหน่วยความจำที่ไม่ใช้แล้ว
การจัดสรรออบเจ็กต์และระบบจัดการหน่วยความจำที่ไม่ใช้แล้ว (GC) เป็นปัญหาที่ลดลงอย่างมาก เนื่องจากมีการเปิดตัว ART เป็นรันไทม์เริ่มต้นใน Android 5.0 แต่ก็ยัง เป็นไปได้ที่จะทำให้เทรดทำงานหนักขึ้นด้วยงานเพิ่มเติมนี้ คุณสามารถจัดสรรได้ เพื่อตอบสนองต่อเหตุการณ์ที่เกิดขึ้นไม่บ่อยนักและไม่เกิดขึ้นหลายครั้งต่อวินาที เช่น ผู้ใช้แตะปุ่ม แต่โปรดทราบว่าการจัดสรรแต่ละครั้งจะมีค่าใช้จ่าย หากอยู่ในลูปที่เรียกใช้บ่อยๆ ให้หลีกเลี่ยงการจัดสรรเพื่อลดภาระของ GC
Systrace จะแสดงให้คุณเห็นว่า GC ทำงานบ่อยหรือไม่ และ Android Memory Profiler จะแสดงให้คุณเห็นว่าการจัดสรร มาจากที่ใด หากหลีกเลี่ยงการจัดสรรได้เมื่อเป็นไปได้ โดยเฉพาะในลูปที่เข้มงวด คุณก็มีโอกาสน้อยที่จะพบปัญหา
ใน Android เวอร์ชันล่าสุด GC จะทำงานในเธรดเบื้องหลังชื่อ HeapTaskDaemon โดยทั่วไป การจัดสรรจำนวนมากอาจหมายถึงการใช้ทรัพยากร CPU เพิ่มเติมใน GC ดังที่แสดงในรูปที่ 5
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- เปรียบเทียบแอป
- ภาพรวมของการวัดประสิทธิภาพของแอป
- แนวทางปฏิบัติแนะนำสำหรับการเพิ่มประสิทธิภาพแอป