Retain Cycle — สาระ, สาเหตุการเกิดขึ้น และการกำจัดในการพัฒนาแอปพลิเคชัน

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-03-29 เวลาอ่าน: 8 นาที

Retain Cycle คือสถานการณ์ใน ARC ที่วัตถุสองหรือมากกว่านั้นอ้างอิงซึ่งกันและกันผ่านการอ้างอิงแบบ strong กลายเป็นวงจบหมุน. ตาม Apple Memory Management Guide, 2026, retain cycle บล็อกการปลอยวัตถุทั้งหมดในวงจรแต่ละตัวมี retain count ≥ 1 แตกต่างจาก การรั่วไหลหน่วยความจำ ใน GC, retain cycle รับประกันว่าวัตถุจะยังมีชีวิตอยู่ตราบใดก็ตามที่ยังมีผู้เข้าร่วมภายนอกอย่างน้อยหนึ่งคนยังมีชีวิตอยู่ — และแม้แต่หลังจากสูญเสียการอ้างอิงภายนอกทั้งหมด, หากวงจรถูกแยกออก.

ประเด็นสำคัญ

  • Retain Cycle — โส้องโซ่ของการอ้างอิงแบบ strong ที่ปิดที่ ARC ไม่สามารถปลอยวัตถุได้
  • สาเหตุ — วัตถุสองหรือมากกว่านั้นถือการอ้างอิงแบบ strong ซึ่งกันและกัน, ไม่สามารถทำให้ retain count เป็นศูนย์ได้
  • ผลสะท้อน — หน่วยความจำรั่วไหล: วัตถุจะอยู่ในหน่วยความจำตลอดไป, การใช้ RAM เพิ่มขึ้น
  • แก้ไข — แทนที่การอ้างอิงแบบ strong หนึ่งในวงจรด้วย weak หรือ unowned
  • การวินิจฉัย — Xcode Memory Debugger, Instruments Leaks, Debug Memory Graph

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

มาดูสถานการณ์ retain cycle แบบคลาสสิกที่นักพัฒนา iOS ทุกคนพบเจอ การเข้าใจรูปแบบเหล่านี้เป็นพื้นฐานของการเขียน โค้ดที่ปลอดภัย ด้วย ARC.

Parent-Child กับตัวแทน

สถานการณ์แบบคลาสสิก: วัตถุแม่ (เช่น UIViewController) สร้างวัตถุลูก และกลายเป็นตัวแทนของมัน หากทั้งสองใช้การอ้างอิงแบบ strong, retain cycle จะเกิดขึ้น แก้ไข — ตัวแทนต้องเป็น weak.

swift
// ข้อผิดพลาด: 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

NSTimer เป็นแหล่งการให้ retain cycle แบบคลาสสิก ตัวจับเวลาถือ target (โดยปกติ self) และ target ถือตัวจับเวลาผ่านคุณสมบัติ แม้ว่าตัวจับเวลาจะเป็นแบบใช้ครั้งเดียว, มันจะไม่ถูกปลอยจนกว่าจะเรียก invalidate แก้ไข: เรียก timer.invalidate() ใน deinit หรือ viewDidDisappear เสมอ.

สถาปัตยกรรมแบบชั้นภูมิ

ในสถาปัตยกรรมที่มีความเป็นเจ้าของแบบลำดับ (ตัวประสานงาน, ตัวเลือกเส้นทาง), วงจรหลายขั้นตอนมักเกิดขึ้น: Coordinator → ViewController → ViewModel → Coordinator (ผ่าน callback) แต่ละการอ้างอิงแบบ strong ในโส้โซ่ต้องได้รับการเลือกอย่างระมัดระวัง — การอ้างอิง weak หนึ่งจุด ในจุดใดจุดหนึ่งในโส้โซ่จะแยกวงจร.

Retain Cycle ใน Closure ของ Swift

Closure ใน Swift จับตัวแปรภายนอกโดยการอ้างอิงแบบ strong หาก closure ถูกเก็บเป็นคุณสมบัติของวัตถุ (เช่น completion handler) และจับ self, closure นั้นจะสร้าง retain cycle: self → closure → self.

