0Pricing
Android Academy · บทเรียน

หน่วยความจำรั่วและวิธีแก้ไข

ค้นหาและหยุดการรั่วไหล

หน่วยความจำรั่วและวิธีแก้ไข เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การวัดประสิทธิภาพ
  2. การควบคุมการจัดองค์ประกอบใหม่
  3. หน่วยความจำรั่วและวิธีแก้ไข
  4. โปรไฟล์การเริ่มต้นและโปรไฟล์พื้นฐาน
← กลับไปที่ Android Academy