การควบคุมการจัดองค์ประกอบใหม่
พารามิเตอร์ที่เสถียรและการจัดองค์ประกอบใหม่น้อยลง
การควบคุมการจัดองค์ประกอบใหม่ เป็นบทเรียน Android Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Android Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Android Academy มีบทเรียนทั้งหมด 4 บทเรียน
การจัดองค์ประกอบใหม่: ปุ่มควบคุมประสิทธิภาพของ Compose
ใน Jetpack Compose UI จะถูกอธิบายด้วยฟังก์ชัน เมื่อสถานะเปลี่ยน Compose จะทำการจัดองค์ประกอบใหม่: เรียกใช้คอมโพสซิเบิลที่อ่านสถานะนั้นอีกครั้งเพื่ออัปเดตหน้าจอ
การจัดองค์ประกอบใหม่เป็นเรื่องปกติและมีต้นทุนต่ำเมื่อกำหนดขอบเขตไว้อย่างเหมาะสม แต่เมื่อเกิดขึ้นบ่อยเกินไปหรือครอบคลุมทรีมากเกินไป ก็จะกลายเป็นสาเหตุอันดับ 1 ของอาการกระตุกใน Compose บทเรียนนี้จะสอนให้คุณทำให้การจัดองค์ประกอบใหม่มีขนาดเล็กและเกิดขึ้นไม่บ่อย
ดูจำนวนครั้งของการจัดองค์ประกอบใหม่
ก่อนแก้ไข ให้เริ่มจากการวัดผล เครื่องมือตรวจสอบเค้าโครงใน Android Studio จะแสดงจำนวนครั้งของการจัดองค์ประกอบใหม่ของคอมโพสซิเบิลแต่ละรายการแบบเรียลไทม์ คุณยังใช้เมตริกของคอมไพเลอร์ Compose หรือตัวนับสำหรับแก้ไขข้อผิดพลาดแบบง่ายได้
เคล็ดลับง่าย ๆ คือ SideEffect จะเพิ่มค่าที่อ้างอิงขึ้นหนึ่งทุกครั้งที่คอมโพสซิเบิลถูกจัดองค์ประกอบใหม่ คุณจึงบันทึกความผิดปกติระหว่างการพัฒนาได้
@Composable
fun RecompositionCounter(tag: String) {
val count = remember { mutableStateOf(0) }
SideEffect { count.value++ }
Log.d("Recompose", "$tag recomposed ${count.value} times")
}
// Drop RecompositionCounter("PriceLabel") inside a composable
// to watch how often it re-runs while you interact.อ่านสถานะให้ช้าที่สุดเท่าที่ทำได้
Compose จะจัดองค์ประกอบใหม่เฉพาะคอมโพสซิเบิลที่อ่านค่าของสถานะนั้น หากองค์ประกอบแม่อ่านสถานะ ทั้งองค์ประกอบแม่จะถูกจัดองค์ประกอบใหม่ แต่หากมีเพียงองค์ประกอบลูกขนาดเล็กที่อ่าน ก็จะจัดองค์ประกอบใหม่เฉพาะองค์ประกอบลูกนั้น
ดังนั้น ให้เลื่อนการอ่านสถานะลงไปในทรี ในเวอร์ชันที่ไม่ดี Column ทั้งหมดจะถูกจัดองค์ประกอบใหม่ทุกครั้งที่มีการเปลี่ยนค่า ส่วนเวอร์ชันที่ดีจะแยกการเปลี่ยนแปลงไว้เฉพาะป้ายกำกับ
// BAD: Column reads `seconds`, so everything recomposes each second
@Composable
fun TimerBad(seconds: Int) {
Column {
ExpensiveHeader()
Text("Elapsed: $seconds")
}
}
// GOOD: only the Text reads the value via a lambda
@Composable
fun TimerGood(seconds: () -> Int) {
Column {
ExpensiveHeader()
Text("Elapsed: ${seconds()}")
}
}เลื่อนการอ่านด้วยแลมบ์ดา
รูปแบบที่ทรงพลังคือ แทนที่จะส่งค่าที่เปลี่ยนบ่อย ให้ส่งแลมบ์ดาที่คืนค่าดังกล่าว คอมโพสซิเบิลที่เรียกแลมบ์ดาในท้ายที่สุดจะเป็นเพียงตัวเดียวที่ถูกจัดองค์ประกอบใหม่
นี่คือเหตุผลที่ Modifier.offset { ... } และ graphicsLayer { ... } รับแลมบ์ดา: ค่าการเลื่อน/แอนิเมชันเปลี่ยนทุกเฟรม และแลมบ์ดาจะกันไม่ให้เกิดการจัดองค์ประกอบใหม่ในระยะการจัดวางทั้งหมด
// Passing the value: parent recomposes every frame of scroll
Box(Modifier.offset(y = scrollOffset.dp))
// Passing a lambda: skips recomposition, updates in the layout phase
Box(Modifier.offset { IntOffset(x = 0, y = scrollOffset.roundToInt()) })
// Same idea for alpha/scale during animation:
Image(
painter = painter,
contentDescription = null,
modifier = Modifier.graphicsLayer { alpha = animatedAlpha() }
)ความเสถียร: เหตุใด Compose จึงข้ามได้
Compose สามารถข้ามการจัดองค์ประกอบใหม่ของคอมโพสซิเบิลได้ หากพารามิเตอร์ทั้งหมดมีความเสถียรและไม่เปลี่ยนแปลง ชนิดข้อมูลจะมีความเสถียรเมื่อ Compose เชื่อถือได้ว่า equals สะท้อนการเปลี่ยนแปลงจริง และฟิลด์สาธารณะจะไม่เปลี่ยนแปลงโดยที่ไม่รู้ตัว
- มีความเสถียร: ชนิดข้อมูลพื้นฐาน
Stringคลาสข้อมูลที่เปลี่ยนแปลงไม่ได้ และState - ไม่เสถียร: อินเทอร์เฟซ
List/Mapคลาสที่มีฟิลด์varและชนิดข้อมูลจากโมดูลที่ไม่มีคอมไพเลอร์
พารามิเตอร์ที่ไม่เสถียรจะบังคับให้เกิดการจัดองค์ประกอบใหม่แม้ไม่มีสิ่งใดเปลี่ยนแปลง
// UNSTABLE: List is an interface; Compose cannot assume immutability,
// so UserList recomposes even if the contents are identical.
@Composable
fun UserList(users: List<User>) { /* ... */ }
data class User(val id: Long, val name: String) // stable: all valsทำให้พารามิเตอร์มีความเสถียร
วิธีแก้ไขทั่วไปสองอย่างสำหรับพารามิเตอร์ที่ไม่เสถียร:
- ใช้คอลเลกชันที่เปลี่ยนแปลงไม่ได้จาก
kotlinx.collections.immutable(เช่นImmutableList) ซึ่ง Compose จะถือว่ามีความเสถียร - ใส่คำกำกับให้คลาสที่คุณควบคุมด้วย
@Immutableหรือ@Stableเพื่อรับรองกับ Compose ว่าคลาสจะไม่เปลี่ยนแปลง
ตอนนี้ Compose จึงสามารถข้ามการจัดองค์ประกอบใหม่ได้อย่างปลอดภัยเมื่อส่งอินสแตนซ์เดิมอีกครั้ง
import kotlinx.collections.immutable.ImmutableList
import androidx.compose.runtime.Immutable
@Immutable
data class UiState(
val title: String,
val users: ImmutableList<User>
)
// Stable parameter -> Compose can skip this when state is unchanged
@Composable
fun UserList(users: ImmutableList<User>) { /* ... */ }remember: อย่าคำนวณใหม่ทุกเฟรม
คอมโพสซิเบิลอาจทำงานหลายครั้ง การคำนวณใด ๆ ที่ไม่ใช่เรื่องง่ายซึ่งทำโดยตรงในเนื้อหาจะทำงานในทุกครั้งที่จัดองค์ประกอบใหม่ ให้ครอบการคำนวณด้วย remember เพื่อให้คำนวณใหม่เฉพาะเมื่อคีย์เปลี่ยน
สำหรับค่าที่ได้จากสถานะอื่น ควรใช้ derivedStateOf ซึ่งจะส่งค่าใหม่เฉพาะเมื่อผลลัพธ์ที่คำนวณได้เปลี่ยนจริง
// Recomputes the sorted list only when `items` changes
val sorted = remember(items) { items.sortedBy { it.name } }
// derivedStateOf: only triggers readers when the BOOLEAN flips,
// not on every scroll pixel
val showButton by remember {
derivedStateOf { listState.firstVisibleItemIndex > 5 }
}
if (showButton) ScrollToTopButton()คีย์ที่เสถียรในรายการแบบ Lazy
ใน LazyColumn/LazyRow ให้กำหนด คีย์ ที่เสถียรให้แต่ละรายการ หากไม่มีคีย์ การแทรกหรือเรียงลำดับรายการใหม่จะบังคับให้ Compose จัดองค์ประกอบใหม่และวัดรายการที่ไม่ได้เปลี่ยนจริงซ้ำ เพราะ Compose ติดตามรายการตามตำแหน่ง
เมื่อมีคีย์ที่เสถียร Compose จะจับคู่รายการระหว่างการอัปเดตและข้ามรายการที่ไม่เปลี่ยนแปลงได้
LazyColumn {
items(
items = users,
key = { user -> user.id } // stable identity
) { user ->
UserRow(user)
}
}
// Now adding a user at the top reuses existing rows
// instead of recomposing the whole list.ยกสถานะขึ้น ส่งเหตุการณ์ขึ้นด้านบน
พารามิเตอร์แลมบ์ดาที่ไม่เสถียรอาจทำให้การข้ามใช้ไม่ได้เช่นกัน หากมีการสร้างอินสแตนซ์แลมบ์ดาใหม่ทุกครั้งที่จัดองค์ประกอบใหม่ ทำให้การเรียกกลับมีความเสถียรด้วยการจดจำไว้หรืออ้างอิงฟังก์ชันที่เสถียร
เมื่อทำร่วมกับการยกสถานะขึ้น (สถานะอยู่ในผู้เรียก เหตุการณ์ไหลขึ้นด้านบน) วิธีนี้จะทำให้คอมโพสซิเบิลปลายทางมีต้นทุนต่ำและสามารถถูกข้ามได้
@Composable
fun SearchBar(query: String, onQueryChange: (String) -> Unit) {
TextField(value = query, onValueChange = onQueryChange)
}
// In the caller, a remembered lambda keeps the reference stable:
val onChange = remember { { newValue: String -> viewModel.setQuery(newValue) } }
SearchBar(query = query, onQueryChange = onChange)หลีกเลี่ยงการอ่านสถานะการเลื่อน/แอนิเมชันในระดับสูงเกินไป
ข้อผิดพลาดที่พบบ่อยคือการอ่านสถานะที่เปลี่ยนเร็ว (ออฟเซ็ตการเลื่อน ความคืบหน้าของแอนิเมชัน) ในคอมโพสซิเบิลระดับสูง ซึ่งบังคับให้ทรีย่อยทั้งหมดถูกจัดองค์ประกอบใหม่ทุกเฟรม
ให้การอ่านดังกล่าวอยู่ภายในแลมบ์ดาของ Modifier (offset {}, graphicsLayer {}, drawBehind {}) เพื่อให้งานเกิดขึ้นในระยะการจัดวางหรือการวาด ไม่ใช่การจัดองค์ประกอบใหม่ นี่คือความแตกต่างระหว่างการเลื่อนที่ลื่นไหล 60fps กับการเลื่อนที่สะดุด
// Animated color used only for drawing -> stay in the draw phase
Box(
Modifier.drawBehind {
drawRect(color = animatedColor()) // lambda read, no recomposition
}
)รายการตรวจสอบการจัดองค์ประกอบใหม่
เมื่อหน้าจอรู้สึกกระตุกระหว่างการโต้ตอบ ให้ตรวจสอบรายการนี้:
- พารามิเตอร์มีความเสถียรหรือไม่ (ข้อมูลที่เปลี่ยนแปลงไม่ได้,
ImmutableList) - คุณอ่านสถานะที่เปลี่ยนเร็วในระดับต่ำที่สุดเท่าที่ทำได้หรือไม่ โดยเหมาะที่สุดคืออ่านในแลมบ์ดาของตัวปรับแต่ง
- รายการแบบ Lazy มีคีย์ที่เสถียรหรือไม่
- การคำนวณที่มีต้นทุนสูงอยู่หลัง
remember/derivedStateOfหรือไม่ - เครื่องมือตรวจสอบเค้าโครงยืนยันหรือไม่ว่าจำนวนครั้งลดลงจริง
ทุกช่องที่คุณทำเครื่องหมายจะช่วยกำจัดงานที่สูญเปล่าจากทุกเฟรม
ตรวจสอบอย่างรวดเร็ว
คุณส่ง List<User> ให้คอมโพสซิเบิลและสังเกตว่ามันถูกจัดองค์ประกอบใหม่แม้ข้อมูลจะไม่เปลี่ยนแปลง วิธีแก้โดยตรงที่สุดคืออะไร
สรุป: การจัดองค์ประกอบใหม่ที่เล็กลงและเกิดน้อยลง
คุณได้เรียนรู้วิธีควบคุมการจัดองค์ประกอบใหม่ ซึ่งเป็นปัญหาด้านประสิทธิภาพอันดับต้น ๆ ของ Compose:
- วัดจำนวนครั้งด้วยเครื่องมือตรวจสอบเค้าโครงหรือตัวนับสำหรับแก้ไขข้อผิดพลาด
- อ่านสถานะในระดับต่ำที่สุดเท่าที่ทำได้ และเลื่อนการอ่านด้วยแลมบ์ดา
- ทำให้พารามิเตอร์มีความเสถียรด้วยข้อมูลที่เปลี่ยนแปลงไม่ได้และ
@Immutable/ImmutableList - ใช้
rememberและderivedStateOfเพื่อหลีกเลี่ยงการคำนวณใหม่ทุกเฟรม - กำหนดคีย์ที่เสถียรให้รายการแบบ Lazy และให้การอ่านสถานะที่เปลี่ยนเร็วอยู่ในแลมบ์ดาของตัวปรับแต่ง
ต่อไป เราจะเปลี่ยนจาก CPU/การจัดองค์ประกอบใหม่ไปสู่หน่วยความจำ เพื่อค้นหาและแก้ไขการรั่วไหล
คำถามที่พบบ่อย
บทเรียน “การควบคุมการจัดองค์ประกอบใหม่” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การควบคุมการจัดองค์ประกอบใหม่” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Android Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Android Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การควบคุมการจัดองค์ประกอบใหม่”
พารามิเตอร์ที่เสถียรและการจัดองค์ประกอบใหม่น้อยลง คุณปฏิบัติ Android Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Android Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Android Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 2 จากทั้งหมด 4 บทเรียน
บทเรียน “การควบคุมการจัดองค์ประกอบใหม่” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Android Academy นี้ได้ไหม
ได้ บทเรียน Android Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การวัดประสิทธิภาพ
- การควบคุมการจัดองค์ประกอบใหม่
- หน่วยความจำรั่วและวิธีแก้ไข
- โปรไฟล์การเริ่มต้นและโปรไฟล์พื้นฐาน