หน่วยความจำรั่ว — หนึ่งในปัญหาที่ร้ายกาจที่สุดในการพัฒนาแอปมือถือ การใช้หน่วยความจำของแอปเพิ่มขึ้นเรื่อย ๆ จนกระทั่งถึงขีดจำกัดที่ระบบปฏิบัติการกำหนด ตามด้วย OutOfMemoryError หรือการยุติการทำงานโดยบังคับ จากข้อมูลของ Square Engineering ประมาณ 40% ของแอป Android มีหน่วยความจำรั่วอย่างน้อยหนึ่งจุดที่สามารถตรวจพบได้ผ่านการทำโปรไฟล์เท่านั้น มาดูสาเหตุและวิธีการป้องกันการเติบโตของหน่วยความจำกัน
ประเด็นสำคัญ
หน่วยความจำรั่ว — สถานการณ์ที่ออบเจ็กต์ที่แอปไม่ต้องการอีกต่อไปยังคงถูกเก็บไว้ในฮีปเนื่องจากการอ้างอิงที่ยังทำงานอยู่จากชุดราก (GC Root) ยังคงชี้ไปยังมัน ตัวเก็บขยะถือว่าออบเจ็กต์ดังกล่าวยังมีชีวิตอยู่และไม่ลบออก
หน่วยความจำบวม — ปัญหาที่กว้างขึ้นเมื่อแอปใช้หน่วยความจำมากกว่าที่จำเป็นสำหรับการทำงานปัจจุบัน สาเหตุ: การแคชมากเกินไป การซ้ำซ้อนของออบเจ็กต์ โครงสร้างข้อมูลที่ไม่เหมาะสม และการกระจายตัวของฮีป
ใน Android แอปแต่ละตัวจะได้รับการจัดสรรฮีปแบบจำกัด (โดยปกติ 64–512 MB ขึ้นอยู่กับอุปกรณ์และเวอร์ชันระบบปฏิบัติการ) ใน iOS ขีดจำกัดเข้มงวดน้อยกว่า แต่ระบบจะส่งคำเตือนหน่วยความจำเมื่อเข้าใกล้ขีดจำกัด
| คุณลักษณะ | Android | iOS |
|---|---|---|
| ขีดจำกัดฮีป | 64–512 MB (ขึ้นอยู่กับอุปกรณ์) | โดยนัย (ระบบ) |
| การเก็บขยะ | ART (พร้อมกัน, ขนาดกะทัดรัด) | ARC (การนับการอ้างอิงอัตโนมัติ) |
| กลไกการรั่ว | การอ้างอิง GC Root | วงจรการคงอยู่ (วงจรการอ้างอิงที่แข็งแกร่ง) |
| ผลลัพธ์ | OutOfMemoryError | คำเตือนหน่วยความจำ → การยุติ |
ตาม Facebook Engineering Blog หน่วยความจำรั่วเป็นสาเหตุของรายงานการขัดข้องประมาณ ~15% ในแอปมือถือ ใน Android ยังมี ANR เพิ่มเติมจากการหยุด GC บ่อยครั้งเมื่อหน่วยความจำเหลือน้อย
การอ้างอิงแบบคงที่ไปยัง Activity — การรั่วแบบคลาสสิกของ Android หากฟิลด์แบบคงที่หรือซิงเกิลตันเก็บการอ้างอิงไปยัง Activity จะไม่ถูกเก็บโดย GC แม้หลังจาก finish() ตราบใดที่ซิงเกิลตันยังมีชีวิตอยู่ Activity เป็นออบเจ็กต์ที่หนักหน่วงซึ่งมีลำดับชั้นวิว ทรัพยากร และ Context
object LeakHolder {
var activityRef: Activity ?= null // leak: static reference to Activity
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle ?= null) {
super.onCreate(savedInstanceState)
LeakHolder.activityRef = this // ❌ MainActivity will never be GC'd
}
}
คลาสนิรนามและแลมบ์ดา — เก็บการอ้างอิงไปยังคลาสภายนอกโดยปริยาย หาก Runnable หรือ Callback ถูกส่งไปยังบริการภายนอกและ Activity ถูกทำลาย ออบเจ็กต์คลาสนิรนามยังคงอยู่ในคิวและป้องกันไม่ให้ Activity ถูกเก็บขยะ
ใน iOS ปัญหาหลักคือ วงจรการคงอยู่: ออบเจ็กต์สองตัวเก็บการอ้างอิงที่แข็งแกร่งซึ่งกันและกัน และ ARC ไม่สามารถทำให้ตัวนับการอ้างอิงเป็นศูนย์สำหรับทั้งสองได้ กรณีทั่วไป: คลอเชอร์ที่จับ self อย่างแข็งแกร่ง และ self ที่เก็บการอ้างอิงไปยังคลอเชอร์
LeakCanary — ไลบรารีจาก Square สำหรับการตรวจจับการรั่วแบบอัตโนมัติใน Android หลังจาก Activity หรือ Fragment ถูกทำลาย มันจะตรวจสอบว่าออบเจ็กต์ถูกเก็บโดย GC หรือไม่ ถ้าไม่ มันจะทำฮีปดัมพ์และแสดงร่องรอยการรั่ว
// LeakCanary 2.x — auto-integration via Application
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary auto-installs in debug build
// via ContentProvider — zero code setup
}
}
// Force check invocation
AppWatcher.objectWatcher.watch(watchedObject, "leak description")
Android Studio Profiler — เครื่องมือในตัวสำหรับการตรวจสอบหน่วยความจำแบบเรียลไทม์ ช่วยให้บันทึกฮีปดัมพ์ ค้นหาออบเจ็กต์ที่น่าสงสัย (Retained Size > 1 MB) และติดตามเส้นทางราก GC ไปยังแต่ละออบเจ็กต์
สำหรับ iOS ให้ใช้ Xcode Memory Graph Debugger มันแสดงภาพกราฟออบเจ็กต์ในหน่วยความจำ แสดงวงจรการคงอยู่ และช่วยให้ตรวจจับการอ้างอิงแบบวงกลมได้ทันที Instruments > Allocations ก็พร้อมใช้งานสำหรับการตรวจสอบระยะยาว
WeakReference — กลไกพื้นฐานสำหรับการอ้างอิงที่ไม่ควรรบกวนการเก็บขยะ หาก GC ตัดสินใจเก็บออบเจ็กต์ WeakReference จะคืนค่า null ใช้สำหรับการเรียกกลับ ตัวฟัง และการอ้างอิงถึงคอมโพเนนต์ UI จากเธรดพื้นหลัง
คอมโพเนนต์ที่รับรู้วงจรชีวิต — แนวทางสถาปัตยกรรมที่นำมาใช้ใน Android Jetpack (Lifecycle, LiveData, Flow, coroutines) การสมัครสมาชิกจะถูกยกเลิกโดยอัตโนมัติที่ onDestroy ซึ่งกำจัดคลาสหลักของการรั่ว
class MyViewModel : ViewModel() {
private val _data = MutableLiveData<List<User>>()
val data: LiveData<List<User>> get() = _data
fun loadData() {
viewModelScope.launch {
val result = repository.fetchData()
_data.postValue(result)
// coroutine auto-cancels on onCleared()
}
}
}
viewModelScope และ lifecycleScope — CoroutineScope ในตัวใน Android ที่ถูกยกเลิกเมื่อเหตุการณ์วงจรชีวิตที่เกี่ยวข้อง ซึ่งกำจัดการรั่วผ่านโคโรทีน — สถานการณ์ที่พบบ่อยที่สุดในการพัฒนา Android สมัยใหม่
Memory Profiler ใน Android Studio — เครื่องมือหลักสำหรับการตรวจสอบฮีป แสดงการจัดสรรสด ภาพรวมฮีป และจำนวนออบเจ็กต์ตามประเภท ช่วยให้บันทึกดัมพ์และวิเคราะห์ใน MAT (Memory Analyzer Tool) เพื่อค้นหาออบเจ็กต์ที่น่าสงสัย
Eclipse MAT — โปรแกรมวิเคราะห์ฮีปดัมพ์บนเดสก์ท็อป หลังจากโหลดไฟล์ HPROF จาก Android Studio MAT จะสร้างโครงสร้างแบบโดมิเนเตอร์ แสดงขนาดการคงอยู่ของแต่ละออบเจ็กต์ และเสนอการวิเคราะห์การรั่วที่น่าสงสัยอัตโนมัติผ่าน Leak Suspects Report
Xcode Memory Graph — ดีบักเกอร์วงจรการคงอยู่แบบภาพ เมื่อคลิกปุ่ม Memory Graph Debugger Xcode จะหยุดแอป สร้างกราฟออบเจ็กต์ที่สมบูรณ์ในหน่วยความจำ และเน้นวงจรการคงอยู่ด้วยสีแดง
| เครื่องมือ | แพลตฟอร์ม | คุณสมบัติ |
|---|---|---|
| LeakCanary | Android | ตรวจจับการรั่วอัตโนมัติหลังถูกทำลาย |
| Memory Profiler | Android Studio | ฮีปดัมพ์ + การจัดสรรสด |
| Eclipse MAT | Android | โครงสร้างแบบโดมิเนเตอร์, Leak Suspects Report |
| Memory Graph | iOS (Xcode) | ตัวแสดงภาพวงจรการคงอยู่ |
ตาม Google I/O 2023 แอปที่ใช้ LeakCanary ในบิลด์ดีบักลดการขัดข้องที่เกี่ยวข้องกับหน่วยความจำลง 30–50% ใน 2 เดือนแรกหลังการนำไปใช้ แนะนำให้เพิ่ม LeakCanary ในช่วงเริ่มต้นโครงงาน
คำถามที่พบบ่อย
การรั่ว — ออบเจ็กต์ที่โค้ดไม่สามารถเข้าถึงได้ แต่ไม่ถูกเก็บโดย GC เนื่องจากการอ้างอิงที่ยังทำงานอยู่ การบวม — แอปเก็บออบเจ็กต์ที่จำเป็นตามตรรกะแต่ในปริมาณที่มากเกินไป (เช่น แคช 50 MB ในแอปที่ทำงานอยู่ 80 MB) การบวมแก้ไขด้วยสถาปัตยกรรม การรั่วแก้ไขด้วยการจัดการการอ้างอิงที่ถูกต้อง
LeakCanary ใช้ ObjectWatcher — หลังจาก onDestroy() ของ Activity มันจะสร้าง WeakReference ไปยัง Activity และเรียกใช้ GC หาก WeakReference ไม่ถูกล้างหลังจาก 5 วินาที LeakCanary จะทำฮีปดัมพ์ วิเคราะห์ห่วงโซ่การอ้างอิงที่สั้นที่สุดจาก GC Root ไปยังออบเจ็กต์ และแสดงสแต็กการรั่วที่แน่นอนพร้อมไฟล์และบรรทัดของโค้ด
Bitmap ใช้หน่วยความจำภายนอกฮีป Java ในหน่วยความจำพื้นเมือง (native heap) ขนาดของ Bitmap หนึ่งอัน = ความกว้าง × ความสูง × 4 ไบต์ (ARGB_8888) รูปภาพ 12 MP (4000×3000) ใช้ 48 MB Android ไม่สามารถปล่อยหน่วยความจำพื้นเมืองได้ทันเวลาเสมอไป ดังนั้นการสะสมของ Bitmap หลายอันนำไปสู่ OOM แม้จะมีฮีป Java เพียงพอ
วงจรการคงอยู่ — สถานการณ์ใน ARC ที่ออบเจ็กต์สองตัวเก็บการอ้างอิงที่แข็งแกร่งซึ่งกันและกัน และตัวนับการอ้างอิงไม่ถึงศูนย์ ตัวอย่างทั่วไป: ViewController ที่มีการอ้างอิงที่แข็งแกร่งไปยังคลอเชอร์ และคลอเชอร์จับ self อย่างแข็งแกร่ง วิธีแก้ไข: ใช้ [weak self] หรือ [unowned self] ในคลอเชอร์
ขนาด ฮีป ขึ้นอยู่กับอุปกรณ์และเวอร์ชัน Android สำหรับอุปกรณ์เก่า (API 15–24) — 64–128 MB สำหรับอุปกรณ์สมัยใหม่ (API 25+) — 256–512 MB ค่าที่แน่นอนสามารถรับได้ผ่าน ActivityManager.getMemoryClass() สำหรับแอปขนาดใหญ่ (เกม โปรแกรมแก้ไข) largeHeap=true ในแมนิเฟสต์จะให้สูงสุด 1 GB
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