RunLoop — دورة معالجة الأحداث في iOS، يتم تنفيذها بواسطة كائنات CFRunLoop (Core Foundation) و NSRunLoop (Foundation). هذه الآلية تنتظر الأحداث (اللمسات، المؤقتات، مصادر الإدخال، الإشعارات) وتوزعها على المعالجات المناسبة في الخيط. كل خيط في iOS لديه على الأكثر RunLoop واحد، لكنه يتم إنشاؤه تلقائياً فقط لـ Main Thread. وفقاً لوثائق Apple CFRunLoop، يعتبر RunLoop أساسياً لعمل المؤقتات والرسوم المتحركة ومراقبة المصادر في الخيوط الخلفية.
النقاط الرئيسية
RunLoop هو كائن بنية تحتية من Core Foundation ينظم معالجة الأحداث على الخيط. في جوهره، هو حلقة لا نهائية (while true) تنتظر وصول الأحداث (sources) وتمررها إلى المعالجات. عندما لا توجد أحداث، يضع RunLoop الخيط في حالة السكون، مما يوفر طاقة البطارية. عند وصول حدث، يستيقظ الخيط، يعالجه، ثم يعود للنوم. RunLoop موجود فقط في iOS/macOS (XNU + Core Foundation) — في Android، دوره يؤديه Looper.
كل خيط لديه على الأكثر RunLoop واحد، يتم إنشاؤه بتكاسل (lazy) عند أول وصول. بالنسبة لـ Main Thread، يتم إنشاء RunLoop تلقائياً عند تشغيل التطبيق. بالنسبة للخيوط الخلفية، لا يتم إنشاء RunLoop حتى يتم استدعاء CFRunLoopGetCurrent() أو RunLoop.current. RunLoop الرئيسي للتطبيق مسؤول عن معالجة أحداث اللمس، عرض الشاشة، تنفيذ كتل DispatchQueue.main وخدمة طبقات Core Animation.
RunLoop ليس خيطاً — إنه آلية داخل الخيط. يمكن للخيط أن يوجد بدون RunLoop (إذا كان ينفذ مهمة متزامنة وينتهي)، لكن RunLoop لا يمكن أن يوجد بدون خيط. عندما لا يحتوي الخيط مع RunLoop نشط على أحداث، لا يستهلك CPU بل يدخل في حالة انتظار — هذا هو الفرق الرئيسي عن حلقة الانتظار النشط التي تستهلك 100% من CPU.
RunLoop يعالج نوعين من مصادر الأحداث: Input Sources (مصادر الإدخال) و Timer Sources (المؤقتات). مصادر الإدخال توصل الأحداث غير المتزامنة: اللمسات، حركات الماوس، بيانات المقبس، رسائل من خيوط أخرى (performSelector:onThread:). مصادر المؤقتات توصل الأحداث المتزامنة المجدولة: NSTimer، CADisplayLink. هناك أيضاً Observers — نقاط دخول لمراقبة حالة RunLoop.
دورة RunLoop تتكون من مراحل متسلسلة: الدخول إلى نمط (kCFRunLoopEntry)، معالجة المؤقتات (kCFRunLoopBeforeTimers)، معالجة مصادر الإدخال (kCFRunLoopBeforeSources)، معالجة المصادر (kCFRunLoopAfterWaiting)، الانتظار (sleep)، والخروج من النمط (kCFRunLoopExit). إذا لم تتم معالجة أي حدث خلال التكرار الحالي، يضع RunLoop الخيط في النوم لفترة غير محددة حتى يوقظه حدث جديد.
import Foundation
// عرض مراحل RunLoop عبر Observer
func observeRunLoopActivities() {
let observer = CFRunLoopObserverCreateWithHandler(
nil,
CFOptionFlags([[.entry, .beforeTimers, .beforeSources,
.afterWaiting, .exit]]),
true, // repeats
0 // priority
) { observer, activity in
switch activity {
case .entry:
print("Entry — تم تفعيل RunLoop")
case .beforeTimers:
print("BeforeTimers — معالجة المؤقتات")
case .beforeSources:
print("BeforeSources — معالجة المصادر")
case .afterWaiting:
print("AfterWaiting — الاستيقاظ بعد النوم")
case .exit:
print("Exit — تم إنهاء RunLoop")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// مثال: RunLoop يعالج مؤقتاً على الخيط الرئيسي
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// يتم استدعاء RunLoop.current.run() على Main Thread بواسطة UIApplicationMain
// تلقائياً — لا حاجة للتشغيل اليدوي
RunLoop.current.run() // هذا الاستدعاء لن يعود على Main Thread
}
مثال observeRunLoopActivities يسجل Observer على RunLoop الرئيسي الذي يسجل كل مرحلة من الدورة. هذا مفيد لتصحيح الأخطاء: إذا رأيت فجوة طويلة بين .beforeTimers و .afterWaiting، فهذا يعني أن RunLoop محظور بعملية على Main Thread. timerOnMainRunLoop يوضح كيف يعمل NSTimer تلقائياً على RunLoop الرئيسي — عند إنشاء Timer.scheduledTimer، يضيف المؤقت إلى RunLoop الحالي افتراضياً (وضع .default).
Looper في Android — نظير RunLoop. Looper.prepare() ينشئ قائمة رسائل (MessageQueue) على خيط، Looper.loop() يبدأ حلقة معالجة لا نهائية. Handler يرسل الرسائل و Runnables إلى هذه القائمة. الفرق الرئيسي: RunLoop يدعم الأنماط (modes)، بينما Looper في Android لا يدعمها. Looper يعالج جميع الرسائل بدون تصفية حسب النمط، مما يجعله أبسط لكن أقل مرونة للسيناريوهات ذات الأولويات (مثلاً، التمرير في iOS يتم معالجته في وضع .tracking بشكل منفصل عن الأحداث الأخرى).
RunLoop Mode — مجموعة من المصادر والمؤقتات والمراقبين النشطين في لحظة معينة. تسمح الأنماط بعزل معالجة الأحداث حسب الأولوية. عندما يقوم المستخدم بتمرير UITableView، يتحول RunLoop إلى وضع .tracking، حيث تتم معالجة أحداث التمرير والمؤقتات/الرسوم المتحركة المرتبطة فقط. جميع المصادر الأخرى (مثل NSURLConnection) يتم تعليقها حتى الخروج من وضع التمرير.
ثلاثة أنماط رئيسية: .default (NSDefaultRunLoopMode) — النمط الرئيسي حيث تتم معالجة جميع الأحداث باستثناء التمرير؛ .tracking (UITrackingRunLoopMode) — يتم تفعيله أثناء التمرير أو التنقل بالإيماءات؛ .common (NSRunLoopCommonModes) — ليس نمطاً منفصلاً بل مجموعة أسماء مستعارة تشمل .default + .tracking. إضافة مصدر إلى .commonModes يضيفه تلقائياً إلى جميع الأنماط في المجموعة.
| النمط | ثابت Core Foundation | ثابت Foundation | متى يكون نشطاً |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | حالة عادية، بدون تمرير |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | التمرير، gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | نمط زائف: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | أول تشغيل لـ RunLoop |
مشكلة كلاسيكية: NSTimer المضاف إلى وضع .default يتوقف عن العمل أثناء التمرير لأن RunLoop يتحول إلى وضع .tracking ولا يعالج المؤقتات من .default. الحل — إضافة المؤقت إلى .commonModes: RunLoop.current.add(timer, forMode: .common). هذا يجعل Timer يعمل في كل من .default و .tracking. بديل — استخدام DispatchQueue.main.async بدلاً من NSTimer، لأن GCD يعمل على مستوى الخيوط وليس على مستوى أنماط RunLoop.
الخيوط الخلفية لا تملك RunLoop افتراضياً. إذا كنت بحاجة إلى تشغيل NSTimer، معالجة NSInputStream/NSOutputStream أو الاستجابة لـ performSelector: في خيط خلفي، يجب عليك إنشاء وتشغيل RunLoop يدوياً. بدون RunLoop، المؤقتات و performSelector: لن تعمل أبداً — سينفذ الخيط الكود وينتهي دون انتظار الأحداث.
لإنشاء RunLoop في خيط خلفي، ما عليك سوى استدعاء RunLoop.current.run() في نهاية عمل الخيط. هذا الاستدعاء يمنع الخيط إلى أجل غير مسمى، معالجاً الأحداث. للإيقاف، استخدم CFRunLoopStop(CFRunLoopGetCurrent()). مهم: RunLoop.current ينشئ RunLoop بتكاسل عند أول وصول — إذا لم يتم استدعاء run()، لن يعالج الأحداث. النمط: تكوين المصادر -> إضافة إلى RunLoop -> استدعاء run().
import Foundation
// خيط خلفي مع RunLoop خاص به
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// يتم إنشاء RunLoop تلقائياً عند استدعاء RunLoop.current
let runLoop = RunLoop.current
// أضف منفذاً للحفاظ على RunLoop نشطاً
runLoop.add(Port(), forMode: .default)
// ابدأ معالجة الأحداث
var isFinished = false
while !isFinished {
// run(mode:before:) يعيد true إذا تمت معالجة حدث
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// إيقاف RunLoop على خيط خلفي
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// الاستخدام: مؤقت على RunLoop خلفي
let manager = BackgroundRunLoopManager()
manager.start()
// إرسال مهمة إلى RunLoop خلفي عبر performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager ينشئ خيطاً خلفياً مع RunLoop دائم. إضافة Port() فارغ ضروري حتى لا ينتهي RunLoop فوراً — بدون مصادر، RunLoop.run() يعيد false ويخرج. performSelector:onThread: يرسل رسالة إلى RunLoop الخلفي — سيتم معالجتها عندما يدخل RunLoop في مرحلة BeforeSources. Stop يستدعي CFRunLoopStop على الخيط الخلفي، منهياً الدورة.
NSTimer ينشئ حدث مؤقت يعالجه RunLoop في مرحلة BeforeTimers. المؤقتات يمكن أن تكون متكررة (repeating) أو غير متكررة (non-repeating). NSTimer لا يضمن دقة التشغيل: إذا كان RunLoop محظوراً بعملية طويلة، سيعمل المؤقت بعد فك الحظر، وسيتم دمج جميع التشغيلات الفائتة في واحدة (للمؤقتات المتكررة — تشغيل واحد «ملحق» على الأكثر).
CADisplayLink — مؤقت متخصص متزامن مع معدل تحديث الشاشة (60/120/144 هرتز). يستخدم للرسوم المتحركة وتحديث الفيديو. CADisplayLink يضاف إلى RunLoop ويعمل قبل كل إطار عرض (قبل أن ترسل Core Animation الطبقة للعرض). إذا تم تخطي إطار (لم يعمل display link خلال 16 مللي ثانية)، يحدث الاستدعاء التالي في دورة VSync التالية.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — رسوم متحركة مع vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// الإضافة إلى وضع .common — يعمل حتى أثناء التمرير
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// يتم استدعاؤه كل إطار (60 FPS → كل 16.6 مللي ثانية)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // إيقاف بعد 5 ثوان
}
}
// NSTimer — مهمة دورية
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// KEY: أضف إلى .common، وإلا سيتجمد المؤقت أثناء التمرير
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
في AnimationController، CADisplayLink مضاف إلى وضع .common، مما يضمن استدعاء step في كل إطار بغض النظر عن التمرير. displayLink.add(to: .current, forMode: .common) — النمط القياسي للرسوم المتحركة التي لا يجب أن تنقطع أثناء التمرير. NSTimer مضاف أيضاً إلى وضع .common ليعمل أثناء التمرير. بدون هذا، كان المؤقت سيعمل فقط في وضع .default.
CFRunLoopObserver — آلية لتتبع مراحل RunLoop. باستخدام Observer، يمكنك تلقي إشعارات حول الدخول إلى نمط، بدء معالجة المؤقتات، بدء معالجة المصادر، الاستيقاظ بعد النوم، والخروج من النمط. يستخدم الأطر (frameworks) المراقبين لاحتياجاتهم الخاصة: Core Animation تستخدمهم لعرض الطبقات قبل أن ينام RunLoop، UIKit — لتحديث التخطيط بعد معالجة الأحداث.
يمكن للمطور أيضاً إضافة مراقبين لأغراضه الخاصة. على سبيل المثال: قياس وقت معالجة الأحداث (التوصيف)، تنفيذ عمليات مؤجلة قبل أن ينام RunLoop (عندما تكون واجهة المستخدم محدثة بالفعل والمستخدم لا يتفاعل)، حفظ البيانات تلقائياً أثناء الخمول الطويل. يتم تسجيل Observer عبر CFRunLoopAddObserver مع تحديد النمط وقناع بت للأنشطة المتعقبة.
أكثر نقاط Observer فائدة: .afterWaiting — يتم تنفيذه بعد استيقاظ RunLoop ويمكن أن يحتوي على كود يجب تشغيله بعد معالجة الحدث؛ .beforeTimers — قبل معالجة المؤقتات، يسمح بقياس الوقت المنقضي منذ المعالجة السابقة؛ .exit — يعمل عند إيقاف RunLoop، مفيد لتنظيف موارد الخيط الخلفي.
CFRunLoopStop — دالة تنهي قسراً التكرار الحالي لـ RunLoop. عند استدعاء CFRunLoopStop(CFRunLoopGetCurrent())، ينهي RunLoop معالجة الحدث الحالي ويخرج من run()، معيداً false. هذه هي الطريقة القياسية لإيقاف RunLoop على خيط خلفي. في Main Thread، لا يُنصح باستخدام CFRunLoopStop — يجب أن يعمل RunLoop الرئيسي طوال عمر التطبيق. للخيوط الخلفية، بعد CFRunLoopStop، يمكن للخيط أن ينتهي أو يواصل تنفيذ الكود بعد run().
الأسئلة الشائعة
RunLoop هو دورة معالجة أحداث في iOS، يتم تنفيذها بواسطة CFRunLoop (Core Foundation) و NSRunLoop (Foundation). ينتظر الأحداث (اللمسات، المؤقتات، مصادر الإدخال) ويوزعها على المعالجات في الخيط. كل خيط يمكن أن يكون لديه RunLoop واحد، لكن يتم إنشاؤه تلقائياً فقط لـ Main Thread. RunLoop يدير الأنماط (.default، .tracking، .common)، معزولاً المعالجة حسب الأولوية.
NSTimer يضاف افتراضياً إلى وضع .default لـ RunLoop. عندما يقوم المستخدم بالتمرير، يتحول RunLoop إلى وضع .tracking ولا يعالج المؤقتات من .default. الحل: أضف المؤقت إلى وضع .common عبر RunLoop.current.add(timer, forMode: .common). .common يجمع .default و .tracking، لذلك يعمل المؤقت في كلا الوضعين.
فقط إذا كان الخيط الخلفي يستخدم مؤقتات (NSTimer)، performSelector:onThread:، NSInputStream/NSOutputStream أو أحداث Source. إذا كان الخيط ينفذ مهمة متزامنة (تحميل ملف، عمليات حسابية) وينتهي — لا حاجة لـ RunLoop. للتشغيل، اتصل بـ RunLoop.current.run() بعد تكوين المصادر. للإيقاف — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop يعمل على مستوى الخيط ويعالج الأحداث بالتسلسل مع دعم الأنماط (modes). DispatchQueue — تجريد لمجموعة خيوط، يتم تنفيذ المهام على أي خيط متاح. GCD لا يدعم الأنماط ويعيش بشكل مستقل عن RunLoop. DispatchQueue.main يستخدم RunLoop الرئيسي لتنفيذ الكتل — هذه هي نقطة التقاطع الوحيدة. للمهام الخلفية، يفضل استخدام GCD.
CADisplayLink هو مؤقت متزامن مع VSync (معدل تحديث الشاشة). يضاف إلى RunLoop ويعمل قبل كل إطار عرض في مرحلة BeforeTimers. CADisplayLink يعمل فقط على Main Thread، لأن عرض الشاشة يحدث هناك. للرسوم المتحركة المستمرة أثناء التمرير، أضفه إلى وضع .common: displayLink.add(to: .current, forMode: .common).
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.