Retain Cycle คือสถานการณ์ใน ARC ที่วัตถุสองหรือมากกว่านั้นอ้างอิงซึ่งกันและกันผ่านการอ้างอิงแบบ strong กลายเป็นวงจบหมุน. ตาม Apple Memory Management Guide, 2026, retain cycle บล็อกการปลอยวัตถุทั้งหมดในวงจรแต่ละตัวมี retain count ≥ 1 แตกต่างจาก การรั่วไหลหน่วยความจำ ใน GC, retain cycle รับประกันว่าวัตถุจะยังมีชีวิตอยู่ตราบใดก็ตามที่ยังมีผู้เข้าร่วมภายนอกอย่างน้อยหนึ่งคนยังมีชีวิตอยู่ — และแม้แต่หลังจากสูญเสียการอ้างอิงภายนอกทั้งหมด, หากวงจรถูกแยกออก.
ประเด็นสำคัญ
Retain Cycle คือสถานการณ์ที่วัตถุสองหรือมากกว่านั้นเป็นเจ้าของซึ่งกันและกันผ่านการอ้างอิงแบบ strong, สร้างกราฟการพึ่งพาที่ปิดที่ ARC ไม่สามารถปลอยวัตถุเหล่านี้ได้ เพราะ retain count ของแต่ละตัวเป็น ≥ 1 เสมอ: วัตถุ A ถือ B, B ถือ A, และตัวนับของพวกมันไม่เคยเป็นศูนย์.
ปัญหาเกิดขึ้นเฉพาะในระบบการนับการอ้างอิง (ARC, MRR) ในการเก็บขยะ (Garbage Collection) ตัวเก็บขยะจะกำหนดความไม่สามารถเข้าถึงได้ผ่านกราฟการอ้างอิงจากชุดราก — วงจรไม่ใช่อุปสรรคใน ARC อย่างไรก็ตาม วงจรเทียบเท่ากับการรั่วไหล เพราะ การปลอยแบบตามการกำหนด โดยการนับไม่สามารถแก้ไขการพึ่งพาแบบวงจรได้.
ตาม WWDC 2012 Session 406, retain cycle เป็นสาเหตุที่พบบ่อยที่สุดของการรั่วไหลหน่วยความจำในแอปพลิเคชัน Objective-C และ Swift สถานการณ์ทั่วไป: ความสัมพันธ์แม่-ลูกกับตัวแทน, closure ที่จับ self, และสถาปัตยกรรมแบบชั้นภูมิที่มีความสัมพันธ์สองทาง.
มาดูสถานการณ์ retain cycle แบบคลาสสิกที่นักพัฒนา iOS ทุกคนพบเจอ การเข้าใจรูปแบบเหล่านี้เป็นพื้นฐานของการเขียน โค้ดที่ปลอดภัย ด้วย ARC.
สถานการณ์แบบคลาสสิก: วัตถุแม่ (เช่น UIViewController) สร้างวัตถุลูก และกลายเป็นตัวแทนของมัน หากทั้งสองใช้การอ้างอิงแบบ strong, retain cycle จะเกิดขึ้น แก้ไข — ตัวแทนต้องเป็น weak.
// ข้อผิดพลาด: retain cycle ผ่าน strong delegate
protocol ChildDelegate: AnyObject { }
class ParentVC: UIViewController, ChildDelegate {
var child: ChildVC?
func showChild() {
child = ChildVC()
child?.delegate = self // Parent → Child (strong)
} // Child → Parent (strong ผ่าน delegate)
} // ⚠️ Retain cycle!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong โดยไว้เดิม
}
// การแก้ไข: weak delegate
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — ไม่ถือไว้
}
ในตัวอย่าง, ParentVC ถือการอ้างอิงแบบ strong ไปยัง ChildVC ผ่านคุณสมบัติ child ChildVC ถือการอ้างอิงแบบ strong ไปยัง ParentVC ผ่าน delegate วงจรปิดสนิท การแก้ไข: weak var delegate — การอ้างอิงไม่เพิ่ม retain count และ ParentVC สามารถปลอยได้.
NSTimer เป็นแหล่งการให้ retain cycle แบบคลาสสิก ตัวจับเวลาถือ target (โดยปกติ self) และ target ถือตัวจับเวลาผ่านคุณสมบัติ แม้ว่าตัวจับเวลาจะเป็นแบบใช้ครั้งเดียว, มันจะไม่ถูกปลอยจนกว่าจะเรียก invalidate แก้ไข: เรียก timer.invalidate() ใน deinit หรือ viewDidDisappear เสมอ.
ในสถาปัตยกรรมที่มีความเป็นเจ้าของแบบลำดับ (ตัวประสานงาน, ตัวเลือกเส้นทาง), วงจรหลายขั้นตอนมักเกิดขึ้น: Coordinator → ViewController → ViewModel → Coordinator (ผ่าน callback) แต่ละการอ้างอิงแบบ strong ในโส้โซ่ต้องได้รับการเลือกอย่างระมัดระวัง — การอ้างอิง weak หนึ่งจุด ในจุดใดจุดหนึ่งในโส้โซ่จะแยกวงจร.
Closure ใน Swift จับตัวแปรภายนอกโดยการอ้างอิงแบบ strong หาก closure ถูกเก็บเป็นคุณสมบัติของวัตถุ (เช่น completion handler) และจับ self, closure นั้นจะสร้าง retain cycle: self → closure → self.
นี่เป็นแหล่งของ retain cycle ที่พบบ่อยที่สุดในการพัฒนา Swift สมัยใหม่ มันเกิดขึ้นโดยนิยนัย — นักพัฒนาอาจไม่สังเกตการจับ self ใน closure, โดยเฉพาะเมื่อใช้ไวยากรณ์แบบย่อโดยไม่มี self ที่ชัดเจน.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ Retain cycle: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ การแก้ไข: capture list กับ weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
capture list [weak self] สร้างการอ้างอิงแบบอ่อนไปยัง self ภายใน closure หาก DownloadService ถูกปลอยก่อนการดำเนินการของ closure, self จะกลายเป็น nil และโค้ดจะออกอย่างปลอดภัยผ่าน guard นี้เป็น รูปแบบมาตรฐาน สำหรับ closure แบบไม่พร้อมใน Swift — ควรใช้ทุกครั้งที่สร้าง closure เป็นคุณสมบัติ.
unowned self เป็นทางเลือกแทน weak self เมื่อ self มีการรับประกันว่าจะมีชีวิตอยู่นานกว่า closure ตัวอย่าง: closure แบบซิงโครนัสที่ทำงานทันที (sorted, filter) ในกรณีเหล่านี้ self มีชีวิตอยู่อย่างแน่นอน และ unowned ปลอดภัย อย่างไรก็ตาม unowned จะคราสห์เมื่อเข้าถึงวัตถุที่ถูกปลอยไปแล้ว — ดังนั้น weak จึงถูกถือว่าเป็น ตัวเลือกที่ปลอดภัยโดยใช้เดิม.
การค้นหา retain cycle ในขั้นต้นเป็นสิ่งสำคัญอย่างยิ่งต่อประสิทธิภาพของแอปพลิเคชัน มาดูเครื่องมือและเทคนิคหลักสำหรับระบุการอ้างอิงแบบวงจรในการพัฒนา iOS.
Xcode Memory Debugger (Debug Memory Graph) เป็นเครื่องมือที่เห็นได้ซึ่งแสดงกราฟของวัตถุในหน่วยความจำพร้อมกับการอ้างอิงของพวกมัน retain cycle จะปรากฏเป็นโส้โซ่ปิดของลูกศรที่แข็งแรง เพื่อเริ่มต้น: คลิกปุ่ม Debug Memory Graph ในแผง Debug area ขณะที่แอปพลิเคชันกำลังทำงาน แต่ละวัตถุจะแสดงให้เห็นประเภท, ที่อยู่ และรายการการอ้างอิง.
Instruments Leaks เป็นโปรไฟเลอร์สำหรับการตรวจจับการรั่วไหลโดยอัตโนมัติ มันบันทึกการจัดสรรคและวิเคราะห์กราฟการอ้างอิงแบบเรียลไทม์ มันตรวจจับไม่เพียงแต่ retain cycle แต่ยังรวมถึงการอ้างอิงที่ถูกลืม, ViewController ที่ไม่ถูกปลอย, และการรั่วไหลอื่นๆ Leaks ชี้ไปยังวัตถุที่แน่นอน และ โส้โซ่การถือไว้.
วิธีที่ง่ายที่สุด คือการเพิ่ม print ใน deinit ของแต่ละคลาสหลัก หาก deinit ไม่ถูกเรียกเมื่อวัตถุควรจะถูกทำลาย, แสดงว่ามี retain cycle วิธีนี้ไม่ต้องการเครื่องมือและมีประสิทธิภาพสำหรับการวินิจฉัยเบื้องต้น.
| เครื่องมือ | ประเภท | เมื่อใหรใช้ |
|---|---|---|
| Memory Debugger | กราฟที่เห็นได้ | ตรวจสอบด้วยตนหลังการเลื่อนไปยังหน้าต่างๆ |
| Instruments Leaks | การวิเคราะห์อัตโนมัติ | การทดสอบการถดถอย, CI |
| deinit print | การบันทึกด้วยตน | การพัฒนา, การตรวจสอบโค้ด |
| Malloc Scribble | แฟลกขณะเริ่มทำงาน | การแก้ไขข้อผิดพลาด use-after-free |
แนวทางที่แนะนำ: ใช้ การบันทึก deinit ระหว่างการพัฒนา, Memory Debugger ระหว่างการทดสอบด้วยตน และ Instruments Leaks ในไฟป์ไลน์ CI/CD สำหรับการตรวจจับการรั่วไหลแบบถดถอยโดยอัตโนมัติ.
การป้องกัน retain cycle ง่ายกว่าการแก้ไขในโปรดัคชัน นี่คือกฏต่างๆ ที่ ลดความเสี่ยง ของการอ้างอิงแบบวงจร.
ตัวแทนและ dataSource ทั้งหมด ต้องเป็น weak กฏต่างนี้ฝังอยูใน UIKit: โปรโตคอลตัวแทนทั้งหมดใน Apple SDK ถูกประกาศด้วยคุณสมบัติ weak (UITableView.delegate, UICollectionView.dataSource) สำหรับโปรโตคอลของคุณเอง, ใช้ weak var delegate: MyDelegate? และให้โปรโตคอลสืบทอดจาก AnyObject.
closure ใดก็ตามที่ถูกเก็บเป็นคุณสมบัติ (completion handler, callback) และจับ self ต้องใช้ [weak self] ใน capture list ข้อยกเว้นคือ closure ที่ทำงานทันที และไม่ได้ถูกเก็บ (sorted, map, filter) สำหรับผู้นั้น unowned self ปลอดภัย.
ในสถาปัตยกรรมที่ซับซ้อน (VIPER, Coordinators, Redux), ติดตามทิศทางของการอ้างอิงแบบ strong เจ้าของถือการอ้างอิงแบบ strong ไปยังผู้ใต้บังคับ แต่ผู้ใต้บังคับต้องอ้างอิงถึงเจ้าของผ่าน weak หรือ unowned เเท่านั้น การไหลข้อมูลแบบทิศทางเดียวช่วยให้การจัดการการอ้างอิงง่ายขึ้น.
// ตัวอย่าง: การตรวจสอบด้วยการบันทึก deinit
class BaseViewController: UIViewController {
deinit {
print("✅ \(type(of: self)) deallocated")
}
}
// การใช้งาน: ViewController ทั้งหมดสืบทอดจาก BaseViewController
class ProfileVC: BaseViewController {
var viewModel: ProfileViewModel?
var onLogout: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
onLogout = { [weak self] in
self?.dismiss(animated: true)
}
}
}
// เมื่อปิด ProfileVC คาดหวังว่าจะเห็น "✅ ProfileVC deallocated" ในคอนโซล
คลาสฐานที่มีการบันทึก deinit จะให้ผลตอบรับทันที หากข้อความไม่ปรากฏเมื่อหน้าจอควรจะปิด, แสดงว่ามี retain cycle ในคลาสนี้ เพิ่มแนวปฏิบัตินี้ในแม่แบบโปรเจ็กต์สำหรับ ViewController ทั้งหมด.
คำถามที่พบบ่อย
Retain cycle เป็นปัญหาเฉพาะของ ARC ที่วงจบของการอ้างอิงแบบ strong บล็อกการปลอย ใน GC, ตัวเก็บขยะวิเคราะห์การสามารถเข้าถึงได้จากชุดราก ไม่ใช่จำนวนการอ้างอิง — ดังนั้นวงจรจึงไม่ใช่การรั่วไหล ใน ARC, อย่างไรก็ตาม, วงจรที่แยกออกใดๆ เป็นการรั่วไหลที่แน่นอน.
การอ้างอิงแบบ weak ไม่ได้เพิ่ม retain count ของวัตถุ หากคุณแทนที่การอ้างอิงแบบ strong หนึ่งในวงจรด้วย weak, retain count ของแต่ละวัตถุจะสามารถกลายเป็นศูนย์ได้ หลังจากที่วัตถุถูกปลอย, การอ้างอิงแบบ weak จะถูกตั้งเป็น nil โดยอัตโนมัติ, ป้องกันการเข้าถึงหน่วยความจำที่ถูกปลอยไปแล้ว.
ได้, retain cycle สามารถรวมวัตถุจำนวนเท่าใดก็ได้: A → B → C → A เพื่อแยกมัน, คุณแค่ต้อง แยกหนึ่งจุดเชื่อมต่อ ในวงจร — แทนที่การอ้างอิงแบบ strong ใดก็ได้ด้วย weak หรือ unowned เครื่องมือจะแสดงกราฟทั้งหมด, ไม่ใช่แค่คู่ของวัตถุ.
GCD (Grand Central Dispatch) ไม่เก็ป closure ไว้หลังจากการดำเนินการ DispatchWorkItem ถูกดำเนินการและปลอย, แม้ว่า closure จะจับ self retain cycle เกิดขึ้นเมื่อ closure ถูกเก็บเป็นคุณสมบัติ (completion handler ในคลาส) เท่านั้น, ไม่ใช่เมื่อส่งไปยังคิว.
Instruments Leaks ไม่สามารถค้นหา retain cycle ชั่วคราว (ที่กินเวลาไม่กี่วินาที) หรือการอ้างอิงแบบวงจรในวัตถุ C/C++ ผ่าน bridging สำหรับการตรวจสอบอย่างสมบูรณ์, ใช้ Memory Debugger ด้วยตนพร้อมกับการบันทึก deinit ของวัตถุหลักทั้งหมดในซีน.
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม