วิศวกรซอฟต์แวร์
Jetpack Compose ช่วยให้ออกแบบอินเทอร์เฟซผู้ใช้คุณภาพสูงได้ง่าย แต่การติดตามปัญหาที่ส่งผลต่อประสิทธิภาพของ UI อาจเป็นเรื่องยาก ปัญหาประเภทนี้อาจทำให้เกิดปัญหาที่ผู้ใช้สังเกตเห็นได้ เช่น เวลาในการตอบสนองเมื่อเริ่มต้นทำงาน และเฟรมที่หายไปซึ่งปรากฏเป็นภาพกระตุกเมื่อเลื่อนหรือภาพเคลื่อนไหวที่กระตุก
ในการพูดคุยครั้งนี้ เราจะแนะนำเครื่องมือเพื่อค้นหาสิ่งที่ส่งผลต่อประสิทธิภาพของ UI โดยมุ่งเน้นที่ Perfetto และการติดตาม (Re-)Composition จากนั้นเราจะสำรวจรูปแบบที่ควรหลีกเลี่ยงซึ่งคุณอาจใช้อยู่และทำให้คุณเสียค่าใช้จ่ายโดยไม่รู้ตัว วิธีค้นหารูปแบบดังกล่าวในโค้ด และรูปแบบที่เหมาะสมซึ่งคุณสามารถใช้แทนได้ เมื่อจบการพูดคุยนี้ คุณจะเข้าใจวิธีคิดใน Compose ด้วยแนวคิดด้านประสิทธิภาพ และวิธีทำโปรไฟล์ วิเคราะห์ และยืนยันการเพิ่มประสิทธิภาพในแอป
ข้อความถอดเสียง
สวัสดี
สวัสดีทุกคน
หวังว่าคุณจะได้พักผ่อนอย่างเต็มที่
หวังว่าคุณจะดื่มกาแฟ ทาครีมบำรุง และทำอะไรต่างๆ เรียบร้อยแล้ว
วันนี้เราจะมาเจาะลึกกันมากขึ้น
ใช่แล้ว เริ่มเลย
วันนี้เราจะมาเจาะลึกกันสักหน่อย
ผมชื่อ Andrei Shikov และวันนี้ผมจะมาแชร์เวทีกับ Andrew Bailey
เราทั้งคู่ทำงานในทีมรันไทม์คอมไพเลอร์ Compose และยังทำสิ่งอื่นๆ อีกมากมาย รวมถึงประสิทธิภาพด้วย
และหัวข้อของการพูดคุยในวันนี้คือการตอบคำถามของคุณ คำถามเกี่ยวกับประสิทธิภาพ เนื่องจาก Jetpack Compose ได้รับการกำหนดให้เป็นชุดเครื่องมือ UI เริ่มต้นสำหรับการพัฒนา Android
และตอนนี้หลายคนก็ถามว่าเราจะทำให้ Compose UI มีประสิทธิภาพมากที่สุดได้อย่างไร
วันนี้เราจะมาเจาะลึกข้อมูลอย่างละเอียดตามที่ได้บอกไปแล้ว
นอกจากนี้ เรายังจะแนะนำเครื่องมือบางอย่างเพื่อให้คุณทำงานได้ดีขึ้นใน Compose ด้วย
สิ่งแรกที่ผมอยากจะเริ่มพูดถึงคือเหตุผลที่ประสิทธิภาพมีความสำคัญต่อเรา
เมื่อนึกถึงคุณภาพและการสร้างแอปที่มีคุณภาพ คุณมักจะนึกถึงภาพเคลื่อนไหว เรานึกถึงชุดฟีเจอร์ที่หลากหลาย และบางครั้งประสิทธิภาพก็อาจลดลง แต่การรับรู้ของผู้ใช้ขึ้นอยู่กับประสิทธิภาพของแอปจริงๆ
ดังนั้นหากภาพเคลื่อนไหวติดขัด การรับรู้จะลดลงทันที
ดังนั้นเมตริกและตัวเลขทั้งหมดที่เราจะพูดถึงในวันนี้จึงเป็นเมตริกพร็อกซีที่เราเลือกเพื่อแสดงความรู้สึกของผู้ใช้ที่มีต่อแอป ความรู้สึกที่มีต่อภาพเคลื่อนไหวทั้งหมด และสิ่งต่างๆ ทั้งหมดที่เกิดขึ้นบนหน้าจอ
ดังนั้น โดยพื้นฐานแล้ว การเพิ่มประสิทธิภาพแอปคือการเพิ่มประสิทธิภาพเพื่อประสบการณ์การใช้งานที่ดีขึ้น
นี่เป็นเพียงแนวคิดที่เราอยากให้คุณมีในขณะที่เราเจาะลึกหัวข้อต่างๆ มากมายในวันนี้
เมื่อคุณเริ่มดูประสิทธิภาพของแอป เราอยากให้คุณทำ 3 สิ่งนี้อย่างรวดเร็ว
สิ่งแรกคือ หากคุณวัดประสิทธิภาพ คุณจะวัดในโหมดเผยแพร่ ดังนั้นเราจึงไม่มีข้อมูลที่แก้ไขข้อบกพร่องได้จำนวนมาก และทั้งหมดนี้ทำให้แอปของเราช้าลง
อย่างที่ 2 คือเปิดใช้ R8 หรือเครื่องมือลดขนาดอื่นๆ
ในปีนี้ เราได้เปิดตัวการเพิ่มประสิทธิภาพที่ช่วยปรับปรุงประสิทธิภาพของโครูทีนได้ถึง 2 เท่า และเรายังทำสิ่งอื่นๆ อีกมากมาย
เราใช้ R8 เป็นอย่างมากเมื่อเพิ่มประสิทธิภาพ Compose
และสิ่งสุดท้าย โดยเฉพาะอย่างยิ่งสำหรับเมตริกการเริ่มต้นระบบ คุณต้องตรวจสอบว่าคุณใช้โปรไฟล์พื้นฐาน
ดังนั้นคลาสที่คุณโหลดในการเริ่มต้นระบบจะได้รับการคอมไพล์และโหลดไว้ล่วงหน้าเมื่อแอปนั้นเริ่มทำงาน
ดังที่คุณเห็นว่านี่แตกต่างจากบิลด์ของแอปที่แก้ไขข้อบกพร่องได้ตามปกติถึง 6 เท่า
และเมื่อคุณวัดผลสิ่งต่างๆ เหล่านั้น สิ่งเหล่านี้จะรวมเข้ากับชัยชนะอื่นๆ ที่คุณทำภายในโค้ด
ตอนนี้คุณได้ทำตามขั้นตอนทั้งหมดแล้ว แต่แอปยังคงกระตุกเล็กน้อย หรือยังมีปัญหาอื่นๆ เกี่ยวกับประสิทธิภาพอยู่ และนี่คือจุดที่คุณควรเข้าสู่ลูปประสิทธิภาพนี้
คุณต้องการเริ่มดูเมตริกบางรายการในสถานการณ์ที่เฉพาะเจาะจง
ตัวอย่างเช่น สมมติว่าแอปใช้เวลานานเกินไปในการโหลด คุณอาจต้องการรีเซ็ตแอป เริ่มต้นแอปจาก Launcher ตามปกติ และตรวจสอบระยะเวลาที่แอปใช้ในการโหลดหน้าจอข้อมูลแรก
หรือแอปอาจกระตุกเล็กน้อย คุณจึงเลื่อนดูและตรวจสอบว่าการเลื่อนนี้ทำซ้ำได้และเสถียร
ดังนั้นสภาพแวดล้อมที่เสถียรในสไลด์นี้จึงมีความสำคัญมาก เพราะเมื่อทำการเพิ่มประสิทธิภาพในภายหลัง คุณจะต้องตรวจสอบว่าเห็นการปรับปรุงเมตริก และยืนยันได้ว่าการเพิ่มประสิทธิภาพเหล่านั้นทำงานได้ตามที่ต้องการจริงๆ
การเปรียบเทียบมีประโยชน์มากในกรณีนี้ เนื่องจากสามารถบันทึกข้อมูลจำนวนมากได้อยู่แล้ว โดยจะทำการเปรียบเทียบซ้ำหลายครั้งเพื่อให้แน่ใจว่าไม่มีสัญญาณรบกวนในอุปกรณ์มากนัก และยังช่วยตั้งค่าอุปกรณ์หลายอย่างด้วย
นอกจากนี้ การเปรียบเทียบยังเป็นสถานการณ์ที่ทำซ้ำได้โดยค่าเริ่มต้น และนี่คือสิ่งที่เราพยายามทำที่นี่
แต่คุณก็สามารถใช้สคริปต์จำนวนมากได้เช่นกันหากต้องการ
และสุดท้าย คุณจะสร้างโปรไฟล์ ปรับปรุงบางอย่างในแอป ดูข้อมูล แล้วทำซ้ำอีกครั้ง และอาจทำซ้ำอีกครั้งด้วยเมตริกสถานการณ์ต่างๆ ที่แอปของคุณประสบปัญหา
เมื่อดูข้อมูล เครื่องมือแรกที่เราจะแนะนำในวันนี้และคุณควรทำความคุ้นเคยเป็นอย่างดีคือ 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 จะแนะนำ ซึ่งก็คือการติดตามองค์ประกอบ
โอเค
ดังนั้น การติดตามการจัดองค์ประกอบภาพ หากคุณดูการสร้างเฟรม 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 อย่างอยู่
โดยทั่วไป หากไม่แน่ใจว่าควรเลือกสถานะใด สถานะที่คำนวณแล้วจะเป็นค่าเริ่มต้นที่ดี
สถานะที่ได้มาจะมีประโยชน์มากที่สุดเมื่อคุณใช้ประโยชน์จากการแคชจริงๆ ซึ่งหมายความว่าคุณทำสิ่งที่ไม่ใช่เรื่องเล็กน้อย เช่น อัลกอริทึมที่ซับซ้อน การจัดสรรขนาดใหญ่ การอ่านสถานะจำนวนมากภายในซึ่งได้รับประโยชน์จากการแคชที่นี่จริงๆ หรือสิ่งที่ไม่ใช่เรื่องเล็กน้อยหรือขึ้นอยู่กับสิ่งต่างๆ เช่น ความเท่าเทียมกันของออบเจ็กต์และความเท่าเทียมกันของเอกลักษณ์ของออบเจ็กต์ ซึ่งคุณสร้างความแตกต่างและได้รับประโยชน์จากการแคชที่สถานะที่ได้มามีให้
อีกประเด็นหนึ่งที่ผมอยากพูดถึงคือทั้งสถานะที่ได้มาและสถานะที่คำนวณต่างก็พยายามหลีกเลี่ยงการทำให้ 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 ของเรา
สุดท้ายนี้ ขอให้สนุกกับการเปรียบเทียบอย่างปลอดภัยและการแต่งเพลง
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
val mediaPlayer = rememberMediaPlayer()
// Asynchronous work and animations
LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}@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
}
}
}@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
val mediaPlayer = rememberMediaPlayer()
// One-shot events
SideEffect(Unit) { mediaPlayer.play() }
}@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
val mediaPlayer = rememberMediaPlayer()
// Synchronize the outside world to the state of composition
SideEffect { mediaPlayer.playbackRate = playbackRate }
}@Composable
fun ShoppingCartScreen(
viewModel: CartViewModel
) {
LaunchedEffect(Unit) { viewModel.loadCart() }
LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }
CartContent(...)
}@Composable
fun ShoppingCartScreen(
viewModel: CartViewModel
) {
LaunchedEffect(Unit) {
launch { viewModel.loadCart() }
launch { viewModel.loadPaymentMethods() }
launch { viewModel.loadDeliveryAddresses() }
}
CartContent(...)
}@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")
}
}
}@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")
}
}
}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.valuevar a by mutableStateOf(0)
val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }
...
// Read-after-write
a++
derivedState.value
computedState.value// 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()
}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) { ... }android skills add jetpack-compose-performance