Memory Graph คือเครื่องมือแสดงภาพของ Xcode Debug Navigator ที่แสดงกราฟของอ็อบเจ็กต์ในหน่วยความจำของแอปพลิเคชันพร้อมกับการอ้างอิงซึ่งกันและกัน แตกต่างจาก heap dump ตรงที่ Memory Graph ไม่เพียงแสดงรายการอ็อบเจ็กต์เท่านั้น แต่แสดงกราฟการอ้างอิงแบบมีทิศทางที่แต่ละโหนดคืออ็อบเจ็กต์และแต่ละขอบคือการอ้างอิง (strong, weak, unowned) ตามข้อมูลจาก Apple WWDC 2018 เครื่องมือนี้ช่วยให้คุณค้นหา retain cycles และหน่วยความจำรั่วได้ในไม่กี่วินาที โดยไม่ต้องวิเคราะห์ข้อมูลดิบของ heap dump
ประเด็นสำคัญ
Memory Graph คือส่วนประกอบของ Xcode Debug Navigator (เปิดตัวใน Xcode 10, WWDC 2018) ที่สร้างกราฟแบบมีทิศทางของอ็อบเจ็กต์ทั้งหมดในหน่วยความจำของกระบวนการที่กำลังดีบัก แต่ละโหนดของกราฟคืออินสแตนซ์ของคลาส (Objective-C หรือ Swift) แต่ละขอบคือการอ้างอิงไปยังอ็อบเจ็กต์อื่น สีของขอบบ่งบอกประเภทการอ้างอิง: สีน้ำเงิน — strong, สีเขียว — weak, สีเทา — unowned กราฟถูกสร้างขึ้นจากข้อมูลของ LLDB และ Objective-C runtime ดังนั้นแอปพลิเคชันต้องถูกคอมไพล์ในโหมด Debug โดยเปิดใช้งานสัญลักษณ์เพื่อการทำงานที่ถูกต้อง
หลักการทำงาน: เมื่อแอปพลิเคชันถูกหยุดที่ breakpoint Xcode จะขออ็อบเจ็กต์ที่ยังมีชีวิตทั้งหมดและการอ้างอิงจาก runtime ผ่าน LLDB LLDB ใช้ objc_getClassList และวนซ้ำผ่านพื้นที่จัดสรรเพื่อสร้างกราฟที่สมบูรณ์ บน ARM64 (Apple Silicon) มีการใช้ฮาร์ดแวร์เพิ่มเติมเพื่อติดตามการจัดสรรโดยไม่ทำให้ช้าลง เวลาในการสร้างกราฟขึ้นอยู่กับขนาดฮีป: สำหรับแอปพลิเคชัน iOS ทั่วไป (50–200 MB) กราฟจะถูกสร้างใน 1–3 วินาที
ตามข้อมูลของ Apple Memory Graph เป็นเครื่องมือเดียวที่สามารถแสดงภาพ retain cycles โดยไม่ต้องแก้ไขโค้ดหรือเพิ่มเครื่องมือวัด แตกต่างจาก Instruments Leaks ตรงที่ Memory Graph ทำงานแบบเรียลไทม์ภายใน Xcode และไม่ต้องเปิดโปรไฟเลอร์แยกต่างหาก สิ่งนี้ทำให้มันเป็นเครื่องมืออันดับแรกสำหรับการวินิจฉัยหน่วยความจำรั่วอย่างรวดเร็วระหว่างการพัฒนา
Heap dump ให้ตารางของอ็อบเจ็กต์ทั้งหมดพร้อมตัวเลข (shallow size, retained size) — เหมาะสำหรับการวิเคราะห์เชิงปริมาณ Memory Graph ให้ภาพแสดงการเชื่อมต่อ — เหมาะสำหรับการค้นหาการอ้างอิงแบบวนซ้ำ เครื่องมือทั้งสองเสริมซึ่งกันและกัน: เริ่มต้นด้วย Memory Graph เพื่อตรวจหา retain cycles อย่างรวดเร็ว จากนั้นใช้ heap dump ผ่าน Instruments Allocations เพื่อวัด retained size ที่แม่นยำ ตามข้อมูลของ objc.io การรวมสองวิธีนี้ครอบคลุม 95% ของสถานการณ์หน่วยความจำรั่ว
Retain cycle คือสถานการณ์ที่อ็อบเจ็กต์สองตัวขึ้นไปยึดถือซึ่งกันและกันด้วยการอ้างอิงแบบ strong ทำให้เกิดลูปปิด ARC ไม่สามารถคืนหน่วยความจำของลูปดังกล่าวได้เพราะ retain count ของแต่ละอ็อบเจ็กต์ไม่ถึงศูนย์ ตัวอย่างคลาสสิก: ViewController และ View โดย View มีการอ้างอิงแบบ strong ไปยัง closure ที่จับ self (ViewController) Memory Graph แสดงลูปดังกล่าวเป็นวงแหวน (ไซเคิล) โดยเน้นให้เห็นเพื่อการระบุอย่างรวดเร็ว
เมื่อ Xcode ตรวจพบ retain cycle มันจะเน้นด้วยเส้นขอบสีส้มและแสดงคำเตือนใน Debug Navigator การคลิกที่ไซเคิลจะแสดงห่วงโซ่การอ้างอิงที่ก่อให้เกิดลูปปิด นักพัฒนาต้องกำหนดว่าขอบ strong ใดควรเป็น weak — โดยปกติคือการอ้างอิงจากอ็อบเจ็กต์ลูกไปยังอ็อบเจ็กต์แม่ (เช่น delegate หรือ closure)
class ViewController: UIViewController {
let service = DataService()
override func viewDidLoad() {
super.viewDidLoad()
// ❌ Retain cycle: ViewController → service → closure → ViewController
service.fetchData { self.updateUI($0) }
}
func updateUI(_ data: Data) {}
}
class DataService {
var completion: ((Data) -> Void)?
func fetchData(handler: @escaping (Data) -> Void) {
self.completion = handler
}
}
ใน Memory Graph คุณจะเห็นสามเหลี่ยม: ViewController → DataService → closure → ViewController วิธีแก้คือทำให้การจับ self เป็นแบบอ่อน: [weak self] หลังจากแก้ไข Memory Graph จะแสดงขอบสีเขียวจาก closure ไปยัง ViewController และ retain cycle จะหายไป
// โค้ดที่แก้ไขแล้ว — การจับ self แบบอ่อน
service.fetchData { [weak self] data in
guard let self else { return }
self.updateUI(data)
}
อินเทอร์เฟซของ Memory Graph Debugger ประกอบด้วยสามแผง: ซ้าย — รายการอ็อบเจ็กต์ที่ยังมีชีวิตทั้งหมด (จัดกลุ่มตามคลาส) พร้อมจำนวนอินสแตนซ์; กลาง — กราฟแสดงภาพพร้อมโหนดที่ลากได้; ขวา — ตัวตรวจสอบสำหรับอ็อบเจ็กต์หรือขอบที่เลือก รายการอ็อบเจ็กต์แสดง: ไอคอนคลาส จำนวนอินสแตนซ์ในหน่วยความจำ retained size ทั้งหมด และเปอร์เซ็นต์ของฮีปทั้งหมด การกรองตามชื่อคลาสรองรับนิพจน์ปกติ
โหนดของกราฟสามารถลากเพื่อปรับปรุงการอ่านได้ การดับเบิลคลิกที่โหนดจะเปิดข้อมูลโดยละเอียดเกี่ยวกับอ็อบเจ็กต์: คุณสมบัติทั้งหมดพร้อมประเภทและค่า สแต็กการเรียก (backtrace) สำหรับแต่ละคุณสมบัติ และประวัติ retain/release Backtrace คือฟีเจอร์สำคัญ: มันแสดงบรรทัดโค้ดที่แน่นอนซึ่งสร้างการอ้างอิงไปยังอ็อบเจ็กต์ สิ่งนี้ช่วยให้ค้นหาต้นตอของการรั่วไหลได้โดยไม่ต้องตรวจสอบโค้ดทั้งหมดด้วยตนเอง
สำหรับกราฟที่ซับซ้อน Xcode มีเค้าโครงอัตโนมัติผ่าน Layout → Hierarchical หรือ Cluster เค้าโครงแบบลำดับชั้นวางอ็อบเจ็กต์รากไว้ด้านบนและอ็อบเจ็กต์ลูกไว้ด้านล่าง ทำให้การค้นหาห่วงโซ่ง่ายขึ้น การจัดกลุ่มแบบคลัสเตอร์จะจัดกลุ่มอ็อบเจ็กต์ที่เกี่ยวข้องกัน ซึ่งสะดวกเมื่อกราฟมีหลายกลุ่มที่แยกจากกัน ตามข้อมูลของ Apple สำหรับแอปพลิเคชันส่วนใหญ่แนะนำให้ใช้เค้าโครงแบบลำดับชั้น — มันใช้งานง่ายและใช้เวลาน้อยกว่าสำหรับการวิเคราะห์ด้วยภาพ
// คำสั่ง LLDB ที่ Memory Graph ใช้ภายใน
(lldb) script import lldb.macosx.heap
(lldb) script heap.find_variable("viewController")
0x600000c4b80: ViewController
(lldb) script heap.refs 0x600000c4b80
0x600000c4b80 -> 0x600003a4c00 (DataService)
ivar: _service, offset: 16
แนวทางที่เป็นระบบสำหรับการวิเคราะห์ Memory Graph ประกอบด้วยหลายขั้นตอน ขั้นตอนที่ 1: เปิดแอปพลิเคชัน ดำเนินการตามสถานการณ์ที่อาจทำให้เกิดการรั่วไหล (เปิด/ปิดหน้าจอ ทำคำขอเครือข่าย) ขั้นตอนที่ 2: คลิกปุ่ม Memory Graph ใน Debug Navigator — Xcode สร้างกราฟ ขั้นตอนที่ 3: ตรวจสอบคำเตือน retain cycle สีส้มในแผงด้านซ้าย ขั้นตอนที่ 4: สำหรับอ็อบเจ็กต์ที่น่าสงสัย ให้ใช้ตัวเลือก Show only cycles — เฉพาะโหนดที่เกี่ยวข้องกับการอ้างอิงแบบวนซ้ำเท่านั้นที่จะแสดง
เมื่อพบ retain cycle แล้ว ให้คลิกที่ขอบของไซเคิลและเปิดแผงตัวตรวจสอบ ส่วน Backtrace แสดงสแต็กการเรียกในขณะที่สร้างการอ้างอิงนี้ ตัวอย่างเช่น ถ้าขอบนำจาก closure ไปยัง self backtrace จะแสดงว่าเมธอดใดและบรรทัดโค้ดใดที่ closure ถูกสร้างขึ้น สิ่งนี้ช่วยลดความจำเป็นในการเดา — คุณเห็นจุดที่สร้างการอ้างอิงที่มีปัญหาได้ทันที ตามข้อมูลของ WWDC Labs การวิเคราะห์ backtrace ช่วยลดเวลาในการวินิจฉัย retain cycle จาก 15–20 นาทีเหลือ 2–3 นาที
class ProfileViewController: UIViewController {
var profileView: ProfileView!
override func viewDidLoad() {
super.viewDidLoad()
profileView = ProfileView()
// Memory Graph จะแสดง retain cycle ที่นี่
profileView.onTap = { [unowned self] in
// ⚠️ unowned อาจทำให้ crash เมื่อ self เป็น nil
self.navigateToDetail()
}
}
func navigateToDetail() { }
}
// ✅ ถูกต้อง: [weak self] + guard let self
profileView.onTap = { [weak self] in
guard let self else { return }
self.navigateToDetail()
}
Memory Graph สามารถแสดงอ็อบเจ็กต์นับพัน ทำให้การค้นหายากขึ้น ใช้ ตัวกรอง ในแผงด้านซ้าย: ป้อนชื่อคลาส (เช่น ProfileViewController) เพื่อแสดงเฉพาะอินสแตนซ์ของคลาสนั้น จากนั้นเลือกอินสแตนซ์ที่ควรถูกคืนหน่วยความจำ (ถ้าปิดหน้าจอแล้วแต่อ็อบเจ็กต์ยังคงอยู่) ใช้ Show Reachable From — เฉพาะการอ้างอิงที่เกี่ยวข้องกับอ็อบเจ็กต์นี้เท่านั้นที่จะแสดง ส่วนที่เหลือของกราฟจะถูกซ่อน
นักพัฒนาที่มีประสบการณ์ใช้ Memory Graph ไม่เพียงเพื่อค้นหาการรั่วไหล แต่ยังเพื่อควบคุมหน่วยความจำเชิงรุก ตรวจสอบ Memory Graph หลังจากการเปลี่ยนแปลงสถาปัตยกรรมสำคัญแต่ละครั้ง — การเพิ่ม delegate ใหม่ closure หรือการสมัครรับ NotificationCenter เพียงเรียกใช้สถานการณ์ทั่วไปและตรวจสอบว่าอ็อบเจ็กต์ถูกคืนหน่วยความจำอย่างถูกต้องและไม่มี retain cycles ใช้เวลา 2–3 นาที แต่ป้องกันการดีบักเป็นชั่วโมงในภายหลัง
Memory Report ใน Xcode (แท็บ Debug Navigator) แสดงกราฟการใช้หน่วยความจำแบบเรียลไทม์ ใช้ร่วมกับ Memory Graph: เปิด Memory Graph เมื่อการใช้หน่วยความจำเพิ่มขึ้นอย่างรวดเร็ว ตัวอย่างเช่น เมื่อเลื่อนรายการยาวที่มีเซลล์โหลดรูปภาพ Memory Graph จะแสดงว่าอ็อบเจ็กต์ใดถูกสร้างขึ้นและอ็อบเจ็กต์ใดถูกคืนหน่วยความจำ ถ้าจำนวนอ็อบเจ็กต์เพิ่มขึ้นโดยไม่ลดลง — นี่คือการรั่วไหลที่อาจเกิดขึ้นซึ่งมองเห็นได้ก่อนที่จะทำให้เกิดการขัดข้อง ตามข้อมูลของ Apple การรวม Memory Graph + Memory Report คือเวิร์กโฟลว์ที่แนะนำสำหรับนักพัฒนา iOS ทุกคนตั้งแต่ Xcode 12
// ตัวอย่างการรั่วไหลใน Objective-C ผ่าน delegation
@interface DownloadManager : NSObject
@property (strong) id delegate; // ❌ ต้องเป็น weak!
@end
@implementation DownloadManager
// Memory Graph จะแสดง retain cycle:
// ViewController → DownloadManager.delegate → ViewController
@end
// การแก้ไข: weak property
@property (weak) id delegate;
ให้ความสนใจเป็นพิเศษกับ closures — แหล่งที่มาที่พบบ่อยที่สุดของ retain cycles ใน Swift เมื่อจับ self ภายใน closure ที่ถูกเก็บเป็นคุณสมบัติของอ็อบเจ็กต์ จะเกิดไซเคิลแบบคลาสสิก Memory Graph แสดงสิ่งนี้เป็น closure (โหนดที่มีสัญลักษณ์ {}) เชื่อมต่อด้วยขอบสีน้ำเงินไปยังอ็อบเจ็กต์ที่ถูกจับ ตรวจสอบ closures ทั้งหมดเป็นประจำ โดยเฉพาะที่ใช้ในการเรียกแบบอะซิงโครนัส GCD Combine และ SwiftUI ตามสถิติของ Point-Free 90% ของการรั่วไหลในโปรเจ็กต์ Swift เกี่ยวข้องกับ closures ที่จับ self
คำถามที่พบบ่อย
Memory Graph ทำงานได้ทั้งสองภาษาเพราะใช้ Objective-C runtime อ็อบเจ็กต์ Swift ที่เข้ากันได้กับ ObjC (คลาสย่อย NSObject ที่มี @objc) จะแสดงได้อย่างสมบูรณ์ โครงสร้างและคลาส Swift แท้ๆ ที่ไม่มีสะพาน ObjC จะแสดงอย่างจำกัด
อ็อบเจ็กต์ต้องถูกลงทะเบียนใน Objective-C runtime ชนิดค่า Swift (struct, enum) จะไม่แสดง ตรวจสอบให้แน่ใจว่าคลาสสืบทอดจาก NSObject หรือใช้แอตทริบิวต์ @objc เพื่อให้มองเห็นใน Memory Graph
น้ำเงิน — การอ้างอิงแบบ strong คงอ็อบเจ็กต์ไว้ เขียว — การอ้างอิงแบบ weak ไม่ส่งผลต่อวงจรชีวิต เทา — การอ้างอิงแบบ unowned Retain cycle เกิดจากขอบสีน้ำเงินเท่านั้น
การสร้างกราฟ หยุด แอปพลิเคชันชั่วคราว 1–3 วินาที และอาจเพิ่มการใช้หน่วยความจำ Xcode ชั่วคราว 200–500 MB ตัวแอปพลิเคชันเองไม่ช้าลงเพราะการตรวจสอบเกิดขึ้นระหว่างการหยุดที่ breakpoint
Xcode ไม่รองรับการส่งออกกราฟโดยตรง ใช้ภาพหน้าจอสำหรับเอกสารหรือสคริปต์ lldb heap.find_variable สำหรับการดึงข้อมูลเชิงโปรแกรม สำหรับการวิเคราะห์โดยละเอียด ใช้ Instruments Allocations ร่วมกับ heap dump
สรุป
{}เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม