RunLoop — κύκλος επεξεργασίας συμβάντων στο iOS, που υλοποιείται από τα αντικείμενα CFRunLoop (Core Foundation) και NSRunLoop (Foundation). Είναι ένας μηχανισμός που περιμένει συμβάντα (αφές, χρονοδιακόπτες, πηγές εισόδου, ειδοποιήσεις) και τα δρομολογεί στους αντίστοιχους χειριστές στο νήμα. Κάθε νήμα στο iOS έχει το πολύ ένα RunLoop, αλλά δημιουργείται αυτόματα μόνο για το Main Thread. Σύμφωνα με την τεκμηρίωση Apple CFRunLoop, το RunLoop είναι κρίσιμο για τη λειτουργία χρονοδιακοπτών, κινούμενων σχεδίων και παρακολούθησης πηγών σε νήματα παρασκηνίου.
Κύρια σημεία
RunLoop — είναι ένα αντικείμενο υποδομής Core Foundation που οργανώνει την επεξεργασία συμβάντων στο νήμα. Στην ουσία είναι ένας ατελείωτος βρόχος (while true) που περιμένει την άφιξη συμβάντων (sources) και τα προωθεί στους χειριστές. Όταν δεν υπάρχουν συμβάντα, το RunLoop θέτει το νήμα σε κατάσταση ύπνου (sleep), εξοικονομώντας ενέργεια μπαταρίας. Με την άφιξη συμβάντος, το νήμα ξυπνά, το επεξεργάζεται και ξανακοιμάται. Το 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, αλλά βρίσκεται σε κατάσταση αναμονής (waiting) — αυτή είναι η βασική διαφορά από το busy-wait που καταναλώνει 100% CPU.
RunLoop επεξεργάζεται δύο τύπους πηγών συμβάντων: Input Sources (πηγές εισόδου) και Timer Sources (χρονοδιακόπτες). Οι Input Sources παρέχουν ασύγχρονα συμβάντα: αφές, κινήσεις ποντικιού, δεδομένα από socket, μηνύματα από άλλα νήματα (performSelector:onThread:). Οι Timer Sources παρέχουν σύγχρονα συμβάντα σύμφωνα με πρόγραμμα: 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 mode).
Android Looper — το ανάλογο του RunLoop. Το Looper.prepare() δημιουργεί μια ουρά μηνυμάτων (MessageQueue) στο νήμα, το Looper.loop() ξεκινά έναν ατελείωτο βρόχο επεξεργασίας. Το Handler στέλνει μηνύματα και Runnable σε αυτήν την ουρά. Η κύρια διαφορά: το RunLoop υποστηρίζει λειτουργίες (modes), ενώ το Android Looper όχι. Το 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). Αυτό θα κάνει τον χρονοδιακόπτη να λειτουργεί τόσο σε .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 Hz). Χρησιμοποιείται για κινούμενα σχέδια και ενημερώσεις βίντεο. Το CADisplayLink προστίθεται στο RunLoop και ενεργοποιείται πριν από κάθε καρέ απόδοσης (πριν το Core Animation στείλει το επίπεδο για απόδοση). Εάν ένα καρέ χαθεί (το display link δεν μπόρεσε να ενεργοποιηθεί εντός 16 ms), η επόμενη κλήση γίνεται στον επόμενο κύκλο 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 ms)
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")
}
// ΚΛΕΙΔΙ: προσθέτουμε σε .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 μπορείτε να λαμβάνετε ειδοποιήσεις για είσοδο σε λειτουργία, έναρξη επεξεργασίας χρονοδιακοπτών, έναρξη επεξεργασίας πηγών, αφύπνιση από ύπνο, έξοδο από λειτουργία. Οι Observer χρησιμοποιούνται από τα πλαίσια για τις ανάγκες τους: το Core Animation τους χρησιμοποιεί για απόδοση επιπέδων πριν από τον ύπνο RunLoop, το UIKit — για ενημέρωση διάταξης μετά από επεξεργασία συμβάντων.
Ο προγραμματιστής μπορεί επίσης να προσθέσει Observer για δικούς του σκοπούς. Για παράδειγμα: μέτρηση χρόνου επεξεργασίας συμβάντων (προφίλ), εκτέλεση αναβαλλόμενων λειτουργιών πριν από τον ύπνο RunLoop (όταν το UI έχει ήδη ενημερωθεί και ο χρήστης δεν αλληλεπιδρά), αυτόματη αποθήκευση δεδομένων κατά μεγάλη αδράνεια. Ο Observer καταχωρείται μέσω CFRunLoopAddObserver με καθορισμό λειτουργίας και bit mask παρακολουθούμενων δραστηριοτήτων.
Τα πιο χρήσιμα σημεία για 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. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης