Runloop

Có một thành phần rất quan trọng đóng vai trò trái tim của App trong iOS, đó chính là runloop.

Cũng như tên gọi của nó, runloop là một vòng lặp ở thread sử dụng để chạy các trình xử lý sự kiện để đáp ứng các event được gửi đến. Nó giống như một hộp thư mà sẽ đợi thông điệp với và gửi tới đúng chỗ nhận thông điệp đó.

Một runloop cơ bản có thể như sau:

while(1)
{
    dispatch_event()
}
Cấu trúc của runloop

Runloop trong iOS chính là CFRunLoop được implement bằng ngôn ngữ C. Function CFRunLoopRunSpecific() được gọi thông qua public API CFRunLoopRun()CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled) sẽ được gọi lặp đi lặp lại để xử lý event cho đến khi nào runloop kết thúc.

Runloop lắng nghe event từ nhiều loại source khác nhau. Input source cung cấp event không đồng bộ( asynchronous), thường là thông điệp từ luồng khác hoặc ứng dụng khác. Timer source gửi event đồng bộ( synchronous) xảy ra tại một thời điểm theo lịch hoặc thời gian lặp lại.

Một thuật ngữ quan trọng trong CFRunLoop chính là CFRunLoopModes. CFRunLoop làm việc với Sources, Sources được đăng ký trên một hoặc nhiều mode. Và runloop cũng được chạy trên một mode nhất định. Khi có một event được gửi tới từ một source, nó sẽ được xử lý nếu mode của source giống mới mode của runloop. Thường runloop sẽ run trên mode default( kCFRunLoopDefaultMode).

Giả sử một timer được add vào mode Tracking( Mode này dùng để xử lý sự kiện khi scroll), mà runloop đang chạy ở mode Default thì event của timer sẽ không được xử lý bởi runloop.

kCFRunLoopCommonModes

Trong runloop có lưu trữ một mảng commonModes. Khi một source được thêm vào runloop với kCFRunLoopCommonModes thì runloop sẽ tự động apply chúng vào tất cả những mode mà define bên trong mảng commonModes. Mục đích sinh ra kCFRunLoopCommonModes để đăng ký nhiều mode cho một source.

Giả sử trong commonModes đang có 2 mode là Tracking và Default. Một timer nào đó được add vào runloop với mode là kCFRunLoopCommonModes, nếu runloop run ở một trong 2 mode Tracking và Default thì lúc đó event của timer sẽ được xử lý.

Input Sources

Input sources gửi các sự kiện không đồng bộ tới thread. Source của event phụ thuộc vào loại input source, thường là một trong 2 loại port-based(source1) và custom source(non port-based, Hay còn gọi là source0). Sự khác biệt giữa chúng là cách chúng được báo hiệu. Port-base được báo hiệu tự động thông bởi Kernel, còn custom source được báo hiệu bằng tay từ một thread khác.

Khi bạn tạo một input source, bạn phải xác định một hoặc nhiều mode( sử dụng kCFRunLoopCommonModes).

Port-Based Sources( Source1)

Trong Cocoa, bạn chỉ việc tạo ra port object và sử dụng phương thức scheduleInRunLoop:forMode: trong NSPort để add port đó vào runloop.

Trong Core Foundation(CF), bạn phải tự tạo cả port và runloop source.

Custom Input Sources( Source0)

Để tạo custom input source, bạn phải sử dụng phương thức CFRunLoopSourceCreate trong Core Foundation.

Cocoa Perform Selector Sources

Khi chúng ta sử dụng hàm performSelector, Cocoa sẽ tạo ra một custom input source và thêm vào runloop, Selector sẽ được chạy ở thread được chỉ định. Cocoa Perform Selector Sources sẽ được xoá khỏi runloop sau khi nó được gọi trên selector đã xác định.

Lưu ý: Khi gọi performSelector sang thread khác, thì bắt buộc thread đó phải đang có một runloop đang hoạt động.

Timer Sources

Timer source cung cấp các event cho thread của bạn tại một thời điểm định sẵn trong tương lai một cách đồng bộ( synchronous). Timer là một cách để thread thông báo một điều gì đó.

Mặc dù nó tạo ra các thông báo dựa trên thời gian, tuy nhiên timer không phải một cơ chế real-time. Giống như input sources, timer được liên kết với một hoặc nhiều mode của runloop. Khi thời gian định trước của timer xảy ra sau khi hàm check timer được chạy thì timer phải đợi tới vòng lặp tiếp theo mới được callback.

Run Loop Observers

Runloop hỗ trợ chúng ta việc theo dõi một số hoạt động chính của runloop để phục vụ cho mục đích nào đó. Bạn có thể theo dõi những hoạt động sau đây:

  • Bắt đầu vào vòng lặp runloop
  • Chuẩn bị xử lý timer
  • Chuẩn bị xử lý sources
  • Chuẩn bị sleep, đợi source hoặc timer được kích hoạt.
  • Sau khi runloop được đánh thức.
  • Kết thúc vòng lặp runloop

Runloop xử lý như thế nào

Mỗi một vòng lặp runloop thực hiện xử lý event như sau:

  1. Thông báo cho observers biết đã bắt đầu chạy runloop.
  2. Thông báo cho observers biết timer đã sẵn sàng được kích hoạt.
  3. Thông báo chuẩn bị xử lý sources.
  4. Xử lý block và source0.
  5. Thông báo chuẩn bị sleep nếu có.
  6. Thread sẽ sleep cho tới khi một trong những event dưới đây xảy ra:
    – Một event tới từ port-based input source(source1).
    – Một timer được kích hoạt.
    – Runloop timeout.
    – Runloop được đánh thức chủ động( Gọi hàm CFRunLoopWakeUp).
  7. Thông báo runloop sau khi được đánh thức.
  8. Xử lý event đang đợi xử lý
    – Xử lý timer.
    – Xử lý GCD async main.
    – Xử lý source1.
    – Xử lý block.
    * Kiểu tra điều kiện kết thúc vòng lặp: Finished, Stopped, TimedOut, HandledSource nếu một trong các điều kiện xảy ra thì tới bước 9. Ngược lại thì quay lại bước 2.
  9. Thông báo cho observers rằng runloop đã kết thúc.

Có một số bước đã bị loại bỏ so với document của apple bởi vì source code đã thay đổi.

Điều kiện kết thúc vòng lặp:

  • kCFRunLoopRunTimedOut: Hết thời gian lặp
  • kCFRunLoopRunFinished: runloop rỗng, ví dụ tất cả source, timer đều đã bị xoá hết.
  • kCFRunLoopRunHandledSource: Source1 đã được xử lý, event đã được gửi đi.
  • kCFRunLoopRunStopped: runloop được chủ động dừng khi gọi CFRunLoopStop()

Các hàm xử lý event trong runloop

Chắc hắn các bạn khi debug chương trình mà breakpoint được dừng chắc cũng thấy ở callstack function kiểu như __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__

Mỗi vòng lặp của của runloop sẽ thực hiện dispatch event bằng những function sau:

static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg)

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)

static void _CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info)

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void))

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info)

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(void (*perform)(void *), void *info)

Mỗi function sử lý một kiểu source khác nhau. Ta sẽ xem xét từng cái.

CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE

Khi bạn gọi dispatch_async sang Main Queue thì runloop sẽ xử lý event này. và function này để xử lý gọi block đó.

CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION

Bằng cách sử dụng CFRunLoopObserver bạn có thể theo dõi hoạt động của runloop ví dụ như bắt đầu runloop, kết thúc runloop,… Có 7 option để bạn có thể theo dõi:kCFRunLoopEntry, kCFRunLoopBeforeTimers, kCFRunLoopBeforeSources, kCFRunLoopBeforeWaiting, kCFRunLoopAfterWaiting, kCFRunLoopExit, kCFRunLoopAllActivities. Function này chính là dùng để gọi callback cho observer runloop.

CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION

Function này để gọi hàm callback của timer hoặc khi bạn sử dụng performSelector:afterDelay:.

CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK

Khi bạn sử dụng function CFRunLoopPerformBlock() để thực hiện chạy block trong vòng lặp tiếp theo của runloop. Thì hàm CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK sẽ được sử dụng để invoke block này.

CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION

Hàm xử lý event source0. Source0 phải được gửi signal bằng tay và gọi CFRunLoopWakeUp từ trong ứng dụng.

CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION

Hàm xử lý event source1. Source1 được gửi signal tự động tới từ Kernel.

Best practice

Khi sử dụng realm observe, thì chúng ta chỉ được sử dụng trên thread nào có runloop. Giả sử chúng ta dùng main thread thì ta không cần làm gì cả vì main thread đã chạy sẵn runloop. Tuy nhiên khi chúng ta muốn chạy trên thread khác, thì chúng ta phải chạy một runloop, sau đó khi cần gọi code observe ta cần dùng hàm CFRunLoopPerformBlock thay vì dispatch_async để chuyển luồng.

Tổng kết

Như vậy là qua bài viết chúng ta đã nắm được khái niệm runloop, các loại mode, nguyên lý hoạt động của runloop.

Cảm ơn các bạn đã đọc bài viết.

Tham khảo thêm:

https://bou.io/RunRunLoopRun.html

https://www.jianshu.com/p/0129d1fee378

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html

Leave a Reply