دورة الاحتفاظ — الجوهر، أسباب الظهور والإزالة في تطوير التطبيقات

المؤلف: IT Sectr نُشر: 2026-03-29 وقت القراءة: 8 دق

دورة الاحتفاظ (Retain Cycle) هي حالة في ARC حيث يشير كائنان أو أكثر إلى بعضهم البعض من خلال مراجع قوية، مشكّلين حلقة مغلقة. وفقًا لـApple Memory Management Guide, 2026، تعمل دورة الاحتفاظ على منع تحرير جميع الكائنات في الدورة لأن كل واحد لديه retain count ≥ 1. وعلى عكس تسرب الذاكرة في GC، فإن دورة الاحتفاظ تضمن بقاء الكائنات حية طالما وجد مشارك خارجي واحد على الأقل في الدورة — وحتى بعد فقدان جميع المراجع الخارجية، إذا كانت الدورة معزولة.

النقاط الرئيسية

  • دورة الاحتفاظ — سلسلة مغلقة من المراجع القوية تمنع ARC من تحرير الكائنات
  • السبب — كائنان (أو أكثر) يحتفظان بمراجع قوية لبعضهما البعض، مما يجعل تصفير عد الاحتفاظ مستحيلًا
  • النتيجة — تسرب الذاكرة: تبقى الكائنات في الذاكرة إلى الأبد، يزداد استهلاك RAM
  • الحل — استبدال إحدى المراجع القوية في الدورة بـ weak أو unowned
  • التشخيص — Xcode Memory Debugger، Instruments Leaks، Debug Memory Graph

ما هو Retain Cycle؟

Retain Cycle هي حالة يمتلك فيها كائنان أو أكثر بعضهم البعض من خلال مراجع قوية، منشئين رسمًا بيانيًا مغلقًا للتبعيات. لا يستطيع ARC تحرير أي من هذه الكائنات لأن عد الاحتفاظ لكل واحد هو دائمًا ≥ 1: الكائن A يحتفظ بـ B، و B يحتفظ بـ A، وعداداتهما لا تصل إلى الصفر أبدًا.

تنشأ المشكلة حصريًا في أنظمة عد المراجع (ARC، MRR). في جمع القمامة (Garbage Collection)، يحدد المجمع عدم الوصولية من خلال رسم المراجع من مجموعة الجذر — الدورات ليست عائقًا. في ARC، على العكس من ذلك، الدورة تعادل تسربًا، لأن التحرير الحتمي بالعد لا يستطيع حل التبعيات الدائرية.

وفقًا لـWWDC 2012 Session 406، تعتبر دورة الاحتفاظ السبب الأكثر شيوعًا لتسرب الذاكرة في تطبيقات Objective-C و Swift. السيناريوهات النموذجية: علاقات أب-ابن مع المفوضين، الإغلاقات التي تلتقط self، والبنى الطبقية ذات العلاقات ثنائية الاتجاه.

أمثلة على دورة الاحتفاظ في تطوير iOS

دعنا نستعرض السيناريوهات الكلاسيكية لدورة الاحتفاظ التي يواجهها كل مطور iOS. فهم هذه الأنماط هو أساس كتابة كود آمن مع ARC.

أب-ابن مع مفوض

السيناريو الكلاسيكي: كائن أب (UIViewController على سبيل المثال) ينشئ كائنًا ابنًا ويصبح مفوضه. إذا استخدم كلاهما مراجع قوية، تنشأ دورة احتفاظ. الحل — يجب أن يكون المفوض weak.

swift
// خطأ: دورة احتفاظ عبر مفوض قوي
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 ودورة الاحتفاظ

NSTimer هو مصدر كلاسيكي لدورات الاحتفاظ. المؤقت يحتفظ بهدفه (عادة self)، والهدف يحتفظ بالمؤقت من خلال خاصية. حتى لو كان المؤقت لمرة واحدة، فلن يتحرر حتى يتم استدعاء invalidate. الحل: دائمًا قم باستدعاء timer.invalidate() في deinit أو viewDidDisappear.

البنى الطبقية

في البنى ذات الملكية المتسلسلة (المنسقون، الموجهون)، تنشأ غالبًا دورات متعددة الخطوات: Coordinator → ViewController → ViewModel → Coordinator (عبر استرجاع). يجب اختيار كل مرجع قوي في السلسلة بوعي — مرجع weak واحد في أي حلقة يكسر الدورة.

دورة الاحتفاظ في إغلاقات Swift

تلتقط الإغلاقات في Swift المتغيرات الخارجية بمرجع قوي. إذا تم تخزين إغلاق كخاصية لكائن (على سبيل المثال، completion handler) والتقط self، فإنه ينشئ دورة احتفاظ: self → closure → self.

هذا هو المصدر الأكثر شيوعًا لدورات الاحتفاظ في تطوير Swift الحديث. يحدث بشكل ضمني — قد لا يلاحظ المطور التقاط self في إغلاق، خاصة عند استخدام بناء مختصر بدون self صريح.

swift
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 في الإغلاقات

unowned self هو بديل لـ weak self عندما يكون self مضمونًا أنه يعيش أطول من الإغلاق. مثال: إغلاقات متزامنة تُنفذ فورًا (sorted، filter). في هذه الحالات، self بالتأكيد موجود، و unowned آمن. ولكن unowned يتعطل عند الوصول إلى كائن محرر — لذا يعتبر weak الخيار الآمن افتراضيًا.

كيف تكتشف دورة الاحتفاظ: أدوات التشخيص

يعتبر اكتشاف دورة الاحتفاظ في مرحلة مبكرة أمرًا حاسمًا لأداء التطبيق. دعنا نستعرض الأدوات والتقنيات الرئيسية لتحديد المراجع الدورية في تطوير iOS.

Xcode Memory Debugger

Xcode Memory Debugger (Debug Memory Graph) هي أداة مرئية تعرض رسم الكائنات في الذاكرة مع مراجعها. تظهر دورة الاحتفاظ كسلسلة مغلقة من الأسهم القوية. للتشغيل: اضغط على زر Debug Memory Graph في لوحة Debug area أثناء تشغيل التطبيق. يظهر كل كائن مع نوعه وعنوانه وقائمة مراجعه.

Instruments Leaks

Instruments Leaks هو بروفايلر للكشف التلقائي عن التسربات. يسجل التخصيصات ويحلل رسم المراجع في الوقت الفعلي. يكتشف ليس فقط دورات الاحتفاظ ولكن أيضًا المراجع المنسية ومتحكمات العرض غير المحررة والتسربات الأخرى. يشير Leaks إلى الكائن الدقيق وسلسلة الاحتفاظ.

تسجيل deinit

أبسط طريقة — إضافة print في deinit لكل فئة رئيسية. إذا لم يتم استدعاء deinit عندما يتوقع تدمير الكائن، فهناك دورة احتفاظ. هذه الطريقة لا تتطلب أدواتًا وهي فعّالة للتشخيص الأولي.

الأداةالنوعمتى تستخدم
Memory Debuggerرسم مرئيالتحقق اليدوي بعد التنقل
Instruments Leaksتحليل تلقائياختبارات الارتداد، CI
deinit printتسجيل يدويالتطوير، مراجعة الكود
Malloc Scribbleعلامة وقت التشغيلتصحيح use-after-free

النهج الموصى به: استخدام تسجيل deinit أثناء التطوير، Memory Debugger أثناء الاختبار اليدوي، و Instruments Leaks في خط التنفيذ CI/CD للكشف التلقائي عن تسربات الارتداد.

الوقاية من دورة الاحتفاظ وأفضل الممارسات

منع دورات الاحتفاظ أسهل من إصلاحها في الإنتاج. إليك بعض القواعد التي تقلل من خطر المراجع الدورية.

قاعدة المفوض weak

جميع المفوضين و 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. يبسط تدفق البيانات أحادي الاتجاه إدارة المراجع.

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 ملاحظات فورية. إذا لم يظهر الرسالة عندما يتوقع إغلاق الشاشة، فهناك دورة احتفاظ في هذه الفئة. أضف هذه الممارسة إلى قالب المشروع لجميع ViewControllers.

الأسئلة الشائعة

كيف يختلف retain cycle عن تسرب الذاكرة في GC؟

Retain cycle هي مشكلة خاصة بـ ARC حيث تعمل حلقة مغلقة من المراجع القوية على منع التحرير. في GC، يحلل المجمع الوصولية من مجموعة الجذر، ليس عداد المراجع — ولذا فالدورات ليست تسربًا. في ARC، على العكس، أي دورة معزولة هي تسرب مضمون.

كيف تكسر المرجع weak دورة الاحتفاظ؟

المرجع weak لا يزيد عد احتفاظ الكائن. إذا استبدلت إحدى المراجع القوية في دورة بـ weak، يمكن لعد احتفاظ كل كائن أن يصل إلى الصفر. بعد تحرير الكائن، يتم تعيين المرجع weak تلقائيًا إلى nil، مما يمنع الوصول إلى الذاكرة المحررة.

هل يمكن أن تتكون دورة احتفاظ من ثلاثة كائنات أو أكثر؟

نعم، يمكن أن تشمل دورة الاحتفاظ عددًا أيًا من الكائنات: A → B → C → A. لكسرها، تحتاج فقط إلى كسر حلقة واحدة في الدورة — استبدال أي مرجع قوي بـ weak أو unowned. تظهر الأدوات الرسم البياني الكامل، ليس فقط أزواج الكائنات.

لماذا لا ينشئ GCD DispatchWorkItem دورة احتفاظ؟

GCD (Grand Central Dispatch) لا يخزن الإغلاق بعد التنفيذ. يتم تنفيذ DispatchWorkItem وتحريره، حتى لو كان الإغلاق يلتقط self. تنشأ دورة الاحتفاظ فقط عندما يتم تخزين إغلاق كخاصية (completion handler في فئة)، ليس عندما يتم تمريره إلى طابور.

ما هي أنواع دورات الاحتفاظ التي لا يكتشفها Instruments؟

Instruments Leaks لا يجد دائمًا دورات الاحتفاظ المؤقتة (تدوم ثواني) ولا المراجع الدورية في كائنات C/C++ عبر الجسر. للتحقق الكامل، استخدم Memory Debugger يدويًا مع تسجيل deinit لجميع الكائنات الرئيسية في المشهد.

الملخص

  • دورة الاحتفاظ — سلسلة مغلقة من المراجع القوية تعمل على منع تحرير الكائنات في ARC
  • الأسباب — مفوضون بمراجع قوية، إغلاقات تلتقط self، علاقات أب-ابن ثنائية الاتجاه
  • الحل — استبدال مرجع قوي واحد بـ weak أو unowned يكسر الدورة
  • الإغلاقات — يجب أن تستخدم completion handlers المخزنة دائمًا [weak self]
  • المفوضون — دائمًا weak؛ يجب أن يرث بروتوكول المفوض من AnyObject
  • الكشف — Xcode Memory Debugger، Instruments Leaks، تسجيل deinit
  • الوقاية — تدفق بيانات أحادي الاتجاه، مفوضون weak، قوائم الالتقاط، فئة أساس مع deinit

سنقوم بتطوير تطبيق جوال جاهز

تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.

مناقشة المشروع

اقرأ أيضًا