นี่เป็นแหล่งของ retain cycle ที่พบบ่อยที่สุดในการพัฒนา Swift สมัยใหม่ มันเกิดขึ้นโดยนิยนัย — นักพัฒนาอาจไม่สังเกตการจับ self ใน closure, โดยเฉพาะเมื่อใช้ไวยากรณ์แบบย่อโดยไม่มี self ที่ชัดเจน.

swift
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 ใน Closure

unowned self เป็นทางเลือกแทน weak self เมื่อ self มีการรับประกันว่าจะมีชีวิตอยู่นานกว่า closure ตัวอย่าง: closure แบบซิงโครนัสที่ทำงานทันที (sorted, filter) ในกรณีเหล่านี้ self มีชีวิตอยู่อย่างแน่นอน และ unowned ปลอดภัย อย่างไรก็ตาม unowned จะคราสห์เมื่อเข้าถึงวัตถุที่ถูกปลอยไปแล้ว — ดังนั้น weak จึงถูกถือว่าเป็น ตัวเลือกที่ปลอดภัยโดยใช้เดิม.

วิธีค้นหา retain cycle: เครื่องมือวินิจฉัย

การค้นหา retain cycle ในขั้นต้นเป็นสิ่งสำคัญอย่างยิ่งต่อประสิทธิภาพของแอปพลิเคชัน มาดูเครื่องมือและเทคนิคหลักสำหรับระบุการอ้างอิงแบบวงจรในการพัฒนา iOS.

Xcode Memory Debugger

Xcode Memory Debugger (Debug Memory Graph) เป็นเครื่องมือที่เห็นได้ซึ่งแสดงกราฟของวัตถุในหน่วยความจำพร้อมกับการอ้างอิงของพวกมัน retain cycle จะปรากฏเป็นโส้โซ่ปิดของลูกศรที่แข็งแรง เพื่อเริ่มต้น: คลิกปุ่ม Debug Memory Graph ในแผง Debug area ขณะที่แอปพลิเคชันกำลังทำงาน แต่ละวัตถุจะแสดงให้เห็นประเภท, ที่อยู่ และรายการการอ้างอิง.

Instruments Leaks

Instruments Leaks เป็นโปรไฟเลอร์สำหรับการตรวจจับการรั่วไหลโดยอัตโนมัติ มันบันทึกการจัดสรรคและวิเคราะห์กราฟการอ้างอิงแบบเรียลไทม์ มันตรวจจับไม่เพียงแต่ retain cycle แต่ยังรวมถึงการอ้างอิงที่ถูกลืม, ViewController ที่ไม่ถูกปลอย, และการรั่วไหลอื่นๆ Leaks ชี้ไปยังวัตถุที่แน่นอน และ โส้โซ่การถือไว้.

การบันทึก deinit

วิธีที่ง่ายที่สุด คือการเพิ่ม 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 และแนวปฏิบัติที่ดีที่สุด

การป้องกัน retain cycle ง่ายกว่าการแก้ไขในโปรดัคชัน นี่คือกฏต่างๆ ที่ ลดความเสี่ยง ของการอ้างอิงแบบวงจร.

กฏตัวแทน weak

ตัวแทนและ dataSource ทั้งหมด ต้องเป็น weak กฏต่างนี้ฝังอยูใน UIKit: โปรโตคอลตัวแทนทั้งหมดใน Apple SDK ถูกประกาศด้วยคุณสมบัติ weak (UITableView.delegate, UICollectionView.dataSource) สำหรับโปรโตคอลของคุณเอง, ใช้ weak var delegate: MyDelegate? และให้โปรโตคอลสืบทอดจาก AnyObject.

Capture list ใน closure

closure ใดก็ตามที่ถูกเก็บเป็นคุณสมบัติ (completion handler, callback) และจับ self ต้องใช้ [weak self] ใน capture list ข้อยกเว้นคือ closure ที่ทำงานทันที และไม่ได้ถูกเก็บ (sorted, map, filter) สำหรับผู้นั้น unowned self ปลอดภัย.

การตรวจสอบสถาปัตยกรรม

ในสถาปัตยกรรมที่ซับซ้อน (VIPER, Coordinators, Redux), ติดตามทิศทางของการอ้างอิงแบบ strong เจ้าของถือการอ้างอิงแบบ strong ไปยังผู้ใต้บังคับ แต่ผู้ใต้บังคับต้องอ้างอิงถึงเจ้าของผ่าน weak หรือ unowned เเท่านั้น การไหลข้อมูลแบบทิศทางเดียวช่วยให้การจัดการการอ้างอิงง่ายขึ้น.

swift
// ตัวอย่าง: การตรวจสอบด้วยการบันทึก 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 แตกต่างจากการรั่วไหลหน่วยความจำใน GC อย่างไร?

Retain cycle เป็นปัญหาเฉพาะของ ARC ที่วงจบของการอ้างอิงแบบ strong บล็อกการปลอย ใน GC, ตัวเก็บขยะวิเคราะห์การสามารถเข้าถึงได้จากชุดราก ไม่ใช่จำนวนการอ้างอิง — ดังนั้นวงจรจึงไม่ใช่การรั่วไหล ใน ARC, อย่างไรก็ตาม, วงจรที่แยกออกใดๆ เป็นการรั่วไหลที่แน่นอน.

การอ้างอิงแบบ weak แยกวงจร retain cycle ได้อย่างไร?

การอ้างอิงแบบ weak ไม่ได้เพิ่ม retain count ของวัตถุ หากคุณแทนที่การอ้างอิงแบบ strong หนึ่งในวงจรด้วย weak, retain count ของแต่ละวัตถุจะสามารถกลายเป็นศูนย์ได้ หลังจากที่วัตถุถูกปลอย, การอ้างอิงแบบ weak จะถูกตั้งเป็น nil โดยอัตโนมัติ, ป้องกันการเข้าถึงหน่วยความจำที่ถูกปลอยไปแล้ว.

retain cycle สามารถประกอบด้วยวัตถุสามหรือมากกว่านั้นได้หรือไม่?

ได้, retain cycle สามารถรวมวัตถุจำนวนเท่าใดก็ได้: A → B → C → A เพื่อแยกมัน, คุณแค่ต้อง แยกหนึ่งจุดเชื่อมต่อ ในวงจร — แทนที่การอ้างอิงแบบ strong ใดก็ได้ด้วย weak หรือ unowned เครื่องมือจะแสดงกราฟทั้งหมด, ไม่ใช่แค่คู่ของวัตถุ.

ทำไม GCD DispatchWorkItem ถึงไม่สร้าง retain cycle?

GCD (Grand Central Dispatch) ไม่เก็ป closure ไว้หลังจากการดำเนินการ DispatchWorkItem ถูกดำเนินการและปลอย, แม้ว่า closure จะจับ self retain cycle เกิดขึ้นเมื่อ closure ถูกเก็บเป็นคุณสมบัติ (completion handler ในคลาส) เท่านั้น, ไม่ใช่เมื่อส่งไปยังคิว.

retain cycle ประเภทใดที่ Instruments ตรวจจับไม่ได้?

Instruments Leaks ไม่สามารถค้นหา retain cycle ชั่วคราว (ที่กินเวลาไม่กี่วินาที) หรือการอ้างอิงแบบวงจรในวัตถุ C/C++ ผ่าน bridging สำหรับการตรวจสอบอย่างสมบูรณ์, ใช้ Memory Debugger ด้วยตนพร้อมกับการบันทึก deinit ของวัตถุหลักทั้งหมดในซีน.

สรุป

  • Retain Cycle — โส้โซ่ของการอ้างอิงแบบ strong ที่ปิดกั้นการปลอยวัตถุใน ARC
  • สาเหตุ — ตัวแทนที่มีการอ้างอิงแบบ strong, closure ที่จับ self, ความสัมพันธ์แม่-ลูกแบบสองทาง
  • แก้ไข — แทนที่การอ้างอิงแบบ strong หนึ่งด้วย weak หรือ unowned ช่วยแยกวงจร
  • Closure — completion handler ที่ถูกเก็บต้องใช้ [weak self] เสมอ
  • ตัวแทน — ต้องเป็น weak; โปรโตคอลตัวแทนต้องสืบทอดจาก AnyObject
  • การตรวจจับ — Xcode Memory Debugger, Instruments Leaks, การบันทึก deinit
  • การป้องกัน — การไหลข้อมูลแบบทิศทางเดียว, weak ตัวแทน, capture list, คลาสฐานที่มี deinit

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม