เครื่องมือและประสิทธิภาพ

เขียนได้ดีกว่าเดิมใน Compose

ลำโพง

เกี่ยวกับ

เกี่ยวกับการเสวนา

Jetpack Compose ช่วยให้ออกแบบอินเทอร์เฟซผู้ใช้คุณภาพสูงได้ง่าย แต่การติดตามปัญหาที่ส่งผลต่อประสิทธิภาพของ UI อาจเป็นเรื่องยาก ปัญหาประเภทนี้อาจทำให้เกิดปัญหาที่ผู้ใช้สังเกตเห็นได้ เช่น เวลาในการตอบสนองเมื่อเริ่มต้นทำงาน และเฟรมที่หายไปซึ่งปรากฏเป็นภาพกระตุกเมื่อเลื่อนหรือภาพเคลื่อนไหวที่กระตุก

ในการพูดคุยครั้งนี้ เราจะแนะนำเครื่องมือเพื่อค้นหาสิ่งที่ส่งผลต่อประสิทธิภาพของ UI โดยมุ่งเน้นที่ Perfetto และการติดตาม (Re-)Composition จากนั้นเราจะสำรวจรูปแบบที่ควรหลีกเลี่ยงซึ่งคุณอาจใช้อยู่และทำให้คุณเสียค่าใช้จ่ายโดยไม่รู้ตัว วิธีค้นหารูปแบบดังกล่าวในโค้ด และรูปแบบที่เหมาะสมซึ่งคุณสามารถใช้แทนได้ เมื่อจบการพูดคุยนี้ คุณจะเข้าใจวิธีคิดใน Compose ด้วยแนวคิดด้านประสิทธิภาพ และวิธีทำโปรไฟล์ วิเคราะห์ และยืนยันการเพิ่มประสิทธิภาพในแอป

สรุป

  • การติดตาม Perfetto จะบันทึกกิจกรรมในอุปกรณ์และแสดงส่วนต่างๆ ของ Compose เช่น องค์ประกอบและเลย์เอาต์ตามชุดข้อความหลัก ใช้เครื่องมือเหล่านี้เพื่อตรวจสอบไทม์ไลน์ของเฟรมและติดตามระยะเวลาของเฟส
  • การดำเนินการทุกอย่างภายใน Composable มีค่าใช้จ่าย ซึ่งรวมถึงการเปิดใช้งานและการยกเลิก Coroutine ตรวจสอบ API ผลข้างเคียงและพิจารณารวมผลข้างเคียงที่มีคีย์เหมือนกันเพื่อลดภาระงานของ Composition
  • การติดตามการจัดองค์ประกอบจะกำกับเนื้อหาฟังก์ชันที่ประกอบกันได้ และการติดตามการจัดองค์ประกอบใหม่ (1.13 ขึ้นไป) จะติดตามการอ่านและการเขียนสถานะที่ทำให้เกิดการจัดองค์ประกอบใหม่ ซึ่งจะช่วยวิเคราะห์งานที่ไม่คาดคิดได้โดยตรงภายในการติดตาม Perfetto
  • ComputedState (Compose 1.13 ขึ้นไป) จะหลีกเลี่ยงการทำให้เป็นโมฆะที่ไม่พึงประสงค์ และโดยทั่วไปจะเร็วกว่า DerivedState ในระหว่างการทำให้เป็นโมฆะแบบอ่านหลังเขียน DerivedState ยังคงมีประโยชน์เมื่อการคำนวณหรือการจัดสรรที่มีค่าใช้จ่ายสูงได้รับประโยชน์จากการแคช
  • การข้าม Composable มีค่าใช้จ่ายในการดูแลรักษาและค่าใช้จ่ายเพิ่มเติมในการเปรียบเทียบพารามิเตอร์ ใช้การข้ามที่มีประสิทธิภาพและความเท่าเทียมกันของข้อมูลระบุตัวตนของออบเจ็กต์ หรือหลีกเลี่ยงการทำให้เป็นโมฆะโดยสิ้นเชิง แทนที่จะบังคับให้เกิดความเสถียร

ทักษะ

แหล่งข้อมูล

Gemini Notebook

หัวข้อ

  • jetpack-compose
  • android-performance
  • perfetto
  • การเปรียบเทียบ
  • โครูทีน
  • composition-tracing
  • android-cli

ข้อความถอดเสียง

บทนำ

  • สวัสดี

  • สวัสดีทุกคน

  • หวังว่าคุณจะได้พักผ่อนอย่างเต็มที่

  • หวังว่าคุณจะดื่มกาแฟ ทาครีมบำรุง และทำอะไรต่างๆ เรียบร้อยแล้ว

  • วันนี้เราจะมาเจาะลึกกันมากขึ้น

  • ใช่แล้ว เริ่มเลย

  • วันนี้เราจะมาเจาะลึกกันสักหน่อย

  • ผมชื่อ Andrei Shikov และวันนี้ผมจะมาแชร์เวทีกับ Andrew Bailey

  • เราทั้งคู่ทำงานในทีมรันไทม์คอมไพเลอร์ Compose และยังทำสิ่งอื่นๆ อีกมากมาย รวมถึงประสิทธิภาพด้วย

  • และหัวข้อของการพูดคุยในวันนี้คือการตอบคำถามของคุณ คำถามเกี่ยวกับประสิทธิภาพ เนื่องจาก Jetpack Compose ได้รับการกำหนดให้เป็นชุดเครื่องมือ UI เริ่มต้นสำหรับการพัฒนา Android

  • และตอนนี้หลายคนก็ถามว่าเราจะทำให้ Compose UI มีประสิทธิภาพมากที่สุดได้อย่างไร

  • วันนี้เราจะมาเจาะลึกข้อมูลอย่างละเอียดตามที่ได้บอกไปแล้ว

  • นอกจากนี้ เรายังจะแนะนำเครื่องมือบางอย่างเพื่อให้คุณทำงานได้ดีขึ้นใน Compose ด้วย

  • สิ่งแรกที่ผมอยากจะเริ่มพูดถึงคือเหตุผลที่ประสิทธิภาพมีความสำคัญต่อเรา

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

  • ดังนั้นหากภาพเคลื่อนไหวติดขัด การรับรู้จะลดลงทันที

  • ดังนั้นเมตริกและตัวเลขทั้งหมดที่เราจะพูดถึงในวันนี้จึงเป็นเมตริกพร็อกซีที่เราเลือกเพื่อแสดงความรู้สึกของผู้ใช้ที่มีต่อแอป ความรู้สึกที่มีต่อภาพเคลื่อนไหวทั้งหมด และสิ่งต่างๆ ทั้งหมดที่เกิดขึ้นบนหน้าจอ

  • ดังนั้น โดยพื้นฐานแล้ว การเพิ่มประสิทธิภาพแอปคือการเพิ่มประสิทธิภาพเพื่อประสบการณ์การใช้งานที่ดีขึ้น

  • นี่เป็นเพียงแนวคิดที่เราอยากให้คุณมีในขณะที่เราเจาะลึกหัวข้อต่างๆ มากมายในวันนี้

  • เมื่อคุณเริ่มดูประสิทธิภาพของแอป เราอยากให้คุณทำ 3 สิ่งนี้อย่างรวดเร็ว

  • สิ่งแรกคือ หากคุณวัดประสิทธิภาพ คุณจะวัดในโหมดเผยแพร่ ดังนั้นเราจึงไม่มีข้อมูลที่แก้ไขข้อบกพร่องได้จำนวนมาก และทั้งหมดนี้ทำให้แอปของเราช้าลง

  • อย่างที่ 2 คือเปิดใช้ R8 หรือเครื่องมือลดขนาดอื่นๆ

  • ในปีนี้ เราได้เปิดตัวการเพิ่มประสิทธิภาพที่ช่วยปรับปรุงประสิทธิภาพของโครูทีนได้ถึง 2 เท่า และเรายังทำสิ่งอื่นๆ อีกมากมาย

  • เราใช้ R8 เป็นอย่างมากเมื่อเพิ่มประสิทธิภาพ Compose

  • และสิ่งสุดท้าย โดยเฉพาะอย่างยิ่งสำหรับเมตริกการเริ่มต้นระบบ คุณต้องตรวจสอบว่าคุณใช้โปรไฟล์พื้นฐาน

  • ดังนั้นคลาสที่คุณโหลดในการเริ่มต้นระบบจะได้รับการคอมไพล์และโหลดไว้ล่วงหน้าเมื่อแอปนั้นเริ่มทำงาน

  • ดังที่คุณเห็นว่านี่แตกต่างจากบิลด์ของแอปที่แก้ไขข้อบกพร่องได้ตามปกติถึง 6 เท่า

  • และเมื่อคุณวัดผลสิ่งต่างๆ เหล่านั้น สิ่งเหล่านี้จะรวมเข้ากับชัยชนะอื่นๆ ที่คุณทำภายในโค้ด

  • ตอนนี้คุณได้ทำตามขั้นตอนทั้งหมดแล้ว แต่แอปยังคงกระตุกเล็กน้อย หรือยังมีปัญหาอื่นๆ เกี่ยวกับประสิทธิภาพอยู่ และนี่คือจุดที่คุณควรเข้าสู่ลูปประสิทธิภาพนี้

  • คุณต้องการเริ่มดูเมตริกบางรายการในสถานการณ์ที่เฉพาะเจาะจง

  • ตัวอย่างเช่น สมมติว่าแอปใช้เวลานานเกินไปในการโหลด คุณอาจต้องการรีเซ็ตแอป เริ่มต้นแอปจาก Launcher ตามปกติ และตรวจสอบระยะเวลาที่แอปใช้ในการโหลดหน้าจอข้อมูลแรก

  • หรือแอปอาจกระตุกเล็กน้อย คุณจึงเลื่อนดูและตรวจสอบว่าการเลื่อนนี้ทำซ้ำได้และเสถียร

  • ดังนั้นสภาพแวดล้อมที่เสถียรในสไลด์นี้จึงมีความสำคัญมาก เพราะเมื่อทำการเพิ่มประสิทธิภาพในภายหลัง คุณจะต้องตรวจสอบว่าเห็นการปรับปรุงเมตริก และยืนยันได้ว่าการเพิ่มประสิทธิภาพเหล่านั้นทำงานได้ตามที่ต้องการจริงๆ

  • การเปรียบเทียบมีประโยชน์มากในกรณีนี้ เนื่องจากสามารถบันทึกข้อมูลจำนวนมากได้อยู่แล้ว โดยจะทำการเปรียบเทียบซ้ำหลายครั้งเพื่อให้แน่ใจว่าไม่มีสัญญาณรบกวนในอุปกรณ์มากนัก และยังช่วยตั้งค่าอุปกรณ์หลายอย่างด้วย

  • นอกจากนี้ การเปรียบเทียบยังเป็นสถานการณ์ที่ทำซ้ำได้โดยค่าเริ่มต้น และนี่คือสิ่งที่เราพยายามทำที่นี่

  • แต่คุณก็สามารถใช้สคริปต์จำนวนมากได้เช่นกันหากต้องการ

  • และสุดท้าย คุณจะสร้างโปรไฟล์ ปรับปรุงบางอย่างในแอป ดูข้อมูล แล้วทำซ้ำอีกครั้ง และอาจทำซ้ำอีกครั้งด้วยเมตริกสถานการณ์ต่างๆ ที่แอปของคุณประสบปัญหา

การทำโปรไฟล์ด้วยการติดตาม Perfetto

  • เมื่อดูข้อมูล เครื่องมือแรกที่เราจะแนะนำในวันนี้และคุณควรทำความคุ้นเคยเป็นอย่างดีคือ Perfetto Trace

  • การติดตาม Perfetto จะบันทึกทุกสิ่งที่เกิดขึ้นในอุปกรณ์ขณะที่คุณบันทึกการติดตามนั้น เช่น ระหว่างการเรียกใช้ Macrobenchmark หรือสคริปต์บางอย่างที่คุณกำลังบันทึก

  • และยังเป็นประโยชน์อย่างมากสำหรับวิศวกรทุกคนที่ทำงานด้านประสิทธิภาพของ Android

  • สำหรับฉัน ฉันใช้เวลาส่วนใหญ่ในวันทำงานไปกับการดู Perfetto Trace ในช่วงนี้

  • ซึ่งจะมีลักษณะดังนี้

  • ซึ่งเป็นข้อมูลจำนวนมาก

  • อย่างที่บอกไปว่าบันทึกทุกอย่างที่เกิดขึ้นในอุปกรณ์

  • ดังนั้น คุณอาจพบปัญหาในการค้นหาตำแหน่งที่แน่นอนของแอปในตอนแรก เราจึงขอแนะนำให้ใช้แถบค้นหาที่ใช้งานง่ายนี้

  • ค้นหา Compose, ค้นหาแพ็กเกจแอป และอื่นๆ

  • เพียงแค่ยึดมั่นในสิ่งที่คุ้นเคย

  • และเมื่อคุณตั้งหลักและค้นหากระบวนการของแอป คุณจะเห็นไทม์ไลน์ของเวลาที่ใช้ในการแสดงผลเฟรมและระยะเวลาที่สิ่งต่างๆ ในแอปใช้

  • หากเราแยกออกมา 1 ตัว คุณจะเห็นลักษณะดังนี้

  • อย่างแรกคือการทำงานของเทรดหลัก

  • ซึ่งเป็นที่ที่ Compose ทำงานส่วนใหญ่

  • จากนั้นก็มี RenderThread

  • นี่คือจุดที่เราประมวลผลคำสั่ง Canvas และเข้ารหัสเป็น GPU

  • จากนั้น GPU จะทำงาน แล้วระบบจะจัดองค์ประกอบนั้น

  • วันนี้เราจะดูเฉพาะชุดข้อความหลักเท่านั้น

  • เนื่องจากนี่คือที่ที่ Compose ทำงาน

  • เรากำลังพูดถึง Compose ดังนั้นจึงควรเน้นเฉพาะเรื่องนั้น

  • และเมื่อเราแยกเฟรมนั้นในเทรดหลัก เราจะเริ่มเห็นส่วนต่างๆ ของ Compose

  • ฉันเลือกมาให้คุณบางส่วนแล้ว

  • It's like composition, measure, layout, draw.

  • หากคุณเคยได้ยินเกี่ยวกับเฟสของ Compose และเคยดูประสิทธิภาพมาก่อน คุณก็น่าจะเห็นเฟสเหล่านี้

  • ดังนั้นจึงมีส่วนย่อยแต่ละส่วนในการติดตาม Perfetto และค้นหาได้ง่ายมากว่าแต่ละระยะใช้เวลานานเท่าใด และระยะใดทำงานในแต่ละเฟรม

  • เราได้พูดคุยกันอย่างละเอียดเกี่ยวกับการเพิ่มประสิทธิภาพงานบางส่วนนี้ และคุณต้องการตรวจสอบว่าคุณทำงานที่ถูกต้องในระยะที่เหมาะสม

  • ตัวอย่างเช่น หากคุณมีภาพเคลื่อนไหวที่อัปเดตสีของกล่องหรืออะไรทำนองนั้น คุณต้องการทำให้ระยะการวาดไม่ถูกต้องเท่านั้น ไม่ใช่คอมโพสิชัน เลย์เอาต์ และอื่นๆ

  • นี่คือข้อมูลเบื้องต้นเกี่ยวกับประสิทธิภาพ และวันนี้เราจะพูดคุยกันอย่างเจาะลึกยิ่งขึ้น หรือเจาะลึกบางเรื่อง และเราจะมุ่งเน้นที่องค์ประกอบโดยเฉพาะ

  • ตอนนี้เราจะส่งต่อให้ Andrew พูดคุยกับเราเกี่ยวกับเรื่องนี้

  • ขอบคุณ Andrei

  • อย่างที่ Andrei พูดถึง เราใช้เวลาไปมากก่อนหน้านี้ในการพูดคุยเกี่ยวกับบริบทของประสิทธิภาพเกี่ยวกับการเพิ่มประสิทธิภาพ เช่น การหลีกเลี่ยงการอ่านย้อนหลัง การตรวจสอบว่าคุณเลื่อนการอ่านสถานะไปยังระยะที่ถูกต้องในวงจรของ Composition และการเพิ่มประสิทธิภาพ และการตรวจสอบว่า Composable ของคุณทำให้เป็นโมฆะตามที่คุณคาดหวัง เพื่อให้คุณไม่ต้องทำงานมากเกินไปในทุกๆ เฟรม

  • นี่เป็นจุดเริ่มต้นที่ดีมาก แต่เรามีเนื้อหาเกี่ยวกับเรื่องนี้อยู่แล้วจำนวนมาก และเราต้องการพัฒนาการสนทนานั้นให้ก้าวหน้าไปอีกขั้น

  • วันนี้เราจะมาพูดถึงสิ่งที่เหลืออยู่ใน Composition หลังจากที่คุณทำการเพิ่มประสิทธิภาพเหล่านั้น และพูดถึงสิ่งที่ Composable ทำ

  • ความจริงที่น่าเศร้าเกี่ยวกับทุกอย่างในโค้ด Compose และทุกอย่างที่คุณจัดส่งมาพร้อมกับแอปในแง่ของประสิทธิภาพก็คือ ทุกอย่างมีต้นทุนที่เกี่ยวข้อง

  • โค้ดทั้งหมดที่คุณจัดส่งในแอปไม่มีโค้ดใดที่ไม่มีค่าใช้จ่าย และแม้แต่สิ่งเล็กๆ น้อยๆ อย่างการอ่านสถานะและการเขียนสิ่งต่างๆ เช่น ผลข้างเคียงภายใน Composition ก็มีค่าใช้จ่ายที่จับต้องได้ซึ่งเกี่ยวข้องกับสิ่งเหล่านั้น และคุณควรคำนึงถึงสิ่งเหล่านี้ โดยเฉพาะอย่างยิ่งเมื่อเริ่มแยกแยะสิ่งที่ Composable ทำจริงๆ และเริ่มบีบประสิทธิภาพภายในแอปเองให้มากขึ้น

  • เพื่อแสดงให้เห็นว่าสิ่งนั้นมีลักษณะอย่างไรในระดับที่เล็กที่สุด ต่อไปนี้คือการดำเนินการที่เล็กที่สุดบางส่วนที่คุณทำได้ทุกที่ในโค้ด โดยเฉพาะภายใน Composable และที่ความละเอียดนี้ เรากำลังพูดถึงระดับนาโนวินาทีในแง่ของต้นทุนโดยประมาณของสิ่งเหล่านี้ในฐานะกรอบความคิด

  • หากต้องการสร้างความแตกต่างในประสิทธิภาพ คุณมี 2 ตัวเลือกดังนี้

  • คุณสามารถนำการดำเนินการที่มีค่าใช้จ่ายต่ำจำนวนมากออก หรือนำการดำเนินการที่มีค่าใช้จ่ายสูงจำนวนน้อยออกก็ได้

  • ในแผนภูมินี้โดยเฉพาะ โครูทีนจะอยู่ด้านที่มีค่าใช้จ่ายสูงกว่าเมื่อพิจารณาจากการดำเนินการเหล่านี้ และเป็นตัวอย่างที่ดีในการเริ่มพิจารณา เพราะหากเรานำอินสแตนซ์เหล่านี้ออกไป 2-3 รายการ เราจะเริ่มเห็นประโยชน์ที่จับต้องได้มากขึ้นในแอปพลิเคชันของเราเร็วขึ้น

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

  • ค่าใช้จ่ายในการยกเลิกเป็นสิ่งที่ควรทราบเป็นพิเศษ เนื่องจากค่าใช้จ่ายนี้จะเกิดขึ้นเป็นส่วนใหญ่เมื่อคุณกำจัดเนื้อหาและอาจย้ายไปยังส่วนอื่นๆ ภายในแอปพลิเคชัน

  • การออกจากหน้าเว็บเป็นกรณีที่ใช้ทรัพยากรสูงมากในแอปพลิเคชันจำนวนมากที่คุณต้องบีบเวลาที่ใช้ในการแสดงผลเฟรมให้ได้มากที่สุด และตรวจสอบว่าคุณใช้ทรัพยากรที่มีอยู่ให้เกิดประโยชน์สูงสุด

  • ดังนั้นคุณจึงควรคำนึงถึงต้นทุนทั้ง 2 อย่างนี้

  • ทั้งหมดนี้ ไม่ว่าเราจะดูอะไรอยู่ก็ตาม จะรวมกันเป็นเวลาประมาณ 16 ล้านนาโนวินาทีในการทำงาน

  • เราจึงมีงบประมาณบางส่วนที่ต้องใช้ และโครูทีนจะเป็นตัวอย่างที่ดีในการเริ่มต้น

