RunLoop — vòng lặp xử lý sự kiện trong iOS, được triển khai bởi các đối tượng CFRunLoop (Core Foundation) và NSRunLoop (Foundation). Đây là cơ chế chờ đợi các sự kiện (chạm, timer, nguồn đầu vào, thông báo) và phân phối chúng đến các bộ xử lý tương ứng trên thread. Mỗi thread trong iOS có tối đa một RunLoop, nhưng nó chỉ được tạo tự động cho Main Thread. Theo Apple CFRunLoop Documentation, RunLoop rất quan trọng cho hoạt động của timer, hoạt ảnh và giám sát nguồn trong các thread nền.
Những điểm chính
RunLoop — là đối tượng cơ sở hạ tầng của Core Foundation tổ chức việc xử lý sự kiện trên thread. Về bản chất, nó là một vòng lặp vô hạn (while true) chờ đợi các sự kiện (sources) đến và chuyển chúng đến bộ xử lý. Khi không có sự kiện, RunLoop đưa thread vào chế độ ngủ (sleep), tiết kiệm năng lượng pin. Khi có sự kiện đến, thread thức dậy, xử lý sự kiện và lại ngủ tiếp. RunLoop chỉ tồn tại trong iOS/macOS (XNU + Core Foundation) — trong Android, vai trò này do Looper đảm nhận.
Mỗi thread có không quá một RunLoop, được tạo theo kiểu lazy (lười) khi truy cập lần đầu. Đối với Main Thread, RunLoop được tạo tự động khi khởi động ứng dụng. Đối với các thread nền, RunLoop không được tạo cho đến khi CFRunLoopGetCurrent() hoặc RunLoop.current được gọi. RunLoop chính của ứng dụng chịu trách nhiệm xử lý các sự kiện chạm, vẽ màn hình, thực thi các khối DispatchQueue.main và duy trì các lớp Core Animation.
RunLoop không phải là thread — nó là cơ chế bên trong thread. Một thread có thể tồn tại mà không có RunLoop (nếu thực hiện tác vụ đồng bộ và kết thúc), nhưng RunLoop không thể tồn tại nếu không có thread. Khi một thread với RunLoop đang hoạt động không có sự kiện, nó không chặn CPU mà ở trạng thái chờ (waiting) — đây là điểm khác biệt chính so với vòng lặp busy-wait tiêu tốn 100% CPU.
RunLoop xử lý hai loại nguồn sự kiện: Input Sources (nguồn đầu vào) và Timer Sources (bộ hẹn giờ). Input Sources cung cấp các sự kiện bất đồng bộ: chạm, chuyển động chuột, dữ liệu từ socket, tin nhắn từ thread khác (performSelector:onThread:). Timer Sources cung cấp các sự kiện đồng bộ theo lịch trình: NSTimer, CADisplayLink. Ngoài ra còn có Observers — các điểm vào để giám sát trạng thái RunLoop.
Vòng lặp RunLoop bao gồm các pha tuần tự: vào chế độ (kCFRunLoopEntry), xử lý timer (kCFRunLoopBeforeTimers), xử lý nguồn đầu vào (kCFRunLoopBeforeSources), xử lý nguồn (kCFRunLoopAfterWaiting), chờ (sleep), thoát chế độ (kCFRunLoopExit). Nếu không có sự kiện nào được xử lý trong lần lặp hiện tại, RunLoop đưa thread vào ngủ trong khoảng thời gian không xác định cho đến khi được đánh thức bởi một sự kiện mới.
import Foundation
// Minh họa các pha của RunLoop thông qua 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 được kích hoạt")
case .beforeTimers:
print("BeforeTimers — xử lý timer")
case .beforeSources:
print("BeforeSources — xử lý nguồn")
case .afterWaiting:
print("AfterWaiting — thức dậy sau khi ngủ")
case .exit:
print("Exit — RunLoop kết thúc")
default:
break
}
}
CFRunLoopAddObserver(
CFRunLoopGetCurrent(),
observer,
.commonModes
)
}
// Ví dụ: RunLoop xử lý một timer trên thread chính
func timerOnMainRunLoop() {
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
print("Tick: \(Date())")
}
// RunLoop.current.run() trên Main Thread được UIApplicationMain gọi
// tự động — không cần khởi động thủ công
RunLoop.current.run() // Lời gọi này sẽ không trả về trên Main Thread
}
Ví dụ observeRunLoopActivities đăng ký một Observer trên RunLoop chính ghi lại từng pha của vòng lặp. Điều này hữu ích cho việc gỡ lỗi: nếu bạn thấy khoảng trống dài giữa .beforeTimers và .afterWaiting, có nghĩa là RunLoop đang bị chặn bởi một thao tác trên Main Thread. timerOnMainRunLoop cho thấy NSTimer tự động hoạt động trên RunLoop chính như thế nào — khi tạo Timer.scheduledTimer, timer được thêm vào RunLoop hiện tại ở chế độ mặc định (.default mode).
Android Looper — tương tự như RunLoop. Looper.prepare() tạo hàng đợi tin nhắn (MessageQueue) trên thread, Looper.loop() khởi động vòng lặp xử lý vô hạn. Handler gửi tin nhắn và Runnable vào hàng đợi này. Sự khác biệt chính: RunLoop hỗ trợ các chế độ (modes), còn Android Looper thì không. Looper xử lý tất cả tin nhắn mà không lọc theo chế độ, điều này làm nó đơn giản hơn nhưng kém linh hoạt hơn cho các tình huống có ưu tiên (ví dụ: cuộn trong iOS được xử lý ở chế độ .tracking riêng biệt với các sự kiện khác).
RunLoop Mode — là tập hợp các nguồn, timer và người quan sát đang hoạt động tại thời điểm đó. Các chế độ cho phép cách ly xử lý sự kiện theo ưu tiên. Khi người dùng cuộn UITableView, RunLoop chuyển sang chế độ .tracking, trong đó chỉ xử lý các sự kiện cuộn và timer/hoạt ảnh tương ứng. Tất cả các nguồn khác (ví dụ: NSURLConnection) bị tạm dừng cho đến khi thoát khỏi chế độ cuộn.
Ba chế độ chính: .default (NSDefaultRunLoopMode) — chế độ chính, xử lý tất cả sự kiện trừ cuộn; .tracking (UITrackingRunLoopMode) — được kích hoạt khi cuộn hoặc điều hướng bằng cử chỉ; .common (NSRunLoopCommonModes) — không phải chế độ riêng mà là tập bí danh bao gồm .default + .tracking. Thêm một nguồn vào .commonModes tự động thêm nó vào tất cả các chế độ trong tập.
| Chế độ | Hằng số Core Foundation | Hằng số Foundation | Khi nào hoạt động |
|---|---|---|---|
| .default | kCFRunLoopDefaultMode | RunLoop.Mode.default | Trạng thái bình thường, không cuộn |
| .tracking | UITrackingRunLoopMode | RunLoop.Mode.tracking | Cuộn, gesture recognizers |
| .common | kCFRunLoopCommonModes | RunLoop.Mode.common | Chế độ giả: default + tracking |
| .initialRun | kCFRunLoopInitialRunRunLoopMode | — | Lần chạy RunLoop đầu tiên |
Vấn đề kinh điển: NSTimer, được thêm vào chế độ .default, ngừng hoạt động trong khi cuộn vì RunLoop chuyển sang chế độ .tracking và không xử lý timer từ .default. Giải pháp — thêm timer vào .commonModes: RunLoop.current.add(timer, forMode: .common). Điều này làm cho Timer hoạt động cả trong .default và .tracking. Giải pháp thay thế — sử dụng DispatchQueue.main.async thay vì NSTimer, vì GCD hoạt động ở cấp độ thread, không phải chế độ RunLoop.
Các thread nền không có RunLoop theo mặc định. Nếu trong thread nền cần chạy NSTimer, xử lý NSInputStream/NSOutputStream hoặc phản ứng với performSelector:, cần phải tạo và khởi động RunLoop thủ công. Nếu không có RunLoop, timer và performSelector: sẽ không bao giờ hoạt động — thread sẽ chạy mã và kết thúc mà không chờ đợi sự kiện.
Để tạo RunLoop trong thread nền, chỉ cần gọi RunLoop.current.run() ở cuối công việc của thread. Lời gọi này chặn thread vô thời hạn để xử lý sự kiện. Để dừng, sử dụng CFRunLoopStop(CFRunLoopGetCurrent()). Quan trọng: RunLoop.current tạo RunLoop theo kiểu lazy khi truy cập lần đầu — nếu không gọi run(), nó sẽ không xử lý sự kiện. Mẫu: cấu hình nguồn -> thêm vào RunLoop -> gọi run().
import Foundation
// Thread nền với RunLoop riêng
class BackgroundRunLoopManager {
private let thread: Thread
private var isRunning = false
init() {
thread = Thread { [weak self] in
// RunLoop được tạo tự động khi gọi RunLoop.current
let runLoop = RunLoop.current
// Chúng ta thêm một Port trống để duy trì RunLoop hoạt động
runLoop.add(Port(), forMode: .default)
// Bắt đầu xử lý sự kiện
var isFinished = false
while !isFinished {
// run(mode:before:) trả về true nếu đã xử lý sự kiện
isFinished = !runLoop.run(mode: .default, before: Date.distantFuture)
}
}
thread.name = "com.app.background-runloop"
}
func start() {
thread.start()
isRunning = true
}
func stop() {
// Dừng RunLoop trên thread nền
self.perform(
#selector(BackgroundRunLoopManager.stopRunLoop),
on: thread,
with: nil,
waitUntilDone: false
)
}
@objc
private func stopRunLoop() {
CFRunLoopStop(CFRunLoopGetCurrent())
isRunning = false
}
}
// Sử dụng: timer trên RunLoop nền
let manager = BackgroundRunLoopManager()
manager.start()
// Gửi tác vụ đến RunLoop nền qua performSelector
manager.perform(
#selector(BackgroundRunLoopManager.backgroundTask),
on: manager.thread,
with: nil,
waitUntilDone: false
)
BackgroundRunLoopManager tạo một thread nền với RunLoop vĩnh viễn. Việc thêm một Port() trống là cần thiết để RunLoop không kết thúc ngay lập tức — nếu không có nguồn, RunLoop.run() trả về false và thoát. performSelector:onThread: gửi tin nhắn đến RunLoop nền — nó sẽ được xử lý khi RunLoop vào pha BeforeSources. Stop gọi CFRunLoopStop trên thread nền, kết thúc vòng lặp.
NSTimer tạo một sự kiện timer mà RunLoop xử lý trong pha BeforeTimers. Timer có thể là repeating (lặp lại) và non-repeating (một lần). NSTimer không đảm bảo độ chính xác: nếu RunLoop bị chặn bởi một thao tác dài, timer sẽ hoạt động sau khi được giải phóng, và tất cả các lần chạy bị bỏ lỡ sẽ được gộp thành một (đối với timer repeating — tối đa một lần chạy “bắt kịp”).
CADisplayLink — timer chuyên biệt, đồng bộ với tốc độ làm mới màn hình (60/120/144 Hz). Nó được sử dụng cho hoạt ảnh và cập nhật video. CADisplayLink được thêm vào RunLoop và hoạt động trước mỗi khung hình render (trước khi Core Animation gửi layer đi render). Nếu một khung hình bị bỏ qua (display link không kịp hoạt động trong 16 ms), lần gọi tiếp theo xảy ra trong chu kỳ VSync tiếp theo.
import UIKit
class AnimationController {
private var displayLink: CADisplayLink?
private var displayLinkTimer: Timer?
private var startTime: CFTimeInterval = 0
// CADisplayLink — hoạt ảnh với vsync
func startDisplayLinkAnimation() {
displayLink = CADisplayLink(target: self,
selector: #selector(step))
// Thêm vào chế độ .common — hoạt động ngay cả khi cuộn
displayLink?.add(to: .current, forMode: .common)
startTime = CACurrentMediaTime()
}
@objc
private func step(displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - startTime
// Được gọi mỗi khung hình (60 FPS → mỗi 16.6 ms)
print("Frame at \(elapsed) seconds")
if elapsed > 5.0 {
displayLink.invalidate() // dừng sau 5 giây
}
}
// NSTimer — tác vụ định kỳ
func startTimerInCommonMode() {
displayLinkTimer?.invalidate()
displayLinkTimer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: true
) { [weak self] timer in
print("Timer tick")
}
// QUAN TRỌNG: chúng ta thêm vào .common, nếu không timer sẽ đóng băng khi cuộn
RunLoop.current.add(displayLinkTimer!, forMode: .common)
}
func stop() {
displayLink?.invalidate()
displayLinkTimer?.invalidate()
}
}
Trong AnimationController, CADisplayLink được thêm vào chế độ .common, đảm bảo rằng step được gọi trên mỗi khung hình bất kể cuộn hay không. displayLink.add(to: .current, forMode: .common) — mẫu tiêu chuẩn cho các hoạt ảnh không nên bị gián đoạn khi cuộn. NSTimer cũng được thêm vào chế độ .common để nó hoạt động trong khi cuộn. Nếu không có điều này, timer sẽ chỉ hoạt động ở chế độ .default.
CFRunLoopObserver — cơ chế theo dõi các pha của RunLoop. Với Observer, bạn có thể nhận thông báo về việc vào chế độ, bắt đầu xử lý timer, bắt đầu xử lý nguồn, thức dậy sau ngủ, thoát chế độ. Các Observer được framework sử dụng cho nhu cầu riêng: Core Animation dùng chúng để render layer trước khi RunLoop ngủ, UIKit — để cập nhật layout sau khi xử lý sự kiện.
Nhà phát triển cũng có thể thêm Observer cho mục đích riêng. Ví dụ: đo thời gian xử lý sự kiện (profiling), thực thi các thao tác trì hoãn trước khi RunLoop ngủ (khi UI đã được cập nhật và người dùng không tương tác), tự động lưu dữ liệu khi không hoạt động kéo dài. Observer được đăng ký thông qua CFRunLoopAddObserver với chỉ định chế độ và mặt nạ bit của các hoạt động được theo dõi.
Các điểm hữu ích nhất cho Observer: .afterWaiting — được thực thi sau khi RunLoop thức dậy và có thể chứa mã cần chạy sau khi xử lý sự kiện; .beforeTimers — trước khi xử lý timer, cho phép đo thời gian đã trôi qua kể từ lần xử lý trước; .exit — kích hoạt khi RunLoop dừng, hữu ích để dọn dẹp tài nguyên của thread nền.
CFRunLoopStop — hàm buộc kết thúc lần lặp hiện tại của RunLoop. Khi gọi CFRunLoopStop(CFRunLoopGetCurrent()), RunLoop kết thúc xử lý sự kiện hiện tại và thoát khỏi run(), trả về false. Đây là cách tiêu chuẩn để dừng RunLoop trên thread nền. Trên Main Thread, CFRunLoopStop không được khuyến nghị — RunLoop chính nên chạy trong suốt vòng đời ứng dụng. Đối với thread nền, sau CFRunLoopStop thread có thể kết thúc hoặc tiếp tục thực thi mã tiếp theo sau run().
Câu hỏi thường gặp
RunLoop — vòng lặp xử lý sự kiện trong iOS, được triển khai bởi CFRunLoop (Core Foundation) và NSRunLoop (Foundation). Nó chờ đợi sự kiện (chạm, timer, nguồn đầu vào) và phân phối chúng đến bộ xử lý trên thread. Mỗi thread có thể có một RunLoop, nhưng tự động nó chỉ được tạo cho Main Thread. RunLoop quản lý các chế độ (.default, .tracking, .common), cách ly xử lý theo ưu tiên.
NSTimer theo mặc định được thêm vào chế độ .default của RunLoop. Khi người dùng cuộn, RunLoop chuyển sang chế độ .tracking và không xử lý timer từ .default. Giải pháp: thêm timer vào chế độ .common qua RunLoop.current.add(timer, forMode: .common). .common kết hợp .default và .tracking, do đó timer hoạt động trong cả hai chế độ.
Chỉ khi thread nền sử dụng timer (NSTimer), performSelector:onThread:, NSInputStream/NSOutputStream hoặc sự kiện Source. Nếu thread thực hiện tác vụ đồng bộ (tải file, tính toán) và kết thúc — không cần RunLoop. Để khởi động, gọi RunLoop.current.run() sau khi cấu hình nguồn. Để dừng — CFRunLoopStop(CFRunLoopGetCurrent()).
RunLoop hoạt động ở cấp độ thread và xử lý sự kiện tuần tự với hỗ trợ chế độ. DispatchQueue — trừu tượng hóa pool thread, các tác vụ được thực thi trên bất kỳ thread trống nào. GCD không hỗ trợ chế độ và sống độc lập với RunLoop. DispatchQueue.main sử dụng RunLoop chính để thực thi khối — đây là điểm giao nhau duy nhất. Đối với tác vụ nền, GCD được ưu tiên hơn.
CADisplayLink — timer đồng bộ với VSync (tốc độ làm mới màn hình). Nó được thêm vào RunLoop và hoạt động trước mỗi khung hình render trong pha BeforeTimers. CADisplayLink chỉ hoạt động trên Main Thread vì việc render màn hình diễn ra ở đó. Để có hoạt ảnh liên tục khi cuộn, hãy thêm nó vào chế độ .common: displayLink.add(to: .current, forMode: .common).
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm