RunLoop — ikot ng pagproseso ng kaganapan sa iOS, na ipinatupad ng mga bagay na CFRunLoop (Core Foundation) at NSRunLoop (Foundation). Ito ay isang mekanismo na naghihintay ng mga kaganapan (pagpindot, timer, pinagmumulan ng input, abiso) at ipinapadala ang mga ito sa mga kaukulang handler sa thread. Ang bawat thread sa iOS ay may maximum na isang RunLoop, ngunit ito ay awtomatikong nilikha lamang para sa Main Thread. Ayon sa Dokumentasyon ng Apple CFRunLoop, ang RunLoop ay kritikal para sa paggana ng mga timer, animation at pag-monitor ng mga pinagmumulan sa mga background thread.
Mga Pangunahing Punto
RunLoop — ay isang bagay na pang-imprastraktura ng Core Foundation na nag-oorganisa ng pagproseso ng mga kaganapan sa thread. Sa esensya ito ay isang walang katapusang loop (while true) na naghihintay ng pagdating ng mga kaganapan (sources) at ipinapadala ang mga ito sa mga handler. Kapag walang mga kaganapan, pinapatulog ng RunLoop ang thread (sleep), nakakatipid ng enerhiya ng baterya. Sa pagdating ng kaganapan, ang thread ay gumigising, pinoproseso ito, at muling natutulog. Ang RunLoop ay umiiral lamang sa iOS/macOS (XNU + Core Foundation) — sa Android ang papel nito ay ginagampanan ng Looper.
Ang bawat thread ay may hindi hihigit sa isang RunLoop, na nilikha nang tamad (lazy) sa unang pag-access. Para sa Main Thread, ang RunLoop ay awtomatikong nilikha kapag nag-start ang app. Para sa mga background thread, ang RunLoop ay hindi nilikha hanggang sa tawagin ang CFRunLoopGetCurrent() o RunLoop.current. Ang pangunahing RunLoop ng app ay responsable para sa pagproseso ng mga touch event, pag-render ng screen, pag-execute ng mga bloke ng DispatchQueue.main, at paglilingkod sa mga layer ng Core Animation.
Ang RunLoop ay hindi thread — ito ay isang mekanismo sa loob ng thread. Ang thread ay maaaring umiral nang walang RunLoop (kung magsasagawa ito ng isang synchronous na gawain at magtatapos), ngunit ang RunLoop ay hindi maaaring umiral nang walang thread. Kapag ang thread na may aktibong RunLoop ay walang mga kaganapan, hindi nito hinaharangan ang CPU, sa halip ito ay nasa estado ng paghihintay (waiting) — ito ang pangunahing pagkakaiba mula sa busy-wait na gumagamit ng 100% CPU.
RunLoop ay nagpoproseso ng dalawang uri ng pinagmumulan ng kaganapan: Input Sources (mga pinagmumulan ng input) at Timer Sources (mga timer). Ang Input Sources ay naghahatid ng mga asynchronous na kaganapan: pagpindot, paggalaw ng mouse, data mula sa socket, mensahe mula sa ibang mga thread (performSelector:onThread:). Ang Timer Sources ay naghahatid ng mga synchronous na kaganapan ayon sa iskedyul: NSTimer, CADisplayLink. Mayroon ding mga Observers — mga entry point para sa pag-monitor ng estado ng RunLoop.
Ang ikot ng RunLoop ay binubuo ng sunud-sunod na mga yugto: pagpasok sa mode (kCFRunLoopEntry), pagproseso ng mga timer (kCFRunLoopBeforeTimers), pagproseso ng mga pinagmumulan ng input (kCFRunLoopBeforeSources), pagproseso ng mga pinagmumulan (kCFRunLoopAfterWaiting), paghihintay (sleep), paglabas sa mode (kCFRunLoopExit). Kung sa kasalukuyang iteration ay walang kaganapan na naproseso, pinapatulog ng RunLoop ang thread para sa hindi tiyak na oras hanggang sa magising ng isang bagong kaganapan.
import Foundation
// Pagpapakita ng mga phase ng RunLoop sa pamamagitan ng 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 — Na-activate ang RunLoop")
case .beforeTimers:
print("BeforeTimers — pagproseso ng mga timer")
case .beforeSources:
print("BeforeSources — pagproseso ng mga pinagmumulan")
case .afterWaiting:
print("AfterWaiting — paggising pagkatapos matulog")
case .exit:
print("Exit — Tapos na ang RunLoop")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Halimbawa: Pinoproseso ng RunLoop ang timer sa pangunahing thread
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// Ang RunLoop.current.run() sa Main Thread ay tinatawag ng UIApplicationMain
// awtomatiko — hindi kailangang manu-manong simulan
RunLoop.current.run() // Ang tawag na ito ay hindi babalik sa Main Thread
}
Ang halimbawang observeRunLoopActivities ay nagrerehistro ng Observer sa pangunahing RunLoop na nagla-log ng bawat yugto ng ikot. Ito ay kapaki-pakinabang para sa debugging: kung makakita ka ng mahabang pagitan sa pagitan ng .beforeTimers at .afterWaiting, nangangahulugan ito na ang RunLoop ay hinarang ng isang operasyon sa Main Thread. Ipinapakita ng timerOnMainRunLoop kung paano awtomatikong gumagana ang NSTimer sa pangunahing RunLoop — kapag gumagawa ng Timer.scheduledTimer, ang timer ay idinaragdag sa kasalukuyang RunLoop bilang default (.default mode).
Android Looper — ang analog ng RunLoop. Ang Looper.prepare() ay lumilikha ng isang pila ng mensahe (MessageQueue) sa thread, ang Looper.loop() ay nag-start ng walang katapusang ikot ng pagproseso. Ang Handler ay nagpapadala ng mga mensahe at Runnable sa pila na ito. Ang pangunahing pagkakaiba: ang RunLoop ay sumusuporta sa mga mode, habang ang Android Looper ay hindi. Pinoproseso ng Looper ang lahat ng mensahe nang walang pag-filter ayon sa mode, na ginagawang mas simple ngunit hindi gaanong flexible sa mga sitwasyon na may priyoridad (halimbawa, ang pag-scroll sa iOS ay pinoproseso sa .tracking mode na hiwalay sa iba pang mga kaganapan).
RunLoop Mode — ay isang set ng mga pinagmumulan, timer at tagamasid na kasalukuyang aktibo. Ang mga mode ay nagpapahintulot sa paghiwalay ng pagproseso ng mga kaganapan ayon sa priyoridad. Kapag ang user ay nag-scroll ng UITableView, ang RunLoop ay lumilipat sa .tracking mode, kung saan ang mga scroll event lamang at kaukulang timer/animasyon ang pinoproseso. Lahat ng iba pang pinagmumulan (halimbawa, NSURLConnection) ay sinuspinde hanggang sa paglabas mula sa scroll mode.
Tatlong pangunahing mode: .default (NSDefaultRunLoopMode) — ang pangunahing mode kung saan lahat ng kaganapan maliban sa pag-scroll ay pinoproseso; .tracking (UITrackingRunLoopMode) — na-activate sa pag-scroll o gesture navigation; .common (NSRunLoopCommonModes) — hindi isang hiwalay na mode, kundi isang set ng mga alias na sumasaklaw sa .default + .tracking. Ang pagdaragdag ng pinagmulan sa .commonModes ay awtomatikong nagdaragdag nito sa lahat ng mode ng set.
| Mode | Core Foundation constant | Foundation constant | Kailan aktibo |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Normal na estado, walang pag-scroll |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Pag-scroll, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Pseudo-mode: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Unang pag-start ng RunLoop |
Klasikong problema: ang NSTimer na idinagdag sa .default mode ay humihinto sa paggana habang nag-scroll, dahil lumilipat ang RunLoop sa .tracking mode at hindi nagpoproseso ng mga timer mula sa .default. Solusyon — idagdag ang timer sa .commonModes: RunLoop.current.add(timer, forMode: .common). Ito ay magpapagana sa timer sa parehong .default at .tracking. Ang alternatibo ay ang paggamit ng DispatchQueue.main.async sa halip na NSTimer, dahil ang GCD ay gumagana sa antas ng thread, hindi sa antas ng RunLoop mode.
Ang mga background thread ay walang RunLoop bilang default. Kung sa background thread kailangang mag-start ng NSTimer, magproseso ng NSInputStream/NSOutputStream, o tumugon sa performSelector:, kailangan mong manu-manong lumikha at mag-start ng RunLoop. Kung walang RunLoop, ang timer at performSelector: ay hindi gagana — ang thread ay mag-execute ng code at magtatapos nang hindi naghihintay ng mga kaganapan.
Upang lumikha ng RunLoop sa background thread, sapat na ang pagtawag ng RunLoop.current.run() sa dulo ng trabaho ng thread. Ang tawag na ito ay humaharang sa thread para sa hindi tiyak na oras, nagpoproseso ng mga kaganapan. Para huminto, gamitin ang CFRunLoopStop(CFRunLoopGetCurrent()). Mahalaga: ang RunLoop.current ay lumilikha ng RunLoop nang tamad sa unang pag-access — kung hindi mo tatawagin ang run(), hindi ito magpoproseso ng mga kaganapan. Pattern: configuration ng mga pinagmumulan -> idagdag sa RunLoop -> tawagin ang run().
import Foundation
// Background thread na may sariling RunLoop
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// Awtomatikong nilikha ang RunLoop kapag tinawag ang RunLoop.current
let runLoop = RunLoop.current
// Nagdaragdag kami ng port para panatilihing aktibo ang RunLoop
runLoop.add(Port(), forMode: .default)
// Sinisimulan ang pagproseso ng kaganapan
var isFinished = false
while !isFinished {
// Ang run(mode:before:) ay nagbabalik ng true kung ang kaganapan ay naproseso
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Pagpapahinto ng RunLoop sa background thread
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Paggamit: timer sa background RunLoop
let manager = BackgroundRunLoopManager()
manager.start()
// Pagpapadala ng gawain sa background RunLoop sa pamamagitan ng performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager ay lumilikha ng background thread na may permanenteng RunLoop. Ang pagdaragdag ng walang laman na Port() ay kinakailangan upang maiwasan ang agarang pagtatapos ng RunLoop — walang mga pinagmumulan, ang RunLoop.run() ay nagbabalik ng false at lumalabas. Ang performSelector:onThread: ay nagpapadala ng mensahe sa background RunLoop — ito ay poproseso kapag pumasok ang RunLoop sa BeforeSources phase. Ang Stop ay tumatawag ng CFRunLoopStop sa background thread, na nagtatapos sa ikot.
NSTimer ay lumilikha ng timer event na pinoproseso ng RunLoop sa BeforeTimers phase. Ang mga timer ay maaaring repeating (umuulit) at non-repeating (isang beses). Hindi ginagarantiyahan ng NSTimer ang katumpakan: kung ang RunLoop ay hinarang ng isang mahabang operasyon, ang timer ay mag-a-activate pagkatapos ng pag-alis ng blocking, at lahat ng mga hindi nakuha na activation ay pagsasamahin sa isa (para sa repeating timer — hindi hihigit sa isang “paghabol” na activation).
CADisplayLink — isang espesyal na timer na naka-synchronize sa refresh rate ng screen (60/120/144 Hz). Ginagamit ito para sa mga animation at pag-update ng video. Ang CADisplayLink ay idinaragdag sa RunLoop at nag-a-activate bago ang bawat rendering frame (bago ipadala ng Core Animation ang layer sa rendering). Kung ang isang frame ay napalampas (hindi na-activate ang display link sa loob ng 16 ms), ang susunod na tawag ay magaganap sa susunod na VSync cycle.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — animation na may vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Pagdagdag sa .common mode — gumagana rin habang nag-scroll
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Tinatawag bawat frame (60 FPS → bawat 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // huminto pagkatapos ng 5 segundo
}
}
// NSTimer — pana-panahong gawain
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// SUSI: idagdag sa .common, kung hindi mag-freeze ang timer habang nag-scroll
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
Sa AnimationController, ang CADisplayLink ay idinagdag sa .common mode, na ginagarantiyahan ang pagtawag ng step sa bawat frame anuman ang pag-scroll. displayLink.add(to: .current, forMode: .common) — ang karaniwang pattern para sa mga animation na hindi dapat maantala habang nag-scroll. Ang NSTimer ay idinagdag din sa .common mode upang tumibok habang nag-scroll. Kung wala ito, ang timer ay gagana lamang sa .default mode.
CFRunLoopObserver — mekanismo para sa pagsubaybay ng mga phase ng RunLoop. Gamit ang Observer, maaari kang makatanggap ng mga abiso tungkol sa pagpasok sa mode, pagsisimula ng pagproseso ng timer, pagsisimula ng pagproseso ng pinagmumulan, paggising mula sa pagtulog, paglabas mula sa mode. Ang mga Observer ay ginagamit ng mga framework para sa kanilang mga pangangailangan: ginagamit sila ng Core Animation para sa pag-render ng mga layer bago matulog ang RunLoop, UIKit — para sa pag-update ng layout pagkatapos ng pagproseso ng mga kaganapan.
Ang developer ay maaari ring magdagdag ng mga Observer para sa kanilang sariling mga layunin. Halimbawa: pagsukat ng oras ng pagproseso ng kaganapan (profiling), pagsasagawa ng mga ipinagpaliban na operasyon bago matulog ang RunLoop (kapag ang UI ay na-update na at ang user ay hindi nakikipag-ugnayan), awtomatikong pag-save ng data sa mahabang kawalan ng aktibidad. Ang Observer ay nirerehistro sa pamamagitan ng CFRunLoopAddObserver na may pagtukoy ng mode at bit mask ng mga sinusubaybayang aktibidad.
Mga pinakakapaki-pakinabang na punto para sa Observer: .afterWaiting — isinasagawa pagkatapos magising ang RunLoop at maaaring maglaman ng code na dapat tumakbo pagkatapos ng pagproseso ng kaganapan; .beforeTimers — bago ang pagproseso ng timer, nagbibigay-daan sa pagsukat ng oras mula sa nakaraang pagproseso; .exit — nag-a-activate kapag huminto ang RunLoop, kapaki-pakinabang para sa paglilinis ng mga mapagkukunan ng background thread.
CFRunLoopStop — function na pumipilit sa pagtatapos ng kasalukuyang RunLoop iteration. Kapag tinawag ang CFRunLoopStop(CFRunLoopGetCurrent()), tinatapos ng RunLoop ang pagproseso ng kasalukuyang kaganapan at lumalabas mula sa run(), nagbabalik ng false. Ito ang karaniwang paraan upang ihinto ang RunLoop sa isang background thread. Sa Main Thread, ang CFRunLoopStop ay hindi inirerekomenda — ang pangunahing RunLoop ay dapat gumana sa buong buhay ng app. Para sa mga background thread, pagkatapos ng CFRunLoopStop ang thread ay maaaring magtapos o magpatuloy sa pag-execute ng susunod na code pagkatapos ng run().
Mga Madalas Itanong
RunLoop — ikot ng kaganapan sa iOS na ipinatupad ng CFRunLoop (Core Foundation) at NSRunLoop (Foundation). Naghihintay ito ng mga kaganapan (pagpindot, timer, pinagmumulan ng input) at ipinapadala ang mga ito sa mga handler sa thread. Ang bawat thread ay maaaring magkaroon ng isang RunLoop, ngunit awtomatiko itong nilikha lamang para sa Main Thread. Pinamamahalaan ng RunLoop ang mga mode (.default, .tracking, .common), na naghihiwalay ng pagproseso ayon sa priyoridad.
NSTimer ay idinaragdag bilang default sa .default mode ng RunLoop. Kapag nag-scroll ang user, lumilipat ang RunLoop sa .tracking mode at hindi nagpoproseso ng mga timer mula sa .default. Solusyon: idagdag ang timer sa .common mode sa pamamagitan ng RunLoop.current.add(timer, forMode: .common). Pinagsasama ng .common ang .default at .tracking, kaya gumagana ang timer sa parehong mode.
Lamang kung ang background thread ay gumagamit ng mga timer (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream o Source na kaganapan. Kung ang thread ay nagsasagawa ng isang synchronous na gawain (pag-download ng file, pag-compute) at nagtatapos — hindi kailangan ang RunLoop. Upang mag-start, tawagin ang RunLoop.current.run() pagkatapos ng configuration ng mga pinagmumulan. Upang huminto — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop ay gumagana sa antas ng thread at nagpoproseso ng mga kaganapan nang sunud-sunod, na may suporta para sa mga mode. DispatchQueue — abstraction ng isang pool ng thread, ang mga gawain ay isinasagawa sa anumang libreng thread. Hindi sinusuportahan ng GCD ang mga mode at umiiral nang hiwalay sa RunLoop. Ginagamit ng DispatchQueue.main ang pangunahing RunLoop para sa pag-execute ng mga bloke — ito ang nag-iisang punto ng intersection. Para sa mga background na gawain, mas gusto ang GCD.
CADisplayLink — timer na naka-synchronize sa VSync (refresh rate ng screen). Idinaragdag ito sa RunLoop at nag-a-activate bago ang bawat rendering frame sa BeforeTimers phase. Ang CADisplayLink ay gumagana lamang sa Main Thread, dahil ang screen rendering ay nangyayari doon. Para sa tuloy-tuloy na animation habang nag-scroll, idagdag ito sa .common mode: displayLink.add(to: .current, forMode: .common).
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din