การจัดการผลข้างเคียง

  • ใน Compose โครูทีนจะปรากฏบ่อยที่สุดเมื่อใช้ API เอฟเฟกต์

  • มี API มากมายที่คุณสามารถใช้กับ SideEffects ได้ตั้งแต่ฟังก์ชันการทำงานสูงไปจนถึงเวลาในการตอบสนองต่ำ

  • ในด้านที่ใช้งานได้มากที่สุดของสเปกตรัมนี้ เรามี LaunchedEffect ซึ่งรวมทุกอย่างสำหรับการทำงานแบบอะซิงโครนัส โดยเฉพาะอย่างยิ่งภาพเคลื่อนไหว สิ่งที่คุณต้องใช้ขอบเขตโครูทีน

  • แต่หากไม่จำเป็นต้องใช้ และคุณสามารถทำงานให้เสร็จได้ด้วย API ที่มีราคาถูกกว่า ก็อาจคุ้มค่าที่จะพิจารณาใช้ API ดังกล่าวแทนหากตรงกับความต้องการใน Use Case ของคุณ

  • เพื่อแสดงให้เห็นว่าในทางปฏิบัติเป็นอย่างไร ในที่นี้ฉันมีคอมโพสิเบิลมีเดียเพลเยอร์เพื่อสาธิต SideEffect ต่างๆ ทั้งหมดและเวลาที่อาจปรากฏขึ้น

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Asynchronous work and animations
        LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
    }
  • เราจะเริ่มด้วยLaunchedEffect

  • ในตัวอย่างนี้ ระบบจะใช้เพื่อโหลดข้อมูลแบบอะซิงโครนัสภายในมีเดียเพลเยอร์

  • หากต้องการขอบเขต Coroutine หรือหากต้องการ Coroutine โดยทั่วไปหรือเพื่อทำการเรียกแบบระงับ ให้ใช้ LaunchedEffect

  • แม้ว่าจะมีราคาแพงกว่าและมีค่าใช้จ่ายที่เกี่ยวข้องกับโครูทีน แต่ตราบใดที่คุณใช้ประโยชน์และให้เหตุผลได้ ก็ให้ใช้ API ที่เหมาะสมกับงาน

  • แต่หากคุณพบว่าตัวเองอยู่ในสถานการณ์ที่ใช้ LaunchedEffect แต่ไม่ได้ใช้ประโยชน์จากโครูทีน ก็อาจเป็นโอกาสดีที่จะพิจารณา API อื่นๆ ที่ทำสิ่งเดียวกันได้ แต่มีค่าใช้จ่ายน้อยกว่า

  • ในกรณีนี้ หากพิจารณาตามสเปกตรัม เราจะเห็นตัวอย่างอื่นๆ ของผลข้างเคียง เช่น DisposableEffect

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val activity = LocalActivity.current
    
        // Setup and teardown
        DisposableEffect(Unit) {
            activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
            onDispose {
                activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
            }
        }
    }
  • DisposableEffect เหมาะอย่างยิ่งเมื่อคุณมีกระบวนการตั้งค่าและแยกส่วนที่เชื่อมโยงกับเอฟเฟกต์

  • ในตัวอย่างมีเดียเพลเยอร์เดียวกันนี้ ฉันมีตัวอย่างที่ล็อกการวางแนวของกิจกรรมเป็นแนวนอนเมื่อมี Composable อยู่ แล้วปลดล็อกการวางแนวนั้นเมื่อ Composable หายไปในภายหลัง

  • หากต้องการผลลัพธ์แบบครั้งเดียวของ LaunchedEffect และ DisposableEffect แต่ไม่จำเป็นต้องใช้ Coroutine และไม่มี Dispose Effect คุณยังใช้ SideEffect ใหม่กับ API คีย์ที่เราเปิดตัวใน Compose 1.12 ได้ด้วย

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // One-shot events
        SideEffect(Unit) { mediaPlayer.play() }
    }
  • ซึ่งเป็นทางเลือกที่ดีสำหรับ API อีก 2 รายการหากคุณเคยใช้ API เหล่านั้น ไม่ว่าจะเป็น LaunchedEffect เพื่อความสะดวก หรือ DisposableEffect เนื่องจากจำเป็นต้องใช้เอฟเฟกต์แบบครั้งเดียว การไปยังส่วนต่างๆ หรือการย้ายข้อมูลไปยัง SideEffect ด้วย Keys API จะช่วยให้คุณทำสิ่งเดียวกันได้โดยมีค่าใช้จ่ายที่ต่ำลงและประสิทธิภาพที่ดีขึ้น โดยเฉพาะอย่างยิ่งหากย้ายข้อมูลหลายครั้งตลอดอายุการใช้งานของแอป

  • และสุดท้าย API ที่มีราคาถูกที่สุดคือ API SideEffect แบบธรรมดา

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Synchronize the outside world to the state of composition
        SideEffect { mediaPlayer.playbackRate = playbackRate }
    }
  • วิธีนี้เหมาะสมที่สุดเมื่อคุณต้องการซิงค์สถานะภายนอกของโลกกับสิ่งที่เกิดขึ้นใน Composition

  • ในตัวอย่างมีเดียเพลเยอร์นี้ ฉันมีอีกตัวอย่างหนึ่งที่แสดงการซิงค์สถานะการเล่นบางอย่างกับพารามิเตอร์บางอย่างที่ได้รับจาก Composition

  • สิ่งหนึ่งที่ควรคำนึงถึงคือ หากคุณตัดสินใจตรวจสอบการใช้งานผลข้างเคียงและย้ายข้อมูลไปยังผลข้างเคียงที่มีค่าใช้จ่ายน้อยกว่า โปรดทราบว่านอกเหนือจากลักษณะการทำงานและกรณีการใช้งานอื่นๆ ที่เราได้พูดถึงไปแล้ว ยังมีความแตกต่างในการรับประกันการจัดลำดับที่มีผลข้างเคียงด้วย

  • เอฟเฟกต์แบบใช้แล้วทิ้งทั้งหมดจะทำงานก่อน ตามด้วยผลข้างเคียงปกติ ไม่ว่าจะมีการคีย์หรือไม่ก็ตาม และหลังจากเฟรมเสร็จสมบูรณ์แล้ว เอฟเฟกต์ที่เปิดใช้งานทั้งหมดจะทำงานเนื่องจากมีการส่งที่เกี่ยวข้อง

  • หากคุณใช้เวลาดังกล่าวในการเปิดตัวเอฟเฟกต์ โปรดคำนึงถึงเรื่องนี้หากกำลังพิจารณาที่จะย้ายข้อมูล

  • และสำหรับการย้ายข้อมูลใดๆ โปรดทดสอบโค้ดและตรวจสอบว่าคุณไม่ได้พึ่งพาการรับประกันเวลาเหล่านี้ และโค้ดยังคงทำงานได้หลังจากนั้น

  • หากลดความซับซ้อนของผลข้างเคียงไม่ได้ คุณยังคงลดจำนวนผลข้างเคียงที่มีได้โดยการรวมผลข้างเคียงเข้าด้วยกัน

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) { viewModel.loadCart() }
        LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
        LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }
    
        CartContent(...)
    }
  • ซึ่งเป็นสิ่งที่ดีที่ควรพิจารณา โดยเฉพาะอย่างยิ่งหากคุณมีเอฟเฟกต์หลายรายการที่ใช้คีย์เดียวกัน

  • ซึ่งเป็นตัวเลือกที่ดีในการพิจารณารวมเข้าด้วยกันเนื่องจากสามารถดำเนินการพร้อมกันได้

  • ในตัวอย่างนี้ ฉันมี LaunchedEffect หลายรายการที่แต่ละรายการทำการเรียกแบบอะซิงโครนัสไปยัง ViewModel บางรายการ

  • หากฉันแทนที่ด้วย LaunchedEffect เดียว แล้วเรียกใช้ launch อื่นๆ ภายในนั้น แม้ว่าฉันจะยังมีจำนวน Coroutine เท่าเดิมที่ทำงานอยู่ที่นี่ แต่ก็ยังใช้โอเวอร์เฮดน้อยกว่าเพราะนำงานออกจาก Composition และไปไว้ใน Coroutine แทน

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) {
            launch { viewModel.loadCart() }
            launch { viewModel.loadPaymentMethods() }
            launch { viewModel.loadDeliveryAddresses() }
        }
    
        CartContent(...)
    }
  • ดังนั้น คุณควรพิจารณาย้ายข้อมูลเป็นส่วนๆ ในสถานการณ์เช่นนี้เพื่อลดผลกระทบเมื่อเป็นไปได้

  • เอฟเฟกต์และโครูทีนเป็นเพียงตัวอย่างหนึ่งของสิ่งที่คุณอาจตรวจสอบภายในแอปพลิเคชัน แต่ Composable และแอปพลิเคชันทุกรายการจะแตกต่างกันและมีโอกาสที่แตกต่างกันภายในแอปพลิเคชัน

  • หากต้องการทำความเข้าใจว่าสิ่งใดเกี่ยวข้องกับแอปพลิเคชันของคุณ เราต้องพูดถึงเครื่องมือถัดไปที่ Andrei จะแนะนำ ซึ่งก็คือการติดตามองค์ประกอบ

การติดตาม Composition และ recomposition

  • โอเค

  • ดังนั้น การติดตามการจัดองค์ประกอบภาพ หากคุณดูการสร้างเฟรม Perfetto ขึ้นมาใหม่ที่เหมือนจริงซึ่งเราเคยดูในสไลด์อื่น คุณจะเห็นว่าเฟรมนี้ให้ข้อมูลเวลาที่มีระยะต่างๆ แต่ไม่ได้ให้รายละเอียดมากนักเกี่ยวกับสิ่งที่เกิดขึ้น

  • อะไร

  • ใช่ สิ่งที่เกิดขึ้นภายใน Composition ซึ่งฟังก์ชันที่ประกอบกันได้ทำงานอยู่

  • และการติดตามองค์ประกอบช่วยเราในเรื่องนี้

  • เราจึงได้เพิ่มเครื่องหมายบางอย่างในคอมไพเลอร์ Compose ซึ่งจะใส่คำอธิบายประกอบให้กับทุกฟังก์ชันและทำให้มองเห็นได้ในการติดตาม Perfetto เพื่อให้คุณตรวจสอบได้ว่าแต่ละฟังก์ชันใช้เวลานานเท่าใด

  • คุณสามารถใช้ข้อมูลดังกล่าวเพื่อดูว่าฟังก์ชันใดที่กำลังจัดองค์ประกอบใหม่

  • อย่างไรก็ตาม วิธีนี้มีค่าใช้จ่ายเพิ่มเติมเล็กน้อย ดังนั้นหากต้องการบันทึกข้อมูลประสิทธิภาพที่แม่นยำที่สุด คุณควรใช้ Perfetto ปกติหรือเรียกใช้โดยไม่มีการบันทึก Perfetto และใช้เมตริกอื่นๆ

  • แต่การติดตามองค์ประกอบจะมีประโยชน์มากหากคุณต้องการทราบว่า Composable แต่ละรายการใช้เวลานานเท่าใด

  • บางครั้งคุณอาจต้องการทราบสาเหตุที่องค์ประกอบต่างๆ มีการจัดองค์ประกอบใหม่ ซึ่งเป็นสิ่งที่เราพยายามแก้ไขในการติดตามรันไทม์ของการติดตาม Compose รุ่นถัดไป ซึ่งก็คือ 1.13

  • ตอนนี้เราขอแนะนำการติดตามการจัดองค์ประกอบใหม่ ซึ่งจะกำกับเนื้อหาการอ่านสถานะและการเขียนสถานะทุกรายการที่ส่งผลให้เกิดการจัดองค์ประกอบใหม่

  • ดังนั้น คุณจะเห็นข้อมูลลักษณะนี้ใน Perfetto Trace ซึ่งคุณจะเห็นการเขียนสถานะแต่ละรายการและสแต็กการเรียกใช้ที่เกิดการเขียนและการอ่าน เพื่อให้คุณเชื่อมโยงกับบรรทัดจริงในโค้ดที่มีการเข้าถึงหรือแก้ไขสถานะเหล่านั้นได้

  • หากต้องการดูข้อมูลเพิ่มเติม โปรดดูที่ลิงก์นี้

  • ซึ่งมีเอกสารประกอบทั้งหมดเกี่ยวกับการติดตามทั้ง Composition และการติดตามการจัดองค์ประกอบใหม่

  • ต่อไป Andrew จะมาพูดถึงวิธีที่เราสามารถใช้สิ่งนี้ได้อย่างมีประสิทธิภาพมากขึ้นเพื่อหลีกเลี่ยงการเขียนสถานะ

การเพิ่มประสิทธิภาพสถานะและการลบล้าง

  • ขอบคุณ Andrei

  • จากผลลัพธ์ที่ได้จากการตรวจสอบนี้ คุณอาจพบว่ามีการทำให้เป็นโมฆะใน Composition ซึ่งไม่มีประโยชน์สำหรับแอปพลิเคชันและควรหลีกเลี่ยงตั้งแต่แรก

  • ตัวอย่างที่พบบ่อยที่สุดที่ฉันพบคือปุ่มเลื่อนขึ้นด้านบนซึ่งขึ้นอยู่กับระดับการมองเห็นหรือความคืบหน้าในการเลื่อนเพื่อแสดงผล

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton = scrollState.value > 0
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • ในกรณีนี้ เนื่องจากฉันกำลังอ่านค่าของสถานะการเลื่อนภายใน Composition ซึ่งเป็นสถานะที่เปลี่ยนแปลงบ่อยมาก โดยอาจเปลี่ยนแปลงได้ถึง 1 ครั้งต่อเฟรม และจะทำให้เกิดการจัดองค์ประกอบใหม่จำนวนมากที่หลีกเลี่ยงได้ เนื่องจากเงื่อนไขนี้ แม้ว่าสถานะการเลื่อนจะเปลี่ยนแปลง แต่เงื่อนไขจริงที่เราสนใจที่นี่ไม่ได้เปลี่ยนแปลงบ่อยนัก

  • นี่คือตัวอย่างที่เราขอแนะนำให้ใช้สิ่งต่างๆ เช่น สถานะที่คำนวณ ซึ่งจะช่วยหลีกเลี่ยงการทำให้เป็นโมฆะสำหรับการเปลี่ยนแปลงสถานะที่ไม่สำคัญต่อแอปพลิเคชันของคุณ

  • ดังนั้นในตัวอย่างนี้ สิ่งที่ผมทำคือรวมการคำนวณไว้ในสถานะที่คำนวณ และเมื่อเราอ่านสถานะนี้ แทนที่จะทำให้ไม่ถูกต้องตามตำแหน่งการเลื่อนเอง ระบบจะทำให้ Composable นี้ไม่ถูกต้องตามเงื่อนไขทั้งหมดและดึงมาจากสถานะอื่นเอง

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • หากคุณเคยเห็นรูปแบบนี้มาก่อน เป็นไปได้ว่าคุณเคยเห็นการใช้รูปแบบนี้กับสถานะที่ได้มาแทน

  • ComputedState เป็นฟีเจอร์ใหม่ใน Compose 1.13 และแม้ว่า ComputedState และ DerivedState จะมีเป้าหมายคล้ายกันในการทำให้เป็นโมฆะแบบมีเงื่อนไขเมื่อการคำนวณเปลี่ยนแปลง แต่วิธีการทำงานของทั้ง 2 ฟีเจอร์นี้มีความแตกต่างกันเล็กน้อย

  • DerivedState ซึ่งเราได้จัดส่งมาตั้งแต่ช่วงเริ่มต้นของ Compose จะแคชค่าที่คำนวณและนำกลับมาใช้ใหม่จนกว่าทรัพยากร Dependency จะเปลี่ยนแปลง

  • เมื่อใช้ ComputedState ระบบจะคำนวณค่าทุกครั้งที่คุณอ่าน

  • การแคชดูเหมือนจะมีประโยชน์ แต่ในหลายๆ สถานการณ์ การแคชมีค่าใช้จ่ายสูงกว่าการอ่านผลลัพธ์เพียงอย่างเดียว

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

  • ในที่นี้ ฉันมีสถานะเดียวกันซึ่งสะท้อนถึงสิ่งที่เราทำในปุ่มเลื่อนขึ้นด้านบน โดยมีเงื่อนไขบางอย่างที่อิงตามสถานะอื่นๆ และฉันมีทั้ง DerivedState และ ComputedState

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    // First read
    derivedState.value
    computedState.value
    
    // Second read
    derivedState.value
    computedState.value
  • นอกจากนี้ ฉันยังอ่านสถานะ 2 ครั้งและเราจะได้ค่าเดียวกันทั้ง 2 ครั้ง แต่สิ่งที่เกิดขึ้นเบื้องหลังนั้นแตกต่างกันระหว่างทั้ง 2 ครั้ง

  • เพื่อให้เข้าใจง่ายขึ้นว่าจริงๆ แล้วจะแสดงอะไรในเครื่องมืออย่าง Perfetto เราสามารถตรวจสอบสิ่งที่เกิดขึ้นจริงเมื่อคุณอ่านทั้ง 2 สถานะนี้

  • ในที่นี้ เราจะเริ่มด้วยการเรียกใช้ computedState.value

  • โดยจะเรียกใช้ lambda การคำนวณ ซึ่งจะอ่านค่าของสถานะ และยังมีการดูแลรักษาเล็กน้อยเพื่อแจ้งว่ามีการอ่านเกิดขึ้น

  • ซึ่งจะเป็นจริงสำหรับสถานะสแนปชอตทั้งหมดใน Compose

  • การอ่านครั้งที่ 2 จะทำงานเหมือนกับการอ่านครั้งแรกทุกประการและมีลักษณะด้านประสิทธิภาพเหมือนกันตลอด

  • เมื่อเปรียบเทียบกันแล้ว การอ่าน DerivedState ครั้งแรกจะทำสิ่งที่คล้ายกัน โดยยังคงต้องเรียกใช้แลมบ์ดาการคำนวณและยังคงทำการแจ้งเตือนอื่นๆ แต่มีค่าใช้จ่ายในการจัดการแคชเล็กน้อย ซึ่งจะลัดวงจรเล็กน้อยในครั้งแรกเนื่องจากไม่มีแคชให้ตรวจสอบ

  • ดังนั้นจึงต้องมีการดูแลรักษาเล็กน้อยสำหรับการแคช และจะทำงานเดียวกันให้เสร็จโดยใช้เวลาเพิ่มขึ้นเพียงเล็กน้อย

  • อย่างไรก็ตาม การอ่านครั้งที่ 2 สำหรับ DerivedState มีประสิทธิภาพดีกว่าการอ่านครั้งที่ 2 สำหรับ ComputedState แม้ว่าทั้ง 2 สถานะนี้จะเป็นสถานะที่ค่อนข้างธรรมดาก็ตาม

  • อย่างที่บอกไปในสไลด์ก่อนหน้า ต้นทุนของการอ่านสแนปชอตนั้นไม่สำคัญในบางกรณี และการอ่านจากแคชนั้นเร็วกว่าการอ่านเอง ซึ่งเป็นเหตุผลที่เราเห็นว่าการอ่านครั้งที่ 2 สำหรับ DerivedState นั้นชนะเพียงเล็กน้อย

  • แต่สถานการณ์ที่น่าสนใจจริงๆ คือสิ่งที่จะเกิดขึ้นเมื่อต้องทำให้สถานะเป็นโมฆะ

  • นี่คือสิ่งที่เราเรียกว่าสถานการณ์การอ่านหลังการเขียน ซึ่งคุณอัปเดตสถานะอย่างใดอย่างหนึ่งที่ใช้ในการคํานวณ และการคํานวณจะต้องดําเนินการอีกครั้ง

  • ในโค้ด เราจะทำสิ่งต่างๆ เช่น แก้ไข "a" อาจเพิ่มค่า แล้วอ่านค่าอีกครั้ง หรืออาจจัดองค์ประกอบใหม่

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    ...
    
    // Read-after-write
    a++
    derivedState.value
    computedState.value
  • สิ่งที่จะปรากฏในแผนภาพจริงจะเหมือนกันทุกประการสำหรับ ComputedState

  • ไม่มีแคชให้จัดการ ซึ่งดูเหมือนกันทุกประการ ไม่ว่าจะเป็นการอ่านครั้งแรก ครั้งที่ 2 ครั้งที่ 3 หรือครั้งใดก็ตาม

  • อย่างไรก็ตาม DerivedState มีงานที่ต้องทำอย่างชัดเจน

  • โดยจะผ่านเส้นทางที่มีค่าใช้จ่ายสูงมากเพื่อดูว่าแคชไม่ถูกต้องก่อนที่จะอัปเดตและเรียกใช้การคำนวณเอง จากนั้นจะต้องติดตามการขึ้นต่อกันและจัดการการสังเกตการณ์จำนวนมากที่ขึ้นต่อกันเพื่อเขียนค่าแคชกลับและทำความเข้าใจว่าค่าทั้งหมดเหล่านี้มีการเปลี่ยนแปลงจริงและจัดการการดูแลรักษาเอง

  • ทั้งหมดนี้รวมกันและทำให้ต้นทุนในการทำให้ DerivedState ไม่ถูกต้องมีราคาแพงมากเมื่อเทียบกับ ComputedState

  • หากต้องการระบุตัวเลขสำหรับการอ้างสิทธิ์ทั้งหมดเหล่านี้ โดยทั่วไปแล้ว ComputedState จะอ่านครั้งแรกได้เร็วกว่าประมาณ 200 นาโนวินาที

  • ComputedState มีประสิทธิภาพต่ำกว่าประมาณ 350 นาโนวินาทีใน Pixel 6 โดยเฉพาะภายใต้เงื่อนไขการเปรียบเทียบที่เฉพาะเจาะจง

  • ดังนั้นในการอ่านครั้งที่ 2 ComputedState จะสูญเสียประสิทธิภาพเล็กน้อยเมื่อเทียบกับ DerivedState

  • แต่ในกรณีที่เราสนใจจริงๆ ซึ่งเป็นกรณีที่เรากำลังทำให้เป็นโมฆะและอาจเป็นกรณีที่คุณต้องทำงานกับเฟรมจริงๆ ComputedState จะเร็วกว่าเกือบ 2 เท่า และจริงๆ แล้วเร็วกว่า 2 เท่าเมื่อเทียบกับ DerivedState

  • ดังนั้น ComputedState จึงเป็นตัวเลือกแรกที่ดีมากสำหรับการใช้งานสถานะ

  • โดยทั่วไปแล้ว คุณยังคงต้องตัดสินใจเมื่อใช้ DerivedState และ ComputedState

  • เราไม่ได้เลิกใช้งาน ComputedState แต่ยังคงมีการใช้งานทั้ง 2 อย่างอยู่

  • โดยทั่วไป หากไม่แน่ใจว่าควรเลือกสถานะใด สถานะที่คำนวณแล้วจะเป็นค่าเริ่มต้นที่ดี

  • สถานะที่ได้มาจะมีประโยชน์มากที่สุดเมื่อคุณใช้ประโยชน์จากการแคชจริงๆ ซึ่งหมายความว่าคุณทำสิ่งที่ไม่ใช่เรื่องเล็กน้อย เช่น อัลกอริทึมที่ซับซ้อน การจัดสรรขนาดใหญ่ การอ่านสถานะจำนวนมากภายในซึ่งได้รับประโยชน์จากการแคชที่นี่จริงๆ หรือสิ่งที่ไม่ใช่เรื่องเล็กน้อยหรือขึ้นอยู่กับสิ่งต่างๆ เช่น ความเท่าเทียมกันของออบเจ็กต์และความเท่าเทียมกันของเอกลักษณ์ของออบเจ็กต์ ซึ่งคุณสร้างความแตกต่างและได้รับประโยชน์จากการแคชที่สถานะที่ได้มามีให้

การข้ามและความเสถียรใน Compose

  • อีกประเด็นหนึ่งที่ผมอยากพูดถึงคือทั้งสถานะที่ได้มาและสถานะที่คำนวณต่างก็พยายามหลีกเลี่ยงการทำให้ Composable ไม่ถูกต้อง

  • ซึ่งแตกต่างจากการข้าม Composable ซึ่งเป็นสิ่งที่เรามักพูดถึงเมื่อนึกถึงการทำให้ไม่ถูกต้องตามเงื่อนไข หรือไม่ทำงานภายใน Composable

  • ความจริงที่เราต้องการเปิดเผยที่นี่คือ แม้ว่าจะมีการพูดถึงความเสถียรและการข้ามภายใน Compose อยู่มาก แต่ก็ไม่ใช่ทั้งหมด และไม่ใช่สิ่งที่เหมาะสมที่จะเพิ่มประสิทธิภาพเสมอไป

  • ดังที่กล่าวไว้ก่อนหน้านี้ ทุกอย่างใน Composition มีค่าใช้จ่ายที่เกี่ยวข้อง ซึ่งรวมถึงการข้ามด้วย

    // What you write
    @Composable fun ContactListItem(
        contact: Contact
    ) { ... }
    
    // What you get
    @Composable fun ContactListItem(
        contact: Contact,
        $composer: Composer?
        $changed: Int
    ) {
        val $dirty = $changed or
            $composer.changed(contact)
    
        if ($dirty) { /* Your function body */ }
        else $composer.skipToGroupEnd()
    }
  • การข้ามไม่ใช่เรื่องเล็กน้อยเสมอไป

  • เราต้องดูแลรักษาหลายอย่างเพื่อเปิดใช้ฟีเจอร์นี้ตั้งแต่แรก

  • เราต้องทำสิ่งต่างๆ เช่น จัดเก็บพารามิเตอร์ทั้งหมดและหาพื้นที่สำหรับพารามิเตอร์เหล่านั้นใน Composition เริ่มต้น จากนั้นอัปเดตระเบียนนั้นเมื่อคุณจัดองค์ประกอบใหม่

  • และเรายังต้องเปรียบเทียบค่าของคุณกับค่าก่อนหน้าเพื่อทำความเข้าใจว่า Composable ควรข้ามหรือไม่

  • และหลังจากนั้น ทั้งหมดนี้ก็เป็นเพียงพลังของการตรวจสอบ if ที่ข้ามโค้ดหรือไม่

  • ซึ่งอาจมีค่าใช้จ่ายสูง ทั้งนี้ขึ้นอยู่กับสิ่งที่เกิดขึ้น

  • มีการเพิ่มประสิทธิภาพเบื้องหลังสำหรับคอมไพเลอร์ ซึ่งหากคุณส่งพารามิเตอร์เดียวกัน เราจะใช้การตรวจสอบความเท่ากันซ้ำได้ แต่นี่ไม่ใช่ทั้งหมด และการเอาชนะปัญหานี้ก็ค่อนข้างง่าย

  • หากต้องการดูตัวอย่างการข้ามที่ทำให้เราผิดหวังเล็กน้อย เราสามารถดูตัวอย่างคลาสสิกของรายการแบบเลื่อนได้ซึ่งมีออบเจ็กต์ขนาดใหญ่ที่มีข้อมูลทั้งหมดที่จะประกอบบนหน้าจอ

    data class ContactsModel(
        val header: String,
        val contacts: List<Contact>
    )
    
    data class Contact(...)
    
    @Composable
    fun ContactsScreen(model: ContactsModel) {
        Column {
            TopAppBar(title = { Text(model.header) })
            ContactsList(model.contacts)
        }
    }
    
    @Composable
    fun ContactsList(contacts: List<Contact>) {
        LazyColumn {
            items(contacts) { ContactListItem(it) }
        }
    }
    
    @Composable
    fun ContactListItem(contact: Contact) { ... }
  • ดังนั้นในที่นี้ โมเดลหน้าจอจะมีบางอย่าง เช่น หน้าจอส่วนหัว และรายการรายชื่อติดต่อทั้งหมดที่ส่งผ่าน Composable 2-3 รายการที่ได้รับการเรียกใช้ Lazy List ในท้ายที่สุด แล้วแสดงบนหน้าจอพร้อมกับเซลล์รายการบางเซลล์

  • ลำดับชั้นของ Composition สำหรับโค้ดนี้จะมีลักษณะดังนี้ โดยเราจะเริ่มจากหน้าจอรายชื่อติดต่ออีกครั้ง จากนั้นจะแยกออกเล็กน้อย แล้วจึงมี Composable อื่นๆ ที่เรียกใช้และแสดงรายการของเรา

  • หากดูวิธีส่งข้อมูล เราจะเริ่มจากโมเดลที่ด้านบน

  • เราจะแยกโครงสร้างออกเป็นสตริงแล้วจึงเป็นลิสต์ จากนั้นลิสต์นั้นจะถูกแปลงเป็นแลมบ์ดาของรายการสำหรับ Lazy List และ Lazy List จะประกอบรายการทั้งหมดที่ปรากฏบนหน้าจอ

  • มาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราแก้ไขรายชื่อติดต่อรายการสุดท้ายในรายการนี้

  • สิ่งที่เราต้องการให้เกิดขึ้นคือการทำให้รายการสุดท้ายในลิสต์ใช้ไม่ได้ แต่สิ่งนั้นจะไม่เกิดขึ้นที่นี่

  • หากเราดูที่การติดตามองค์ประกอบสำหรับการจัดองค์ประกอบใหม่นี้ จะมีลักษณะดังนี้ โดยเราจะเริ่มที่หน้าจอรายชื่อติดต่อ แล้วค่อยๆ ไล่ลงมาจนถึงรายการนี้

  • และนี่เป็นวิธีที่ค่อนข้างแพงในการลงมาที่นี่

  • นอกจากนี้ เรายังเห็นว่ามีส่วนต่างๆ เหล่านี้ที่ดูเหมือนว่าไม่มีอะไรเกิดขึ้นภายใน Trace

  • และแม้ว่าจะไม่ชัดเจนในที่นี้ แต่จริงๆ แล้วนี่คือจุดที่ Composable ตัดสินใจว่าจะข้ามหรือไม่

  • สำหรับตัวอย่างนี้ ระบบจะข้าม Composable เพียง 2 รายการเนื่องจากวิธีที่ Lazy List จัดองค์ประกอบใหม่

  • เมื่อคุณให้ Lambda เนื้อหาใหม่ ระบบจะล้างข้อมูลรายการทั้งหมดที่มี

  • ดังนั้นระบบจะทำให้รายชื่อติดต่อ 2 รายการที่ไม่มีการเปลี่ยนแปลงใช้งานไม่ได้ นอกเหนือจากรายการที่มีการเปลี่ยนแปลง และรายชื่อติดต่อ 2 รายการดังกล่าวเป็นเพียงรายการเดียวที่ข้ามได้

  • ส่วนอื่นๆ ทั้งหมดจะทำการเปรียบเทียบความเท่ากันแบบข้ามนี้เพื่อดูว่าต้องดำเนินการจริงหรือไม่

  • หากออบเจ็กต์มีความเสถียร การเปรียบเทียบนี้อาจมีค่าใช้จ่ายค่อนข้างสูง

  • หากคุณบังคับให้เราต้องตรวจสอบโครงสร้างข้อมูลทั้งหมดของคุณ การดำเนินการนี้อาจเพิ่มขึ้นและเป็นต้นทุนที่ไม่สำคัญเพียงเพื่อบอกว่าออบเจ็กต์ 2 รายการเท่ากันหรือไม่

  • และที่แย่กว่านั้นคือ เนื่องจากเราได้แยกโครงสร้างแล้ว เราจึงต้องทำการเปรียบเทียบแบบเดียวกันในรายการถึง 2 ครั้ง

  • หากคุณปล่อยให้คอมไพเลอร์ทำงานตามลักษณะการทำงานเริ่มต้นและใช้การข้ามแบบเข้มงวด คุณจะยังคงข้ามได้และยังคงตรวจสอบเพื่อข้าม Composable แบบมีเงื่อนไขได้ แต่จะใช้เส้นทางที่เร็วกว่ามากในการใช้ความเท่าเทียมกันของเอกลักษณ์ออบเจ็กต์แทนความเท่าเทียมกันเชิงโครงสร้าง

  • วิธีนี้ช่วยให้เราคำนวณได้เร็วขึ้นมาก และแม้ว่าเราจะจัดองค์ประกอบรายการเดียวกันใหม่ในเฟรมนี้ ซึ่งเป็นจุดสำคัญที่เราต้องเข้าไปอย่างรวดเร็ว แต่เราก็ไม่ต้องเสียเวลามากเกินไปในการพิจารณาว่าเราไม่สามารถข้าม Composable เหล่านี้ได้จริงๆ

  • นี่เป็นเหตุผลที่เราเน้นการใช้เครื่องมือต่างๆ เช่น การเปรียบเทียบและการติดตาม เพื่อทำความเข้าใจสิ่งที่เกิดขึ้นภายในแอปพลิเคชันของคุณ แทนที่จะนับการคอมโพสใหม่โดยตรง

  • แม้ว่าจะเป็นพร็อกซีที่มีประโยชน์ แต่ก็ไม่ใช่สิ่งที่คุณต้องให้ความสำคัญมากเกินไปหรือให้ความสำคัญมากเกินไปตลอดการเพิ่มประสิทธิภาพแอปพลิเคชัน

  • โปรดทราบว่าแทนที่จะพยายามข้าม Composable ให้หลีกเลี่ยงการทำให้ Composable ไม่ถูกต้องตั้งแต่แรก

การวิเคราะห์การดำเนินการด้วยร่องรอยเมธอด

  • เราเห็นในตัวอย่างนี้ว่าการติดตามองค์ประกอบสามารถให้ข้อมูลจำนวนมากเกี่ยวกับสิ่งที่เกิดขึ้น แต่ก็ไม่จำเป็นต้องแสดงภาพทั้งหมด

  • Andrei จะมาพูดถึงร่องรอยเมธอดให้เราฟัง

  • โอเค

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

  • และบางครั้งอาจเป็นสิ่งที่ Compose ทำ บางครั้งอาจเป็นสิ่งที่อยู่ในโค้ดของคุณ ซึ่งยากที่จะระบุได้ด้วยการดูที่การติดตามการคอมโพสิชันที่นี่

  • สิ่งที่คุณต้องทำตอนนี้คือรับร่องรอยเมธอด

  • มีบางอย่างในเครื่องมือสร้างโปรไฟล์ เช่น รายการในเครื่องมือสร้างโปรไฟล์ใน Android Studio หรือคุณจะใช้คำสั่ง ADB เพื่อดึงการติดตามก็ได้ และสิ่งที่คุณจะได้รับคือการเรียกใช้ฟังก์ชันทุกครั้งที่เกิดขึ้นในโลกของ Java

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

  • มีสิ่งหนึ่งที่ฉันอยากจะ-- ดูไอเทมนี้ตรงกลาง

  • สิ่งที่ฉันอยากจะบอกก็คือการติดตามเมธอดมีค่าใช้จ่ายสูงมาก

  • คุณจะมีโอเวอร์เฮดเพิ่มขึ้น 10-20 เท่า ขึ้นอยู่กับความซับซ้อนของแผนผังฟังก์ชัน เนื่องจากทุกการเรียกใช้ฟังก์ชันจะเพิ่มโอเวอร์เฮดเล็กน้อย

  • และเพื่อแสดงให้เห็นภาพ เราสามารถดูภาพหน้าจอได้

  • นี่คือลักษณะของร่องรอยเมธอดสำหรับการเริ่มต้นแอปโดยปกติ และคุณอาจถามว่า "ฉันจะทำความเข้าใจเรื่องนี้ได้อย่างไร"

  • โดยปกติแล้ว คุณจะเริ่มการติดตามองค์ประกอบโดยดูที่ฟังก์ชันที่มีอยู่ตรงนี้

  • ดังนั้นนี่จึงเหมือนกับร่องรอยการจัดองค์ประกอบที่เรียบง่าย ซึ่งมี ScreenContent, Button บางส่วน และฟังก์ชันอื่นๆ

  • สมมติว่าเราต้องการดูว่าเกิดอะไรขึ้นใน ScreenContent

  • Perfetto มีแถบค้นหา

  • คุณโหลดร่องรอยเมธอดในโปรแกรมดู Perfetto แล้วค้นหาฟังก์ชันเดียวกับที่มีที่นี่ ซึ่งก็คือ ScreenContent โดยจะไปยังสิ่งที่มีลักษณะคล้ายกัน ซึ่งก็คือร่องรอยเมธอดสำหรับหน้าจอ

  • คุณจะเห็นว่ามีComposer.startGroupและการโทรแบบกลุ่มอยู่

  • นี่คือสิ่งที่รันไทม์และคอมไพเลอร์ทำในเบื้องหลังเพื่อให้แน่ใจว่าองค์ประกอบของคุณทำงานได้อย่างมหัศจรรย์ แต่การเรียกที่น่าสนใจในที่นี้คือ Composer.changed

  • วิธีนี้ช่วยให้เราตรวจสอบได้ว่าพารามิเตอร์มีการเปลี่ยนแปลงระหว่างการเรียกใช้ก่อนหน้าและการเรียกใช้ Composable ปัจจุบันหรือไม่

  • และ Composer.changed ส่วนใหญ่ก็ถูกครอบงำโดย ScreenModel.equals นี้

  • ดังนั้น ScreenModel อาจเป็นคลาสข้อมูลขนาดใหญ่บางประเภท และเราอาจทำให้คลาสนี้เสถียรเพื่อช่วยในการนับการจัดองค์ประกอบใหม่ จึงอาจเป็นสิ่งที่ปรากฏในรายงานคอมไพเลอร์

  • และบางทีการนำคำอธิบายประกอบที่เสถียรนี้ออกอาจช่วยเพิ่มประสิทธิภาพในกรณีนี้ได้

  • ดังนั้นคุณจึงต้องมองหาปัญหานี้ และร่องรอยเมธอดของ Composition เป็นเครื่องมือที่ดีที่สุดในการค้นหา

  • โอเค

  • วันนี้เราได้ดูข้อมูลหลายอย่างแล้ว ตอนนี้เราจะให้ Andrew มาสรุปให้ฟัง

เครื่องมือออกแบบและประสิทธิภาพที่ประกอบได้รวดเร็ว

  • เพื่อสรุปและให้ภาพรวมระดับสูงของทุกสิ่งที่เราได้พูดถึงในวันนี้ มี 2-3 สิ่งที่ควรคำนึงถึงเมื่อคุณออกแบบ Composable และเพิ่มประสิทธิภาพแอปที่คุณมีอยู่แล้ว

  • ก่อนอื่น โปรดทราบว่า Composable ที่มีต้นทุนต่ำกว่าและ Composable ที่คุณไม่ได้ทำให้ไม่ถูกต้องนั้นดีกว่า Composable ที่คุณข้ามไป

  • โปรดทราบว่าเมื่อออกแบบ Composable คุณควรพิจารณาอาร์กิวเมนต์ที่มีขนาดเล็กกว่าและเปรียบเทียบได้ง่ายกว่า และอาจนำการเปรียบเทียบกับการเรียกใช้ฟังก์ชันอื่นๆ มาใช้ซ้ำเพื่อใช้ประโยชน์จากการเพิ่มประสิทธิภาพ

  • เมื่อใช้ API ภายใน Composable โปรดทราบว่าประสิทธิภาพหรือเวลาที่ใช้ในการแสดงผลเฟรมจะเป็นผลมาจากต้นทุนของ API เองและความถี่ที่คุณใช้ API เหล่านั้น

  • โปรดคำนึงถึงค่าใช้จ่ายเหล่านี้ที่เพิ่มขึ้น และโปรดทราบว่าอาจมีค่าใช้จ่ายในการแยกส่วนที่เกี่ยวข้องซึ่งคุณอาจต้องจ่ายในช่วงเวลาที่เหมาะสมที่สุดสำหรับแอปพลิเคชันของคุณ

  • นอกจากนี้ โปรดทราบว่าคุณควรเป็นพลเมืองที่ดีเมื่อทำการเพิ่มประสิทธิภาพภายในแอปของคุณเอง

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

  • นอกจากนี้ โปรดทราบว่าอาจมีการแลกเปลี่ยนระหว่างความสามารถในการบำรุงรักษาและประสิทธิภาพภายในแอปพลิเคชัน

  • ดังนั้น ให้ทำสิ่งที่ดีที่สุดสำหรับคุณและทีมเพื่อให้แน่ใจว่าคุณไม่ได้เพิ่มประสิทธิภาพมากเกินไปและทำให้ตัวเองตกที่นั่งลำบากในเรื่องความสามารถในการบำรุงรักษา

  • และหากต้องการความช่วยเหลือเพิ่มเติม คุณยังมีเครื่องมืออีกอย่างหนึ่งที่พร้อมใช้งาน ซึ่งก็คือ AI Agent ที่คุณมีอยู่แล้ว

  • เราได้เปิดตัวทักษะด้านประสิทธิภาพของ Jetpack Compose ซึ่งเป็นส่วนหนึ่งของ Android CLI และเป็นส่วนหนึ่งของทักษะที่เรานำเสนอในนั้น โดยมีข้อมูลมากมายที่เราได้พูดถึงในวันนี้ และสามารถตรวจสอบแอปพลิเคชันของคุณและให้คำแนะนำสำหรับการเพิ่มประสิทธิภาพประเภทนี้ได้

  • คุณติดตั้งใน Agent ที่มีอยู่ได้โดยใช้ Android CLI

    android skills add jetpack-compose-performance
  • ดูข้อมูลเพิ่มเติมเกี่ยวกับ Android CLI ได้ที่ developer.android.com

  • นอกจากนี้ คุณยังดูข้อมูลเพิ่มเติมเกี่ยวกับทักษะต่างๆ ได้ในที่เก็บ GitHub ของเรา

  • สุดท้ายนี้ ขอให้สนุกกับการเปรียบเทียบอย่างปลอดภัยและการแต่งเพลง

รหัส

ข้อมูลโค้ด

ข้อมูลโค้ด Kotlin ที่ 09:34 ใน "การจัดการผลข้างเคียง"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Asynchronous work and animations
    LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}
ข้อมูลโค้ด Kotlin ที่เวลา 10:15 ใน "การจัดการผลข้างเคียง"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val activity = LocalActivity.current

    // Setup and teardown
    DisposableEffect(Unit) {
        activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
        onDispose {
            activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
        }
    }
}
ข้อมูลโค้ด Kotlin ที่นาที 10:36 ใน "การจัดการผลข้างเคียง"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // One-shot events
    SideEffect(Unit) { mediaPlayer.play() }
}
ข้อมูลโค้ด Kotlin ที่นาที 11:12 ใน "การจัดการผลข้างเคียง"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Synchronize the outside world to the state of composition
    SideEffect { mediaPlayer.playbackRate = playbackRate }
}
ข้อมูลโค้ด Kotlin ที่นาที 12:12 ใน "การจัดการผลข้างเคียง"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) { viewModel.loadCart() }
    LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
    LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }

    CartContent(...)
}
ข้อมูลโค้ด Kotlin ที่นาที 12:36 ใน "การจัดการผลข้างเคียง"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) {
        launch { viewModel.loadCart() }
        launch { viewModel.loadPaymentMethods() }
        launch { viewModel.loadDeliveryAddresses() }
    }

    CartContent(...)
}
ข้อมูลโค้ด Kotlin ที่นาที 15:12 ใน "State optimization and invalidations"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton = scrollState.value > 0
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
ข้อมูลโค้ด Kotlin ที่นาที 15:51 ใน "การเพิ่มประสิทธิภาพสถานะและการทำให้เป็นโมฆะ"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
ข้อมูลโค้ด Kotlin ที่นาที 16:49 ใน "การเพิ่มประสิทธิภาพสถานะและการทำให้เป็นโมฆะ"
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

// First read
derivedState.value
computedState.value

// Second read
derivedState.value
computedState.value
ข้อมูลโค้ด Kotlin ที่นาที 18:35 ใน "การเพิ่มประสิทธิภาพสถานะและการทำให้เป็นโมฆะ"
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

...

// Read-after-write
a++
derivedState.value
computedState.value
ข้อมูลโค้ด Kotlin ที่นาที 21:07 ใน "การข้ามและความเสถียรใน Compose"
// What you write
@Composable fun ContactListItem(
    contact: Contact
) { ... }

// What you get
@Composable fun ContactListItem(
    contact: Contact,
    $composer: Composer?
    $changed: Int
) {
    val $dirty = $changed or
        $composer.changed(contact)

    if ($dirty) { /* Your function body */ }
    else $composer.skipToGroupEnd()
}
ข้อมูลโค้ด Kotlin ที่นาที 21:53 ใน "การข้ามและความเสถียรใน Compose"
data class ContactsModel(
    val header: String,
    val contacts: List<Contact>
)

data class Contact(...)

@Composable
fun ContactsScreen(model: ContactsModel) {
    Column {
        TopAppBar(title = { Text(model.header) })
        ContactsList(model.contacts)
    }
}

@Composable
fun ContactsList(contacts: List<Contact>) {
    LazyColumn {
        items(contacts) { ContactListItem(it) }
    }
}

@Composable
fun ContactListItem(contact: Contact) { ... }
Shell snippet at 29:40 in "Fast composable design and performance tools"
android skills add jetpack-compose-performance