Callback — ما هو، دوال الاستدعاء العكسي وكيف تعمل

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

Callback هي دالة تُمرر إلى دالة أخرى كوسيطة ويتم تنفيذها بعد اكتمال عملية غير متزامنة. في تطوير التطبيقات المحمولة، يُستخدم callback لمعالجة نتائج طلبات الشبكة، والعمل مع قواعد البيانات والرسوم المتحركة. وفقًا لوثائق Apple (2025)، فإن الإغلاقات (closures) في Swift هي الشكل الأساسي للـ callback وتُستخدم في URLSession وGCD وCombine. في Android، يتم تنفيذ callback من خلال الواجهات، وتعبيرات Kotlin lambda وListenableFuture.

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

  • Callback — دالة استدعاء عكسي تُمرر كوسيطة للتنفيذ غير المتزامن.
  • Swift يستخدم الإغلاقات (closures) مع الكلمة المفتاحية @escaping للـ callback.
  • Kotlin يستخدم تعبيرات lambda ودوال الترتيب الأعلى للـ callback.
  • Retain cycle — تسرب ذاكري عند التقاط self في callback على iOS.
  • Callback Hell — مشكلة الـ callback المتداخلة، التي تُحل باستخدام async/await والكوروتينات.

ما هو الـ Callback؟

Callback (دالة الاستدعاء العكسي) هو كود قابل للتنفيذ يُمرر إلى دالة أخرى ويُستدعى بعد اكتمال إجراء معين. في تطوير التطبيقات المحمولة، يُعد callback آلية أساسية للبرمجة غير المتزامنة، مما يسمح بالاستجابة لاكتمال طلبات الشبكة والمؤقتات والرسوم المتحركة وعمليات الإدخال/الإخراج دون حظر الخيط الرئيسي. يوفر Swift وKotlin تراكيب نحوية مدمجة لإنشاء callback — الإغلاقات والتعبيرات lambda على التوالي.

مبدأ عمل الـ Callback

تقبل دالة الترتيب الأعلى دالة أخرى كمعامل وتستدعيها بعد تنفيذ منطقها الرئيسي. يتم إعادة تدفق التحكم إلى المتصل عبر callback، ومن هنا جاء الاسم. في iOS، يُستخدم callback في UIKit (رسوم UIView.animate المتحركة)، وFoundation (URLSession.dataTask)، وCombine (sink). في Android، يُستخدم callback في View.OnClickListener وRetrofit Callback وRoom DAO. تستبدل واجهات برمجة التطبيقات الحديثة بشكل متزايد callback بـ async/await أو الكوروتينات، لكن فهم callback ضروري للعمل مع الكود القديم وواجهات برمجة التطبيقات منخفضة المستوى.

Callback المتزامن وغير المتزامن

يمكن أن يكون callback متزامنًا (يُستدعى فورًا داخل الدالة) أو غير متزامن (يُستدعى لاحقًا من خيط أو قائمة انتظار أخرى). تُستخدم الـ callbacks المتزامنة للترتيب (المقارنات) واجتياز المجموعات. تُستخدم الـ callbacks غير المتزامنة لطلبات الشبكة وقراءة الملفات والعمل مع أجهزة الاستشعار. الفرق مهم جدًا لفهم threading: يتم تنفيذ الـ callback المتزامن في نفس الخيط، بينما يتم تنفيذ الـ callback غير المتزامن في خيط تحدده وحدة التوزيع (DispatchQueue في iOS، Dispatchers في Kotlin).

كيف يعمل الـ Callback في iOS وAndroid؟

آلية الـ callback في كلتا المنصتين تقوم على نفس المبدأ: تمرر دالة ككائن من الدرجة الأولى وتُخزن حتى لحظة التنفيذ. لكن التنفيذ يختلف بسبب اختلاف نماذج اللغات. في iOS، الـ callback هو إغلاق (closure) يلتقط المتغيرات من السياق المحيط. في Android، يتم تنفيذ الـ callback غالبًا من خلال الفئات المجهولة أو تعبيرات Kotlin lambda، التي تُترجم إلى FunctionalInterface.

دورة حياة الـ Callback في iOS

عند استدعاء دالة غير متزامنة، يتم تخزين الإغلاق في heap مع المتغيرات الملتقطة. عند اكتمال العملية، يضع نظام GCD أو OperationQueue الـ callback في قائمة الانتظار المناسبة (قائمة الانتظار الرئيسية أو الخلفية). بعد التنفيذ، يُحذف الـ callback من الذاكرة عند عدم وجود مراجع قوية. تمنع قائمة الالتقاط ([weak self]) الاحتفاظ بالكائن بعد تحريره. بدون قائمة الالتقاط، ينشأ retain cycle حيث يشير الكائن والـ callback إلى بعضهما البعض.

swift
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        completion(.success(data))
    }
    task.resume()
}

// الاستخدام مع [weak self]
fetchData { [weak self] result in
    guard let self else { return }
    switch result {
    case .success(let data):
        self.updateUI(data)
    case .failure(let error):
        self.showError(error)
    }
}

دورة حياة الـ Callback في Android

في Android، يُمرر الـ callback من خلال واجهة أو lambda. عند تنفيذ عملية غير متزامنة عبر ExecutorService أو كوروتين، يُخزن الـ callback في الذاكرة حتى اكتمال العمل في الخلفية. تُترجم تعبيرات Kotlin lambda إلى فئات مجهولة تلتقط المتغيرات الخارجية. يتطلب غياب المراجع الضعيفة في JVM إدارة يدوية: إلغاء الـ callback في onDestroy() أو إلغاء الكوروتينات عبر Job.cancel(). يحل ViewModel وLiveData هذه المشكلة على مستوى المكون المعماري.

kotlin
interface Callback<T> {
    fun onSuccess(data: T)
    fun onError(error: Throwable)
}

class Repository {
    fun loadData(callback: Callback<List<User>>) {
        thread {
            try {
                val result = api.fetchUsers()
                runOnUiThread { callback.onSuccess(result) }
            } catch (e: Exception) {
                runOnUiThread { callback.onError(e) }
            }
        }
    }
}

// الاستخدام مع lambda
repository.loadData(object : Callback<List<User>> {
    override fun onSuccess(data: List<User>) { showUsers(data) }
    override fun onError(error: Throwable) { showError(error.message) }
})

صياغة الـ Callback في Swift وKotlin

يتم تحديد صياغة الـ callback من خلال قدرات اللغة على التعامل مع الدوال ككائنات من الدرجة الأولى. في Swift، تتميز الإغلاقات بصياغة موجزة مع أسماء وسيطات تلقائية ($0, $1). في Kotlin، تدعم تعبيرات lambda أيضًا it لوسيطة واحدة. تظهر الاختلافات في معالجة التقاط المتغيرات (قائمة الالتقاط في Swift مقابل المراجع القابلة للتغيير في Kotlin) والتصنيف (Result مقابل Result).

Callback في Swift: الإغلاقات (closures)

إغلاق Swift هو كتلة كود مستقلة يمكن تمريرها واستخدامها في دالة أخرى. يمكن أن تكون الإغلاقات عامة (مسماة) ومتداخلة وعلى مستوى التعبير. @escaping تميز الإغلاق الذي سيتم تنفيذه بعد عودة الدالة — وهذا شرط إلزامي للـ callbacks غير المتزامنة. بدون @escaping، يمكن تنفيذ الإغلاق فقط داخل جسم الدالة. تسمح صياغة trailing closure بتمرير الإغلاق بعد الأقواس: fetchData { result in ... }.

swift
typealias NetworkResult = (Result<[String: Any], Error>) -> Void

func performRequest(
    url: URL,
    then handler: @escaping NetworkResult
) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        handler(Result {
            guard let json = try JSONSerialization.jsonObject(with: data)
            else { throw NetworkError.invalidData }
            return json as! [String: Any]
        })
    }
    task.resume()
}

performRequest(url: url) { result in
    switch result {
    case .success(let json): process(json)
    case .failure(let error): log(error.localizedDescription)
    }
}

Callback في Kotlin: تعبيرات lambda ودوال الترتيب الأعلى

يدعم Kotlin دوال الترتيب الأعلى التي تقبل دوال أخرى كمعاملات. يُمرر callback في Kotlin عبر معامل من النوع (T) -> Unit أو (T) -> R لقيم الإرجاع. تستبدل دوال suspend في كوروتينات Kotlin الـ callbacks بكود تسلسلي، لكن تبقى الـ callbacks في واجهات برمجة التطبيقات المتوافقة مع Java وAndroid SDK (View.setOnClickListener، TextWatcher). تلتقط تعبيرات Kotlin lambda تلقائيًا متغيرات val، بينما تتطلب متغيرات var أغلفة قابلية التغيير.

kotlin
fun <T, R> processWithCallback(
    input: T,
    transform: (T) -> R,
    onResult: (R) -> Unit
) {
    thread {
        val result = transform(input)
        runOnUiThread { onResult(result) }
    }
}

// مثال مع lambda
processWithCallback(
    input = "Hello",
    transform = { it.length },
    onResult = { length ->
        textView.text = "Length: $length"
    }
)

Retain cycles وتسريبات الذاكرة في الـ Callback

Retain cycle هو حالة يحتفظ فيها كائنان بمراجع قوية لبعضهما البعض، مما يمنع مدير الذاكرة من تحريرهما. في Swift، ينشأ retain cycle عندما يلتقط viewController إغلاقًا ويلتقط الإغلاق self. في Kotlin/Java، يحدث تسرب عندما تمرر Activity فئة داخلية أو lambda إلى عملية خلفية طويلة الأمد. وفقًا لجلسة WWDC 10216 (2024)، فإن الإدارة غير الصحيحة للإغلاقات هي ثالث أكثر أسباب تسرب الذاكرة شيوعًا في تطبيقات iOS.

Retain cycles في Swift

يستخدم Swift العد التلقائي للمراجع (ARC) الذي يحرر الكائن عندما يصل عداد المراجع إلى الصفر. تمنع قائمة الالتقاط [weak self] أو [unowned self] في الإغلاق retain cycles. ينشئ weak self مرجعًا اختياريًا يصبح nil عند تحرير الكائن. يفترض unowned self أن الكائن يعيش أطول من الإغلاق — عند انتهاك هذا الافتراض يحدث crash. يُوصى باستخدام weak self كخيار آمن افتراضي.

swift
class DataController {
    var onDataUpdate: ((String) -> Void)?

    func setupCallback() {
        // Retain cycle!
        onDataUpdate = { text in
            self.process(text)
        }

        // تم التصحيح: [weak self]
        onDataUpdate = { [weak self] text in
            guard let self else { return }
            self.process(text)
        }
    }

    func process(_ input: String) { }
}

تسريبات الذاكرة في Android

في Android، يحدث تسرب الذاكرة بسبب callback عندما تمرر Activity أو Fragment مستمعًا إلى مكون singleton (مثل EventBus أو Service). يسمح WeakReference لمجمع القمامة بتحرير Activity حتى إذا كان هناك مرجع ضعيف إليها. تحل المكونات lifecycle-aware (LiveData، Flow) المشكلة تلقائيًا. يمكن أن تسبب تعبيرات Kotlin lambda التي تلتقط سياق Activity تسريبات أيضًا: تخزن lambda ضمنيًا مرجعًا إلى this.

kotlin
class SafeCallbackManager {
    private val listeners = mutableListOf<WeakReference<(String) -> Unit>>()

    fun addListener(callback: (String) -> Unit) {
        listeners.add(WeakReference(callback))
    }

    fun notifyAll(data: String) {
        val iterator = listeners.iterator()
        while (iterator.hasNext()) {
            val ref = iterator.next().get()
            if (ref != null) ref(data)
            else iterator.remove()
        }
    }
}

// الاستخدام في Fragment
manager.addListener { result ->
    // WeakReference لا يحتفظ بـ Fragment
    updateUI(result)
}

Callback Hell وطرق مكافحته

Callback Hell (المعروف أيضًا باسم هرم الموت) هو حالة تنشئ فيها العديد من الـ callbacks المتداخلة بنية كود عميقة التداخل، يصعب قراءتها وتصحيحها. تتطلب كل خطوة تالية انتظار اكتمال السابقة، مما يؤدي إلى تداخل من 5-10 مستويات. هذه المشكلة مميزة للعمليات غير المتزامنة المتسلسلة: تحميل البيانات → التحليل → الحفظ في قاعدة البيانات → تحديث واجهة المستخدم.

الحلول في Swift: async/await

قدم Swift 5.5 الدوال غير المتزامنة (async/await)، التي تتيح كتابة كود غير متزامن بشكل تسلسلي. AsyncSequence وAsyncStream يستبدلان التكرارات القائمة على callback. يوفر إطار Combine عوامل مثل flatMap وmerge وcombineLatest لتكوين التدفقات غير المتزامنة دون تداخل. ومع ذلك، تبقى الـ callbacks ضرورية للعمل مع واجهات برمجة تطبيقات Objective-C والمكتبات الخارجية دون دعم async.

swift
// Callbacks المتداخلة — Callback Hell
loginUser(credentials) { user in
    fetchProfile(user.id) { profile in
        downloadAvatar(profile.avatarUrl) { image in
            cacheImage(image) { success in
                updateUI(user, profile, image)
            }
        }
    }
}

// async/await — الحل
func loadUserExperience() async throws {
    let user = try await loginUser(credentials)
    let profile = try await fetchProfile(user.id)
    let image = try await downloadAvatar(profile.avatarUrl)
    try await cacheImage(image)
    updateUI(user, profile, image)
}

الحلول في Kotlin: الكوروتينات وFlow

تستبدل كوروتينات Kotlin الـ callbacks بدوال suspend للتنفيذ التسلسلي. يوفر Flow تدفقات باردة مع عوامل مثل map وflatMapConcat وcombine. يسمح CoroutineScope بإلغاء جميع الكوروتينات الجارية عند تدمير المكون. تحتوي Room وRetrofit ومكتبات Jetpack الأخرى على دعم مدمج لدوال suspend، مما يلغي الحاجة إلى الـ callbacks في العمليات القياسية.

kotlin
// Callbacks المتسلسلة — Callback Hell
api.login(credentials) { user ->
    api.fetchProfile(user.id) { profile ->
        api.download(profile.avatarUrl) { bytes ->
            file.save(bytes) { result ->
                textView.text = result.toString()
            }
        }
    }
}

// الكوروتينات — الحل
suspend fun loadUserData() {
    val user = withContext(Dispatchers.IO) { api.login(credentials) }
    val profile = withContext(Dispatchers.IO) { api.fetchProfile(user.id) }
    val bytes = withContext(Dispatchers.IO) { api.download(profile.avatarUrl) }
    withContext(Dispatchers.IO) { file.save(bytes) }
    textView.text = "Done"
}

Callback مقابل Delegate: ماذا تختار؟

Callback وDelegate هما نهجان للإعلام غير المتزامن، ويعتمد الاختيار على المتطلبات المعمارية. الـ Callback مناسب للعمليات لمرة واحدة بنتيجة واحدة. الـ Delegate مصمم للأحداث المتعددة بتوقيعات دوال مختلفة. توصي Apple باستخدام delegate للبروتوكولات المعقدة بعدة دوال، وcallback للإغلاقات البسيطة بنتيجة واحدة. في Android، يحل الـ callback محل delegate في معظم الحالات بسبب دعم lambda.

متى تختار Callback

الـ Callback مثالي للعمليات بنتيجة واحدة: طلب شبكة، قراءة ملف، رسم متحرك مع كتلة إكمال. المزايا: صياغة مدمجة، عدم الحاجة لبروتوكول منفصل، التقاط مباشر للسياق. العيوب: تعقيد في النتائج المتعددة (تقدم، إيقاف مؤقت، إلغاء)، استحالة الإرسال المتعدد (إذا كان من الممكن استدعاء callback أكثر من مرة — استخدم publisher).

متى تختار Delegate

الـ Delegate مناسب للبروتوكولات بعدة دوال إلزامية واختيارية: UITableViewDelegate، CLLocationManagerDelegate، اتصالات Bluetooth. المزايا: تصنيف واضح لكل دالة، توثيق عبر البروتوكول، دعم الدوال الاختيارية عبر @objc optional. العيوب: كود نموذجي (boilerplate)، مرجع ضعيف إلى delegate إلزامي (weak var delegate)، تعقيد في التقاط السياق.

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

ما الفرق بين callback ودالة الترتيب الأعلى؟

Callback هو حالة خاصة من دالة الترتيب الأعلى. تقبل دالة الترتيب الأعلى دالة أخرى كوسيطة أو تعيدها. الـ Callback هو دالة تُمرر خصيصًا للتنفيذ غير المتزامن بعد اكتمال العملية. يتم تنفيذ جميع الـ callbacks من خلال دوال الترتيب الأعلى، لكن ليست كل دالة ترتيب أعلى هي callback.

هل يمكن استدعاء callback عدة مرات؟

وفقًا للاتفاق، يجب استدعاء callback مرة واحدة بالضبط — إما success أو failure. يُعتبر الاستدعاء المتعدد لنفس callback خطأ في التصميم. للأحداث المتعددة (التقدم، تدفق البيانات)، استخدم Observable أو Publisher أو Flow — فهي تدعم الإصدار المتعدد للقيم. تخالف بعض واجهات برمجة التطبيقات هذه القاعدة، مما يؤدي إلى أخطاء يصعب اكتشافها.

ما هو trailing closure في Swift؟

Trailing closure هو سكر نحوي في Swift يسمح بتمرير إغلاق بعد الأقواس المستديرة لاستدعاء الدالة. إذا كانت الدالة تقبل إغلاقًا كآخر وسيطة، يمكن وضعه خارج الأقواس: fetchData { result in ... }. للإغلاقات المتعددة، يُطبق trailing closure على الأخير فقط؛ والباقي يُسمى داخل الأقواس. هذا يحسن قابلية قراءة واجهات برمجة التطبيقات القائمة على callback.

كيف تتجنب تسرب الذاكرة مع callback في Android؟

استخدم WeakReference للمستمعين طويلي العمر، وألغِ الكوروتينات عبر Job.cancel() في onDestroy()، واستخدم lifecycleScope للإلغاء التلقائي. يحل ViewModel + LiveData/Flow المشكلة على المستوى المعماري. تجنب تمرير سياق Activity إلى callbacks ثابتة — استخدم Application context. تلتقط تعبيرات Kotlin lambda this ضمنيًا، تحقق باستخدام memory profiler.

هل سيحل async/await محل الـ callbacks بالكامل؟

Async/await يحل محل الـ callbacks للكود غير المتزامن التسلسلي، لكن ليس للهندسة المعمارية القائمة على الأحداث. تبقى الـ callbacks في واجهات برمجة تطبيقات النظام (View.OnClickListener، مفوضو URLSession) والـ callbacks ذات التقدم والمكتبات الخارجية. الاستبدال الكامل مستحيل بسبب التوافق العكسي. الاستراتيجية الحديثة هي استخدام async/await مع أغلفة callback (continuation في Swift، suspendCancellableCoroutine في Kotlin).

الخلاصة

  • Callback — دالة استدعاء عكسي تُمرر كوسيطة للتنفيذ غير المتزامن بعد اكتمال العملية.
  • Swift ينفذ الـ callbacks من خلال الإغلاقات مع @escaping وقائمة الالتقاط [weak self] وصياغة trailing closure.
  • Kotlin يستخدم تعبيرات lambda ودوال الترتيب الأعلى ودوال suspend للكوروتينات للتزامن.
  • Retain cycle في iOS يُمنع بقائمة الالتقاط؛ في Android — بـ WeakReference والمكونات lifecycle-aware.
  • Callback Hell يُحل باستخدام async/await في Swift والكوروتينات مع Flow في Kotlin.
  • Delegate أفضل من callback للبروتوكولات بعدة دوال؛ وcallback للعمليات لمرة واحدة.
  • استخدم callback للعمليات غير المتزامنة البسيطة، async/await للسلاسل المتسلسلة، وdelegate للأحداث المتعددة.

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

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

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

اقرأ أيضًا