หน่วยความจำรั่วและวิธีแก้ไข
ค้นหาและหยุดการรั่วไหล
หน่วยความจำรั่วและวิธีแก้ไข เป็นบทเรียน Android Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Android Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Android Academy มีบทเรียนทั้งหมด 4 บทเรียน
การรั่วไหลของหน่วยความจำคืออะไร
การรั่วไหลของหน่วยความจำเกิดขึ้นเมื่อออบเจ็กต์ที่ไม่จำเป็นแล้วไม่สามารถถูกเก็บขยะได้ เพราะยังมีบางสิ่งเก็บการอ้างอิงไปยังออบเจ็กต์เหล่านั้นอยู่ บน Android ผู้ที่มักตกเป็นเหยื่อคือ Activity หรือ Context ที่ยังคงอยู่หลังจากหน้าจอถูกทำลาย
การรั่วไหลจะทำให้ฮีปเพิ่มขึ้นเรื่อย ๆ ทำให้ต้องเก็บขยะบ่อยขึ้น (ซึ่งหยุด UI ชั่วคราวและทำให้เกิดอาการกระตุก) และท้ายที่สุดทำให้แอปหยุดทำงานด้วย OutOfMemoryError บทเรียนนี้จะแสดงวิธีค้นหาและแก้ไขการรั่วไหล
การเก็บขยะตัดสินใจเก็บสิ่งใดไว้อย่างไร
ตัวเก็บขยะของ Android จะเก็บออบเจ็กต์ทุกตัวที่เข้าถึงได้จากรากของ GC (เธรดที่ยังทำงาน ฟิลด์สแตติก และอื่น ๆ) ส่วนออบเจ็กต์ที่เข้าถึงไม่ได้จะถูกคืนหน่วยความจำ
การรั่วไหลเป็นเพียงเส้นทางการเข้าถึงที่ไม่พึงประสงค์: ออบเจ็กต์ที่มีอายุยาวนานเก็บการอ้างอิงไปยังออบเจ็กต์ที่มีอายุสั้น ตัวอย่าง Kotlin ล้วนด้านล่างแสดงให้เห็นว่าการเข้าถึงได้ทำให้ออบเจ็กต์ยังคงอยู่ในหน่วยความจำ
object GlobalCache {
val items = mutableListOf<ByteArray>()
}
fun cacheSomething() {
// This 1MB array stays alive forever because GlobalCache
// (a static singleton, a GC root) keeps referencing it.
GlobalCache.items.add(ByteArray(1_000_000))
}
fun main() {
repeat(3) { cacheSomething() }
println("Held arrays: ${GlobalCache.items.size}") // never freed
}กรณีคลาสสิก: Activity รั่วไหล
การรั่วไหลของหน่วยความจำที่พบบ่อยที่สุดใน Android คือ ออบเจ็กต์แบบ static หรือออบเจ็กต์ที่มีอายุการใช้งานยาวนานเก็บ Context ไว้ เมื่อ Activity ถูกทำลาย (เช่น การหมุนหน้าจอหรือการนำทาง) ระบบต้องการคืนหน่วยความจำ แต่การอ้างอิงแบบ static ยังคงทำให้ออบเจ็กต์นั้นอยู่ต่อไปไม่สิ้นสุด
โค้ดด้านล่างทำให้เกิดการรั่วไหล เนื่องจากซิงเกิลตันแคช context ของ Activity ไว้
// LEAK: static field holds an Activity Context
object Analytics {
var context: Context? = null // <- keeps Activity alive
fun init(ctx: Context) { context = ctx }
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.init(this) // passing the Activity -> leak on rotation
}
}วิธีแก้: ใช้ Context ของแอปพลิเคชัน
เมื่อจำเป็นต้องเก็บ Context ไว้ในสิ่งที่มีอายุการใช้งานยาวนาน ให้เก็บ บริบทของแอปพลิเคชัน ซึ่งจะมีชีวิตอยู่ตลอดกระบวนการทำงานทั้งหมดและปลอดภัยต่อการเก็บไว้
เก็บบริบทของ Activity/View ไว้เฉพาะช่วงเวลาที่หน้าจอนั้นยังมีอยู่เท่านั้น กฎข้อนี้เพียงข้อเดียวช่วยป้องกันการรั่วไหลของหน่วยความจำใน Android ได้เกือบทั้งหมด
object Analytics {
private var appContext: Context? = null
fun init(ctx: Context) {
// applicationContext is process-scoped and safe to keep
appContext = ctx.applicationContext
}
}
// Call site is now leak-free:
Analytics.init(this) // stores applicationContext, not the Activityคลาสภายในและตัวจัดการ
คลาสภายในที่ไม่ใช่ static (รวมถึงตัวรับฟังส่วนใหญ่ Runnable และการเรียกกลับของ Handler) จะเก็บการอ้างอิงแบบ โดยนัยไปยังคลาสภายนอกของตน หากงานนั้นมีอายุยืนยาวกว่าหน้าจอ หน้าจอก็จะรั่วไหล
Handler.postDelayed ที่หน่วงเวลาไว้เป็นสาเหตุที่พบบ่อย ข้อความที่รอดำเนินการจะทำให้ Activity ยังคงอยู่จนกว่าจะถูกเรียกใช้งาน
// LEAK: the posted Runnable holds the Activity for 60 seconds
handler.postDelayed({ updateUi() }, 60_000)
// FIX: cancel pending work when the screen goes away
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}โครูทีน: กำหนดขอบเขตให้งาน
โครูทีนที่มีอายุยืนยาวกว่าหน้าจอจะทำให้ทุกสิ่งที่โครูทีนเก็บการอ้างอิงไว้รั่วไหล วิธีแก้คือ การทำงานพร้อมกันแบบมีโครงสร้าง โดยเริ่มงานในขอบเขตที่ผูกกับวงจรชีวิต เพื่อให้งานถูกยกเลิกโดยอัตโนมัติ
ใน ViewModel ให้ใช้ viewModelScope ส่วนในตัวควบคุมส่วนติดต่อผู้ใช้ให้ใช้ lifecycleScope เมื่อเจ้าของถูกทำลาย ขอบเขตจะยกเลิกงานและปล่อยการอ้างอิง
class FeedViewModel : ViewModel() {
fun load() {
// Cancelled automatically when the ViewModel is cleared
viewModelScope.launch {
val feed = repository.fetchFeed()
_state.value = feed
}
}
}
// In Compose, collect tied to the lifecycle:
val state by viewModel.state.collectAsStateWithLifecycle()DisposableEffect ใน Compose
ใน Compose สิ่งใดก็ตามที่คุณลงทะเบียนไว้ ต้องยกเลิกการลงทะเบียนเมื่อคอมโพสสิเบิลออกจากหน้าจอ DisposableEffect มีบล็อก onDispose สำหรับกรณีนี้โดยเฉพาะ เช่น ตัวรับฟัง ตัวสังเกตการณ์ เซนเซอร์ และตัวรับการออกอากาศ
หากลืมล้างข้อมูลในส่วนนี้ การเรียกกลับและทุกสิ่งที่การเรียกกลับเก็บการอ้างอิงไว้จะรั่วไหล
@Composable
fun LocationDisplay(manager: LocationManager) {
DisposableEffect(manager) {
val listener = LocationListener { /* update */ }
manager.register(listener)
onDispose { manager.unregister(listener) } // prevents the leak
}
}ค้นหาการรั่วไหลด้วย LeakCanary
LeakCanary เป็นเครื่องมือมาตรฐานสำหรับตรวจจับการรั่วไหลโดยอัตโนมัติในรุ่นแก้ไขข้อบกพร่อง เครื่องมือนี้จะตรวจดูออบเจ็กต์ที่ถูกทำลาย หากออบเจ็กต์ใดยังไม่ถูกเก็บคืนโดยตัวเก็บขยะ เครื่องมือจะถ่ายข้อมูลฮีปและแสดง สายโซ่การอ้างอิง ที่แน่นอนซึ่งทำให้ออบเจ็กต์นั้นยังคงอยู่
สายโซ่นั้นจะบอกแนวทางแก้ไข เพราะชี้ตรงไปยังเขตข้อมูลหรือตัวรับฟังที่เป็นต้นเหตุ
// build.gradle.kts (app module)
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
// No code needed: on a debug build, navigate away from a screen and
// LeakCanary posts a notification with the leak trace, e.g.:
// Analytics.context -> MainActivity (leaked)ข้อมูลฮีปในเครื่องมือวิเคราะห์ประสิทธิภาพ
สำหรับการรั่วไหลที่ LeakCanary ตรวจไม่พบ (การเพิ่มขึ้นอย่างช้า ๆ การรั่วไหลในโค้ดเนทีฟ หรือแคชขนาดใหญ่) ให้ใช้ เครื่องมือวิเคราะห์หน่วยความจำ ถ่าย ข้อมูลฮีป บังคับให้ GC ทำงาน แล้วตรวจสอบว่าคลาสใดมีอินสแตนซ์มากผิดปกติ
หากจำนวนอินสแตนซ์ของ Activity, Fragment หรือ ViewModel ยังคงเพิ่มขึ้นหลังจากนำทางออกจากหน้านั้น นี่คือหลักฐานสำคัญของการรั่วไหล เครื่องมือวิเคราะห์ยังแสดงเส้นทางไปยังรากของ GC เพื่อให้คุณติดตามการอ้างอิงได้
บิตแมปและการจัดสรรหน่วยความจำขนาดใหญ่
ปัญหาหน่วยความจำไม่ได้เกิดจากการรั่วไหลของการอ้างอิงทุกกรณี บิตแมป ขนาดใหญ่และแคชที่ไม่มีขีดจำกัดอาจทำให้ฮีปเต็มได้ด้วยตัวเอง ภาพถ่ายที่มีความละเอียดเต็มอาจใช้หน่วยความจำหลายสิบเมกะไบต์
ให้ไลบรารีรูปภาพ (Coil/Glide) ลดขนาดภาพลงให้เท่ากับขนาดที่แสดง และกำหนดขนาดสูงสุดของแคชที่คุณสร้างอย่างชัดเจน ตัวอย่างโค้ดแสดงแคช LRU ที่จำกัดขนาด
// Bound memory: keep at most ~1/8 of available app memory
val maxKb = (Runtime.getRuntime().maxMemory() / 1024 / 8).toInt()
val bitmapCache = object : LruCache<String, Bitmap>(maxKb) {
override fun sizeOf(key: String, value: Bitmap) = value.byteCount / 1024
}
// An unbounded HashMap<String, Bitmap> would grow until OutOfMemoryError.รายการตรวจสอบเพื่อป้องกันการรั่วไหล
สร้างนิสัยเหล่านี้ แล้วการรั่วไหลส่วนใหญ่จะไม่เกิดขึ้น:
- เก็บ applicationContext ไว้ในออบเจ็กต์ที่มีอายุการใช้งานยาวนาน ห้ามเก็บ Activity/View
- ทำงานแบบอะซิงโครนัสในโครูทีนที่มีขอบเขตตามวงจรชีวิต (
viewModelScope,lifecycleScope) - ยกเลิกการลงทะเบียนตัวรับฟัง/ตัวรับการออกอากาศเสมอ (
onDispose,onDestroy) - ยกเลิก Handler ที่หน่วงเวลา และตัวจับเวลา
- กำหนดขนาดสูงสุดของแคชและลดขนาดบิตแมป
- เก็บ LeakCanary ไว้ในรุ่นแก้ไขข้อบกพร่อง และดำเนินการตามร่องรอยที่เครื่องมือรายงาน
ตรวจสอบอย่างรวดเร็ว
คุณต้องการให้ออบเจ็กต์ซิงเกิลตันเก็บ Context ไว้ตลอดอายุการใช้งานของมัน ควรเก็บบริบทใดเพื่อหลีกเลี่ยงการทำให้หน้าจอรั่วไหล
สรุป: ดูแลหน่วยความจำให้สะอาด
คุณได้เรียนรู้ว่าการรั่วไหลคืออะไรและจะหยุดการรั่วไหลได้อย่างไร:
- การรั่วไหลคือเส้นทาง การเข้าถึงได้ ที่ไม่ต้องการ ซึ่งทำให้ออบเจ็กต์ที่ควรถูกทำลายยังคงอยู่
- การรั่วไหลแบบคลาสสิกคือการอ้างอิงแบบ static/อายุยืนยาวไปยัง Activity/Context — ให้เก็บ applicationContext แทน
- ตัวรับฟังในคลาสภายใน Handler ที่หน่วงเวลา และ โครูทีน ที่ไม่มีขอบเขต จะทำให้เจ้าของรั่วไหล — ให้กำหนดขอบเขตและยกเลิกงานเหล่านั้น
- ใช้
DisposableEffectใน Compose เพื่อยกเลิกการลงทะเบียนการเรียกกลับ - LeakCanary และ เครื่องมือวิเคราะห์หน่วยความจำ ช่วยเปิดเผยสายโซ่การอ้างอิง
- กำหนดขนาดสูงสุดของแคชและลดขนาด บิตแมป
ถัดไป: ทำให้แอปเริ่มทำงานได้อย่างรวดเร็วด้วยการเพิ่มประสิทธิภาพการเริ่มต้นและโพรไฟล์พื้นฐาน
คำถามที่พบบ่อย
บทเรียน “หน่วยความจำรั่วและวิธีแก้ไข” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “หน่วยความจำรั่วและวิธีแก้ไข” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Android Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Android Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “หน่วยความจำรั่วและวิธีแก้ไข”
ค้นหาและหยุดการรั่วไหล คุณปฏิบัติ Android Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Android Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Android Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “หน่วยความจำรั่วและวิธีแก้ไข” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Android Academy นี้ได้ไหม
ได้ บทเรียน Android Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การวัดประสิทธิภาพ
- การควบคุมการจัดองค์ประกอบใหม่
- หน่วยความจำรั่วและวิธีแก้ไข
- โปรไฟล์การเริ่มต้นและโปรไฟล์พื้นฐาน