LeakCanary เป็นไลบรารีโอเพนซอร์สจาก Square สำหรับการตรวจจับการรั่วไหลของหน่วยความจำโดยอัตโนมัติในแอปพลิเคชัน Android มันรวมเข้ากับกระบวนการพัฒนาและติดตามวงจรชีวิตของ Activity, Fragment, ViewModel และคอมโพเนนต์อื่น ๆ แบบเรียลไทม์ โดยแจ้งสัญญาณการรั่วไหลทันทีที่เกิดขึ้น ตามข้อมูลจาก Square Open Source ไลบรารีนี้ถูกใช้ในโปรเจกต์นับพันและถือเป็นมาตรฐานโดยพฤตินัยสำหรับการวินิจฉัยหน่วยความจำบน Android
ประเด็นสำคัญ
LeakCanary เป็นไลบรารีสำหรับตรวจจับการรั่วไหลของหน่วยความจำโดยอัตโนมัติในแอปพลิเคชัน Android พัฒนาโดย Square มันรวมเข้ากับกระบวนการสร้างแอปและตรวจสอบโดยอัตโนมัติว่าออบเจกต์ที่ควรถูกทำลาย (Activity, Fragment, View) ยังคงอยู่ในหน่วยความจำหรือไม่ เมื่อตรวจพบการรั่วไหล LeakCanary จะสร้าง heap dump และวิเคราะห์สายโซ่อ้างอิงที่ยึดออบเจกต์ไว้
ไลบรารีกลายเป็นมาตรฐานในชุมชน Android: ตามข้อมูลจาก GitHub โปรเจกต์มีดาวมากกว่า 28,000 ดวงและถูกใช้ในแอปพลิเคชันของ Google, Uber, Airbnb และ Facebook LeakCanary มีให้ใช้งานสองเวอร์ชันหลัก: เวอร์ชันคลาสสิก 1.x (พร้อมการกำหนดค่าด้วยตนเอง) และเวอร์ชันทันสมัย 2.x (การรวมระบบอัตโนมัติผ่าน ContentProvider) เวอร์ชัน 2.x ไม่ต้องการการแก้ไขคลาส Application — แค่ dependency ก็เพียงพอสำหรับการทำงานที่สมบูรณ์
งานหลักของ LeakCanary คือตรวจจับเมื่อออบเจกต์ยังคงอยู่ในหน่วยความจำหลังจากวงจรชีวิตของมันสิ้นสุดลง ซึ่งเป็นเรื่องปกติสำหรับการรั่วไหลผ่านฟิลด์สแตติก, ซิงเกิลตัน, คอลแบ็กที่ไม่ได้ยกเลิกการลงทะเบียน, คลาสนิรนาม และ closure ที่จับออบเจกต์ภายนอก
การรั่วไหลของหน่วยความจำบน Android มีความสำคัญมากกว่าบนเดสก์ท็อปเนื่องจาก RAM ที่จำกัดบนอุปกรณ์มือถือ แม้แต่การรั่วไหล 5–10 MB ในทุกการเปลี่ยนหน้าจออาจนำไปสู่ OutOfMemoryError หลังจากใช้งานแอป 30–40 นาที LeakCanary ตรวจจับปัญหาเหล่านี้ในขั้นตอนการพัฒนา โดยไม่ต้องรอการล่มในโปรดักชัน
LeakCanary ใช้การอ้างอิงแบบอ่อน (WeakReference) ร่วมกับการเก็บขยะแบบบังคับ เมื่อ Activity หรือ Fragment เรียก onDestroy LeakCanary จะสร้าง WeakReference ไปยังออบเจกต์นั้นและเรียก GC หลังจากหน่วงเวลาเล็กน้อย (ค่าเริ่มต้น 5 วินาที) หากออบเจกต์ยังคงเข้าถึงได้ผ่าน WeakReference หลังจาก GC แสดงว่ามันถูกยึดไว้โดยการอ้างอิงแบบแข็ง — มีการบันทึกการรั่วไหล
หลังจากตรวจพบการรั่วไหล LeakCanary จะทำ heap dump (ดัมพ์หน่วยความจำ) — ภาพรวมทั้งหมดของหน่วยความจำแอปในรูปแบบ HPROF จากนั้นโปรแกรมวิเคราะห์ในตัว (Shark สำหรับเวอร์ชัน 2.x) จะสร้างกราฟการเข้าถึงจาก GC Roots ไปยังออบเจกต์ที่รั่วไหลและหาเส้นทางที่สั้นที่สุด — สายโซ่อ้างอิงที่ยึดออบเจกต์ไว้ในหน่วยความจำ
// ตรรกะการตรวจจับ LeakCanary แบบง่าย
class ObjectWatcher {
private val watchedReferences = CopyOnWriteArrayList<KeyedWeakReference>()
fun watch(watchedObject: Any, description: String) {
val reference = KeyedWeakReference(watchedObject, description)
watchedReferences.add(reference)
BackgroundHandler.postDelayed({
checkForLeaks()
}, 5000)
}
private fun checkForLeaks() {
GcTrigger.runGc() // GC แบบบังคับ
for (ref in watchedReferences) {
if (ref.get() != null) {
onLeakFound(ref) // ออบเจกต์รอดจาก GC — นี่คือการรั่วไหล
}
}
}
}
จุดสำคัญคือการเรียก GcTrigger.runGc() แบบบังคับ หากไม่มีมัน เป็นไปไม่ได้ที่จะแยกแยะออบเจกต์ที่รั่วไหลจริง ๆ กับออบเจกต์ที่ GC ยังไม่ได้เก็บ LeakCanary ทำเช่นนี้ถึงสามครั้ง: หากหลังจากสามรอบ GC ออบเจกต์ยังคงอยู่ในหน่วยความจำ การรั่วไหลจะได้รับการยืนยัน
Shark คือโปรแกรมวิเคราะห์ heap dump ในตัวของ LeakCanary 2.x ที่เขียนด้วย Kotlin แตกต่างจากโปรแกรมวิเคราะห์ HAHA รุ่นก่อนหน้า Shark ไม่โหลดไฟล์ HPROF ทั้งหมดเข้าสู่หน่วยความจำ แต่สำรวจกราฟออบเจกต์ด้วยการจัดสรรน้อยที่สุด ซึ่งลดการใช้ RAM ระหว่างการวิเคราะห์จาก 50 MB เหลือ 2–5 MB และลดเวลาในการวิเคราะห์จาก 30 วินาทีเหลือ 1–3 วินาที
การติดตั้ง LeakCanary 2.x ในโปรเจกต์ Android สมัยใหม่ใช้เพียงบรรทัดเดียวใน build.gradle ไลบรารีใช้ ContentProvider สำหรับการเริ่มต้นอัตโนมัติ — ไม่จำเป็นต้องแก้ไขคลาส Application หรือเพิ่มโค้ดใด ๆ ไปยัง MainActivity โดยเพิ่ม dependency สำหรับบิลด์ debug เท่านั้น เพื่อไม่ให้ APK ที่เผยแพร่มีโค้ดเกินจำเป็น
// build.gradle (app/module)
dependencies {
// debugImplementation — ไลบรารีสำหรับบิลด์ debug เท่านั้น
debugImplementation "com.squareup.leakcanary:leakcanary-android:2.14"
}
หลังจากเพิ่ม dependency และสร้างโปรเจกต์ใหม่ LeakCanary จะปรากฏในแอปโดยอัตโนมัติ ในการเปิดครั้งแรก ไลบรารีจะแสดงการแจ้งเตือนของระบบเพื่อยืนยันการเปิดใช้งาน การรั่วไหลที่ตรวจพบทั้งหมดจะแสดงเป็นการแจ้งเตือน — การแตะการแจ้งเตือนจะเปิดหน้าจอพร้อมรายงานโดยละเอียด (LeakTrace)
สำหรับการปรับแต่ง คุณสามารถสร้าง AppWatcherInstaller ของคุณเองและแทนที่พารามิเตอร์: หมดเวลา GC, รายการประเภทออบเจกต์ที่ติดตาม, การเปิดใช้งานการบันทึก heap dump ลงดิสก์ อย่างไรก็ตาม สำหรับ 90% ของโปรเจกต์ การกำหนดค่าเริ่มต้นนั้นเหมาะสมที่สุด
เริ่มตั้งแต่เวอร์ชัน 2.12 LeakCanary รองรับการติดตาม ViewModel, ขอบเขต coroutine และออบเจกต์ State ของ Compose โดยอัตโนมัติ ไม่จำเป็นต้องมี dependency เพิ่มเติม — ไลบรารีตรวจจับโดยอัตโนมัติว่าคอมโพเนนต์ Jetpack ใดถูกใช้ในโปรเจกต์และเปิดใช้งานตัวตรวจจับที่เกี่ยวข้อง
รายงาน LeakCanary (LeakTrace) คือสายโซ่อ้างอิงหลายบรรทัดจาก GC Root ไปยังออบเจกต์ที่รั่วไหล แต่ละบรรทัดแสดงคลาสและฟิลด์ที่การอ้างอิงแบบแข็งผ่าน นักพัฒนาควรอ่านสายโซ่จากล่างขึ้นบน: บรรทัดล่างสุดคือออบเจกต์ที่รั่วไหล บรรทัดบนสุดคือจุดเข้า (GC Root)
LeakTrace ทั่วไปมีลักษณะดังนี้: GC Root → ฟิลด์สแตติกของ Application → ซิงเกิลตัน → คอลแบ็ก → Activity หากนักพัฒนาเห็นสายโซ่ดังกล่าว ปัญหาก็ชัดเจน: ซิงเกิลตัน ถือคอลแบ็กที่จับการอ้างอิงไปยัง Activity วิธีแก้ไข — เปลี่ยนการอ้างอิงแบบแข็งเป็นการอ้างอิงแบบอ่อนในซิงเกิลตัน
┬
├─ android.app.Application
│ Leaking: NO (Application — singleton)
│ ↓ Application.leakedActivities
├─ java.util.ArrayList
│ Leaking: NO (ArrayList — normal)
│ ↓ ArrayList[0]
├─ com.example.MainActivity
│ Leaking: YES (Activity destroyed but still in memory)
│ ↓ MainActivity.mCallback
├─ com.example.CallbackWrapper
│ Leaking: UNKNOWN
│ ↓ CallbackWrapper.mListener
│ ~~~~~~~~~~
├─ com.example.MyCallback (anonymous)
│ Leaking: UNKNOWN
│ ↓ MyCallback.this$0
├─ com.example.MainActivity
│ Leaking: YES (MainActivity is the leak)
╰
ในตัวอย่างนี้ LeakCanary แสดงให้เห็นว่า MainActivity ถูกยึดผ่านสายโซ่: Application → ArrayList → MainActivity → CallbackWrapper → MyCallback → MainActivity อีกครั้ง ลูกศร this$0 บ่งชี้ว่าคลาสนิรนาม MyCallback จับการอ้างอิงภายนอกไปยัง Activity วิธีแก้ไข — ทำให้คอลแบ็กเป็นการอ้างอิงแบบอ่อนหรือยกเลิกมันใน onDestroy
LeakCanary ยังแสดง สถานะการรั่วไหล สำหรับแต่ละองค์ประกอบในสายโซ่: NO (ไม่มีการรั่วไหล — เป็นองค์ประกอบราก), YES (ควรทำลายออบเจกต์), UNKNOWN (ไม่สามารถระบุสถานะได้) สถานะ UNKNOWN ไม่ได้หมายถึงปัญหา — มันคือออบเจกต์กลางที่ LeakCanary ไม่สามารถจำแนกได้อย่างชัดเจน
การเปลี่ยนจากเวอร์ชัน 1.x เป็น 2.x เป็นการเปลี่ยนแปลงครั้งใหญ่: นักพัฒนเขียนไลบรารีใหม่ทั้งหมด แทนที่โปรแกรมวิเคราะห์ HAHA แบบเก่าด้วยเอนจินของตัวเอง Shark ที่เขียนด้วย Kotlin Shark ทำงานเร็วกว่าหลายเท่า ต้องการหน่วยความจำน้อยกว่าสำหรับการวิเคราะห์ และระบุสาเหตุที่แท้จริงของการรั่วไหลได้แม่นยำกว่า
| พารามิเตอร์ | LeakCanary 1.x | LeakCanary 2.x |
|---|---|---|
| ภาษาโปรแกรมวิเคราะห์ | Java (HAHA — fork ของ Android SDK) | Kotlin (Shark — เอนจินของตัวเอง) |
| การติดตั้ง | กำหนดค่า AppWatcher ด้วยตนเองใน Application | อัตโนมัติผ่าน ContentProvider |
| ความเร็ว | 10–30 วินาทีต่อการวิเคราะห์ heap dump | 1–5 วินาทีต่อการวิเคราะห์ heap dump |
| ประสิทธิภาพ | ใช้ RAM 10–50 MB ระหว่างการวิเคราะห์ | ใช้ RAM 2–10 MB ระหว่างการวิเคราะห์ |
ข้อได้เปรียบหลักของ Shark คือมันไม่โหลด heap dump ทั้งหมดเข้าสู่หน่วยความจำ แต่สำรวจกราฟการอ้างอิงด้วยการจัดสรรน้อยที่สุด ทำให้ LeakCanary 2.x เหมาะสำหรับการใช้งานบนอุปกรณ์ที่มี RAM ต่ำโดยไม่มีความเสี่ยงของ OutOfMemoryError ระหว่างการวิเคราะห์
เวอร์ชัน 2.x ยังเพิ่มความสามารถในการ ส่งออก heap dump ไปยังไฟล์สำหรับการวิเคราะห์ภายหลังใน Android Studio Memory Profiler โดยเปิดใช้งานการตั้งค่า dumpHeapWhenLeakFound ในการกำหนดค่า AppWatcher
LeakCanary ตรวจจับการรั่วไหลหลายประเภทที่พบบน Android ได้อย่างมีประสิทธิภาพ ที่พบบ่อยที่สุดคือการรั่วไหลผ่านการอ้างอิงสแตติกไปยัง Activity — นักพัฒนาเก็บการอ้างอิงไปยังบริบทของ Activity ในซิงเกิลตัน และ Activity ไม่สามารถถูกรวบรวมโดย GC หลังจากวงจรชีวิตสิ้นสุดลง
ประเภทที่พบบ่อยเป็นอันดับสองคือการรั่วไหลผ่าน ผู้ฟังที่ไม่ได้ยกเลิกการลงทะเบียน หาก registerListener ถูกเรียกใน onStart แต่ unregisterListener ไม่ได้ถูกเรียกใน onStop/onDestroy ออบเจกต์ผู้ฟังจะถูกยึดโดยระบบแม้หลังจากกิจกรรมถูกทำลาย LeakCanary แสดงให้เห็นอย่างชัดเจนว่าผู้ฟังใดและในบริการระบบใดยังคงมีชีวิตอยู่
// การรั่วไหลทั่วไป: Activity ถูกจับในคอลแบ็กของซิงเกิลตัน
object AnalyticsManager {
private var callback: ((String) -> Unit)? = null
fun register(callback: (String) -> Unit) {
this.callback = callback // การอ้างอิงแบบแข็งไปยังคอลแบ็ก
}
fun unregister() {
callback = null // อย่าลืมเรียกใน onDestroy!
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
AnalyticsManager.register { event ->
logEvent(event) // แลมบ์ดาจับ this
}
// หากไม่เรียก unregister ใน onDestroy → การรั่วไหลของ Activity
}
}
ประเภทที่สามคือการรั่วไหลผ่าน Fragment ใน BackStack หาก FragmentTransaction.addToBackStack() ถูกเรียกโดยไม่ลบ Fragment เมื่อกลับไป อินสแตนซ์ Fragment เก่าจะยังคงอยู่ในหน่วยความจำ LeakCanary ช่วยตรวจจับการรั่วไหลที่ซ่อนอยู่เหล่านี้ในระยะแรกของการพัฒนา
สำหรับการรั่วไหลที่ตรวจพบแต่ละครั้ง LeakCanary ให้คำอธิบายและคำแนะนำในการแก้ไข เวอร์ชัน 2.14 เพิ่มการรวมเข้ากับ Android Lint — ไลบรารีสามารถสร้างงานในระบบติดตามปัญหาอัตโนมัติเมื่อตรวจพบการรั่วไหลใน CI
คำถามที่พบบ่อย
ใช่ จำเป็นอย่างยิ่ง LeakCanary ถูกเพิ่มผ่าน debugImplementation ใน build.gradle ซึ่งจะแยกมันออกจากบิลด์ที่เผยแพร่โดยอัตโนมัติ หากใช้ implementation ไลบรารีจะรวมอยู่ใน APK ที่เผยแพร่และแสดงการรั่วไหลให้ผู้ใช้เห็น — ซึ่งไม่สามารถยอมรับได้
ผลกระทบต่อประสิทธิภาพนั้นน้อยมาก LeakCanary จะทำงานหลังจาก onDestroy ของคอมโพเนนต์เท่านั้น และไม่รบกวนการเรนเดอร์ UI หรือการจัดการสัมผัส ค่าใช้จ่ายเดียวคือการหยุด GC แบบบังคับสั้น ๆ (ประมาณ 100 มิลลิวินาที) และการเขียน heap dump เมื่อเกิดการรั่วไหล (เสี้ยววินาที)
LeakCanary จะบันทึก heap dump ในรูปแบบ HPROF ไปยังโฟลเดอร์แอปโดยอัตโนมัติ ไฟล์สามารถส่งออกผ่าน Android Studio: Device File Explorer → data/data/com.example/files/leakcanary/ หากต้องการดู ให้เปิดไฟล์ใน Memory Profiler ผ่าน Capture → Open Heap Dump
ใช่ ตั้งแต่เวอร์ชัน 2.12 LeakCanary รองรับ Jetpack Compose อย่างเต็มที่ ไลบรารีติดตามบริบท Composition และออบเจกต์ State ตรวจจับการรั่วไหลในฟังก์ชัน Composable โดยอัตโนมัติ ไม่จำเป็นต้องกำหนดค่าแยกต่างหาก — ใช้งานได้ทันที
ผลบวกปลอมเกิดขึ้นได้แต่พบได้ยาก LeakCanary ใช้การเรียก GC สามครั้งก่อนประกาศการรั่วไหล ซึ่งกำจัดผลบวกปลอมส่วนใหญ่ หากคุณคิดว่าการตรวจจับเป็นผลบวกปลอม ให้สร้าง IgnoredReference สำหรับคลาสเฉพาะในการกำหนดค่า
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม