دورة الاحتفاظ (Retain Cycle) هي حالة في ARC حيث يشير كائنان أو أكثر إلى بعضهم البعض من خلال مراجع قوية، مشكّلين حلقة مغلقة. وفقًا لـApple Memory Management Guide, 2026، تعمل دورة الاحتفاظ على منع تحرير جميع الكائنات في الدورة لأن كل واحد لديه retain count ≥ 1. وعلى عكس تسرب الذاكرة في GC، فإن دورة الاحتفاظ تضمن بقاء الكائنات حية طالما وجد مشارك خارجي واحد على الأقل في الدورة — وحتى بعد فقدان جميع المراجع الخارجية، إذا كانت الدورة معزولة.
النقاط الرئيسية
Retain Cycle هي حالة يمتلك فيها كائنان أو أكثر بعضهم البعض من خلال مراجع قوية، منشئين رسمًا بيانيًا مغلقًا للتبعيات. لا يستطيع ARC تحرير أي من هذه الكائنات لأن عد الاحتفاظ لكل واحد هو دائمًا ≥ 1: الكائن A يحتفظ بـ B، و B يحتفظ بـ A، وعداداتهما لا تصل إلى الصفر أبدًا.
تنشأ المشكلة حصريًا في أنظمة عد المراجع (ARC، MRR). في جمع القمامة (Garbage Collection)، يحدد المجمع عدم الوصولية من خلال رسم المراجع من مجموعة الجذر — الدورات ليست عائقًا. في ARC، على العكس من ذلك، الدورة تعادل تسربًا، لأن التحرير الحتمي بالعد لا يستطيع حل التبعيات الدائرية.
وفقًا لـWWDC 2012 Session 406، تعتبر دورة الاحتفاظ السبب الأكثر شيوعًا لتسرب الذاكرة في تطبيقات Objective-C و Swift. السيناريوهات النموذجية: علاقات أب-ابن مع المفوضين، الإغلاقات التي تلتقط self، والبنى الطبقية ذات العلاقات ثنائية الاتجاه.
دعنا نستعرض السيناريوهات الكلاسيكية لدورة الاحتفاظ التي يواجهها كل مطور iOS. فهم هذه الأنماط هو أساس كتابة كود آمن مع ARC.
السيناريو الكلاسيكي: كائن أب (UIViewController على سبيل المثال) ينشئ كائنًا ابنًا ويصبح مفوضه. إذا استخدم كلاهما مراجع قوية، تنشأ دورة احتفاظ. الحل — يجب أن يكون المفوض weak.
// خطأ: دورة احتفاظ عبر مفوض قوي
protocol ChildDelegate: AnyObject { }
class ParentVC: UIViewController, ChildDelegate {
var child: ChildVC?
func showChild() {
child = ChildVC()
child?.delegate = self // Parent → Child (strong)
} // Child → Parent (strong عبر delegate)
} // ⚠️ دورة احتفاظ!
class ChildVC: UIViewController {
var delegate: ChildDelegate? // ❌ strong افتراضيًا
}
// الإصلاح: مفوض weak
class ChildVC: UIViewController {
weak var delegate: ChildDelegate? // ✅ weak — لا يحتفظ
}
في المثال، ParentVC يحتفظ بمرجع قوي إلى ChildVC من خلال خاصية child. ChildVC يحتفظ بمرجع قوي إلى ParentVC من خلال delegate. الدورة مغلقة. التصليح: weak var delegate — المرجع لا يزيد عد الاحتفاظ، ويمكن تحرير ParentVC.
NSTimer هو مصدر كلاسيكي لدورات الاحتفاظ. المؤقت يحتفظ بهدفه (عادة self)، والهدف يحتفظ بالمؤقت من خلال خاصية. حتى لو كان المؤقت لمرة واحدة، فلن يتحرر حتى يتم استدعاء invalidate. الحل: دائمًا قم باستدعاء timer.invalidate() في deinit أو viewDidDisappear.
في البنى ذات الملكية المتسلسلة (المنسقون، الموجهون)، تنشأ غالبًا دورات متعددة الخطوات: Coordinator → ViewController → ViewModel → Coordinator (عبر استرجاع). يجب اختيار كل مرجع قوي في السلسلة بوعي — مرجع weak واحد في أي حلقة يكسر الدورة.
تلتقط الإغلاقات في Swift المتغيرات الخارجية بمرجع قوي. إذا تم تخزين إغلاق كخاصية لكائن (على سبيل المثال، completion handler) والتقط self، فإنه ينشئ دورة احتفاظ: self → closure → self.
هذا هو المصدر الأكثر شيوعًا لدورات الاحتفاظ في تطوير Swift الحديث. يحدث بشكل ضمني — قد لا يلاحظ المطور التقاط self في إغلاق، خاصة عند استخدام بناء مختصر بدون self صريح.
class DownloadService {
var onComplete: ((Data) -> Void)?
var result: Data?
func startDownload() {
// ❌ دورة احتفاظ: self → onComplete → self
onComplete = { data in
self.result = data
self.notifyUI()
}
// ✅ الإصلاح: قائمة الالتقاط مع weak self
onComplete = { [weak self] data in
guard let self else { return }
self.result = data
self.notifyUI()
}
}
func notifyUI() { }
}
ينشئ قائمة الالتقاط [weak self] مرجعًا ضعيفًا إلى self داخل الإغلاق. إذا تم تحرير DownloadService قبل تنفيذ الإغلاق، يصبح self nil، ويخرج الكود بأمان من خلال guard. هذا نمط قياسي للإغلاقات غير المتزامنة في Swift — يجب استخدامه كلما تم تخزين إغلاق كخاصية.
unowned self هو بديل لـ weak self عندما يكون self مضمونًا أنه يعيش أطول من الإغلاق. مثال: إغلاقات متزامنة تُنفذ فورًا (sorted، filter). في هذه الحالات، self بالتأكيد موجود، و unowned آمن. ولكن unowned يتعطل عند الوصول إلى كائن محرر — لذا يعتبر weak الخيار الآمن افتراضيًا.
يعتبر اكتشاف دورة الاحتفاظ في مرحلة مبكرة أمرًا حاسمًا لأداء التطبيق. دعنا نستعرض الأدوات والتقنيات الرئيسية لتحديد المراجع الدورية في تطوير iOS.
Xcode Memory Debugger (Debug Memory Graph) هي أداة مرئية تعرض رسم الكائنات في الذاكرة مع مراجعها. تظهر دورة الاحتفاظ كسلسلة مغلقة من الأسهم القوية. للتشغيل: اضغط على زر Debug Memory Graph في لوحة Debug area أثناء تشغيل التطبيق. يظهر كل كائن مع نوعه وعنوانه وقائمة مراجعه.
Instruments Leaks هو بروفايلر للكشف التلقائي عن التسربات. يسجل التخصيصات ويحلل رسم المراجع في الوقت الفعلي. يكتشف ليس فقط دورات الاحتفاظ ولكن أيضًا المراجع المنسية ومتحكمات العرض غير المحررة والتسربات الأخرى. يشير Leaks إلى الكائن الدقيق وسلسلة الاحتفاظ.
أبسط طريقة — إضافة print في deinit لكل فئة رئيسية. إذا لم يتم استدعاء deinit عندما يتوقع تدمير الكائن، فهناك دورة احتفاظ. هذه الطريقة لا تتطلب أدواتًا وهي فعّالة للتشخيص الأولي.
| الأداة | النوع | متى تستخدم |
|---|---|---|
| Memory Debugger | رسم مرئي | التحقق اليدوي بعد التنقل |
| Instruments Leaks | تحليل تلقائي | اختبارات الارتداد، CI |
| deinit print | تسجيل يدوي | التطوير، مراجعة الكود |
| Malloc Scribble | علامة وقت التشغيل | تصحيح use-after-free |
النهج الموصى به: استخدام تسجيل deinit أثناء التطوير، Memory Debugger أثناء الاختبار اليدوي، و Instruments Leaks في خط التنفيذ CI/CD للكشف التلقائي عن تسربات الارتداد.
منع دورات الاحتفاظ أسهل من إصلاحها في الإنتاج. إليك بعض القواعد التي تقلل من خطر المراجع الدورية.
جميع المفوضين و dataSources يجب أن تكون weak. هذه القاعدة مضمنة في UIKit: جميع بروتوكولات المفوضين في SDK أبل معلنة بخاصيات weak (UITableView.delegate، UICollectionView.dataSource). لبروتوكولاتك الخاصة، استخدم weak var delegate: MyDelegate? وأورث البروتوكول من AnyObject.
أي إغلاق يتم تخزينه كخاصية (completion handler، callback) ويلتقط self يجب أن يستخدم [weak self] في قائمة الالتقاط. الاستثناء هو الإغلاقات التي تُنفذ فورًا ولا تُخزن (sorted، map، filter). في تلك الحالات، unowned self آمن.
في البنى المعقدة (VIPER، Coordinators، Redux)، تتبع اتجاه المراجع القوية. المالك يحتفظ بمرجع قوي إلى التابع، ولكن التابع يجب أن يشير إلى المالك فقط من خلال 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 ملاحظات فورية. إذا لم يظهر الرسالة عندما يتوقع إغلاق الشاشة، فهناك دورة احتفاظ في هذه الفئة. أضف هذه الممارسة إلى قالب المشروع لجميع ViewControllers.
الأسئلة الشائعة
Retain cycle هي مشكلة خاصة بـ ARC حيث تعمل حلقة مغلقة من المراجع القوية على منع التحرير. في GC، يحلل المجمع الوصولية من مجموعة الجذر، ليس عداد المراجع — ولذا فالدورات ليست تسربًا. في ARC، على العكس، أي دورة معزولة هي تسرب مضمون.
المرجع weak لا يزيد عد احتفاظ الكائن. إذا استبدلت إحدى المراجع القوية في دورة بـ weak، يمكن لعد احتفاظ كل كائن أن يصل إلى الصفر. بعد تحرير الكائن، يتم تعيين المرجع weak تلقائيًا إلى nil، مما يمنع الوصول إلى الذاكرة المحررة.
نعم، يمكن أن تشمل دورة الاحتفاظ عددًا أيًا من الكائنات: A → B → C → A. لكسرها، تحتاج فقط إلى كسر حلقة واحدة في الدورة — استبدال أي مرجع قوي بـ weak أو unowned. تظهر الأدوات الرسم البياني الكامل، ليس فقط أزواج الكائنات.
GCD (Grand Central Dispatch) لا يخزن الإغلاق بعد التنفيذ. يتم تنفيذ DispatchWorkItem وتحريره، حتى لو كان الإغلاق يلتقط self. تنشأ دورة الاحتفاظ فقط عندما يتم تخزين إغلاق كخاصية (completion handler في فئة)، ليس عندما يتم تمريره إلى طابور.
Instruments Leaks لا يجد دائمًا دورات الاحتفاظ المؤقتة (تدوم ثواني) ولا المراجع الدورية في كائنات C/C++ عبر الجسر. للتحقق الكامل، استخدم Memory Debugger يدويًا مع تسجيل deinit لجميع الكائنات الرئيسية في المشهد.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا