IOS RunLoop詳解以及API使用
使用RunLoop的目的:
1) 使用埠或自定義輸入源來 和其他執行緒通訊
2) 使用執行緒的定時器; ( 在子執行緒中新增定時器 )
3) cocoa中使用任何performSelector...的方法
4) 使執行緒長期性工作
否則,開啟一個執行緒的RunLoop沒有意義
一 獲取/建立RunLoop物件
蘋果不允許直接建立RunLoop,它提供了兩個自動獲取的函式: CFRunLoopGetMain()和CFRunLoopGetCurrent(). 執行緒和RunLoop之間是一一對應的。執行緒建立時並沒有RunLoop,且如果不去獲取,那麼RunLoop一直都不存在. RunLoop的建立發生在第一次獲取的時候( 除了主執行緒的RunLoop,你只能在一個執行緒的內部獲取自己對應的RunLoop ) ;RunLoop的銷燬發生線上程結束後;
1) [NSRunLoop currentRunLoop];
2) [NSRunLoop mainRunLoop];
3) CFRunLoopGetCurrent();
4) CFRunLoopGetMain();
5) [NSRunLoop currentRunLoop].getCFRunLoop; //NSRunLoop轉CFRunLoopRef
主執行緒的RunLoop是程式開始就建立的;子執行緒的RunLoop在第一次獲取RunLoop物件時建立的;
二. 配置RunLoop ( 輸入源source0,source1, timer, observer )
2.1 RunLoop的組成結構
CFRunLoopRef RunLoop物件
CFRunLoopModeRef RunLoop中的模式,每次只能執行在一種模式下
CFRunLoopSourceRef 輸入源source0,source1
CFRunLoopTimerRef 輸入源timer
CFRunLoopObserverRef RunLoop的觀察者物件
RunLoop結構體的內部結構:
struct __CFRunLoop {
CFStringSetRef _commonModes; //set
CFMutableSetRef _commonModeItems; //set
CFMutableSetRef _modes; //set
CFRunLoopMideRef _currentMode ; //當前執行的mode
}
1. commonModeItems 能增加common表示的mode; modes也能增加多個RunLoopModeRef;
2. RunLoop支援查詢當前執行緒所執行的Mode, currentMode;
3. RunLoop中存在一個common集合,用來組合幾種mode,讓其在commonMode時能並存執行;
RunLoopMode結構體的內部結構
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _source0; //set
CFMutableSetRef _source1; //set
CFMutableArrayRef _observers; //Array
CFMutableArrayRef _timers; //Array
}
1.每個Mode都有一個名字,用來區分不同的Mode, 以及加入commonMode;
2.管理事件輸入源集合: source0, source1;
3. 管理timer輸入源: timer;
4.該Mode執行時,RunLoop所處狀態的觀察者集合,用來獲取RunLoop所處的不同狀態;
可以列印 NSLog(@"%@",[NSRunLoop currentRunLoop]);來知道
(1) RunLoop的執行狀態(stop or run),
(2) 在哪種mode(KCFRunLoopDefaultMode)下執行
(3) 以及註冊為CommondMode的源或者timer;
(4) source, timer,observer的數量
關係:
1. CFRunLoopRef代表 RunLoop的執行模式;
2. RunLoop包含若干個Mode, 而每個Mode中又註冊了若干個<Set>Source, <Set>timer, <Array>observer; 事件輸入源.timer等不直接與RunLoop有關聯,而是註冊在Mode中, RunLoop每次只能在一種Mode下執行,只有註冊在此Mode下的source,timer,observer才能被執行和反饋;
3. 如果需要切換Mode,只能退出runLoop,重新指定一個Mode進入。這樣不同組Mode註冊的source,timer,observer相互獨立;
CFRunLoopModeRef 沒有對外暴露,只有系統註冊的5中Mode;
CFRunLoopSourceRef
是事件產生的地方. source有兩個版本:source0和source1.
(1) source0, 只包含一個回撥(函式指標),它並不能主動觸發事件。使用時,需要先呼叫CFRunLoopSourceSignal(source),將這個Source標記為待處理,然後手動呼叫CFRunLoopWajeUp(runLoop)來喚醒RunLoop,讓其處理這個事件;
(2) source1, 包含一個mach_port和一個回撥,被用於通過核心和其他執行緒相互發送訊息.這種source能主動喚醒RunLoop的執行緒;
CFRunLoopTimerRef
是基於時間的觸發器,和NSTimer可以混用.其包含一個時間長度和回撥;當其加入到RunLoop時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥.
CFRunLoopObserverRef
是觀察者,每個Observer都包含一個回撥, 當RunLoop的狀態發生變化時,觀察者就能通過回撥接受這個變化;
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
上面的source,timer,obsercer被統稱為modeItem, 一個item可以被同時加入多個mode,一個item被重複加入同一個mode時不會有效果.如果一個mode中一個item都沒有,則RunLoop會直接退出,不進入迴圈;
2.1RunLoop是以指定的Mode執行的,指定的Mode必須存在一個輸入源或者Timer,否則在進入Loop之前RunLoop就退出了;
1) 輸入源 source0, source1
2) 定時器 timer
3) 觀察者observer
2.2 生成觀察者observer
observer只能通過CFRunLoopRef物件的API去新增; 生成並向RunLoop中加入observer:
CFRunLoopObserverContext context = {0, (__bridge void *)(self),NULL,NULL,NULL}; //不註冊任何回撥函式
CFRunLoopObserverRef observer1 = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, test, &context);
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer1, kCFRunLoopDefaultMode);
void test( CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info ){ }
2.3 生成自定義事件source (source0)
2.3.1 CFRunLoopSourceContext
typedef struct {
CFIndexversion;
void *info;
const void *(*retain)(const void *info);
void(*release)(const void *info);
CFStringRef(*copyDescription)(const void *info);
Boolean(*equal)(const void *info1, const void *info2);
CFHashCode(*hash)(const void *info);
void(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void(*cancel)(void *info, CFRunLoopRef rl,CFRunLoopMode mode);
void(*perform)(void *info);
} CFRunLoopSourceContext;
//輸入源的上下文物件,用來當該輸入源狀態發生變化時,進行回撥
CFRunLoopSourceContext context = {
0,
(__bridge void *)(self),
NULL,
NULL,
NULL,
NULL,
NULL,
/* source註冊到Mode時候的回撥 void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/
schedule,
/* source從Mode中刪除時候的回撥 void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/
cancel,
/* source在RunLoop中執行時的回撥 void perform( void * info ){}*/
perform
};
void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }
void cancel( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }
void perform( void * info ){ }
2.3.2 CFRunLoopSourceRef
NSTimer,CFRunLoopTimerRef 或 CFRunLoopObserverRef註冊在RunLoop中的某種Mode下之後,會根據時間,或者runLoop的狀態自動執行回撥,但是source0卻需要程式設計師自己在其他執行緒中去傳送訊號;, 下面介紹CFRunLoopSourceRef(source0)的註冊,回撥使用,以及移除;
(1)建立source的環境,設定註冊,執行,刪除三個對應的回撥;
CFRunLoopSourceContext context = {
0,
(__bridge void *)(self),
NULL,
NULL,
NULL,
NULL,
NULL,
schedule,
cancel,
perform
};
(2) 建立source (source0)
self.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
(3) 註冊到指定Mode中
self.runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource( self.runLoop, self.source, kCFRunLoopDefaultMode );
(4) Source所處狀態回撥:( 3中狀態:註冊,執行,刪除 )
因為註冊,刪除都會自動觸發對應的回撥方法;
關於"執行"回撥的呼叫: 因為我們註冊的事件都是source0,所以沒有對應的訊號能夠喚醒你註冊的事件,而且CFRunLoopSouorceSignal沒有喚醒RunLoop對應執行緒的能力 ,那麼只有你自己去喚醒了; 使用如下方法去喚醒該source;
CFRunLoopSouorceSignal( self.source );
CFRunLoopWakeup( self.runLoop );
(5) Source從Mode中移除:
CFRunLoopSourceInvalidate(self.source); 此時會有刪除回撥;
2.3.3 生成基於時間的Timer
NSTimer在RunLoop中註冊:
NSTimer * timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
CFRunLoopTimerRef在RunLoop中註冊
CFRunLoopTimerContext context = { 0, NULL,NULL,NULL,NULL };
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 1, 0.1, 0, 0, callback, &context);
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
2.3.4 生成基於埠的源:
NSPort, NSMachPort,CFMessagePortRef
現在ios系統不允許生成帶有名字的port,否則直接報錯並且crash; 只能使用匿名port,匿名port是不會被回撥的;現在的唯一作用就是如AFNetworking中使用一樣,註冊一個匿名port,讓RunLoop能進入Loop不退出;
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
2.2 執行緒安全與RunLoop物件
CFRunLoopRef執行緒安全
NSRunLoop執行緒不安全,所以操作應該在runloop自身對應的執行緒的中完成;
三.啟動RunLoop
3.1 啟動的API:
1) [[NSRunLoop currentRunLoop] run]; //無條件且以預設的NSDefaultRunLoopMode啟動
2) [[NSRunLoop currentRunLoop] runUntilDate:[NSDate new]]; //指定過期時間且以預設的NSDefaultRunLoopMode啟動
3) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate new]];//指定過期時間,指定啟動方式
4) CFRunLoopRun(); //子執行緒的runLoop需要啟動
5) CFRunLoopRunInMode(<#CFRunLoopMode mode#>, <#CFTimeInterval seconds#>, <#Boolean returnAfterSourceHandled#>)
3.2 Int32 result = CFRunLoopRunInMode( kCFRunLoopDefaultMode. 10, YES );
if( result==kcFRunLoopRunStopped || result==kCFRunLoopFinished )
{… }
注意: RunLoop啟動的Mode,必須存在至少一個輸入源或者Timer,否則無法進入Loop,其中並不包括Observer;
四.退出RunLoop
讓RunLoop退出的方法:
(1)給RunLoop設定超時時間;
(2) 通知RunLoop停止
CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
(3) 刪除Mode中的所有輸入源; (這種方法不太好用)
五.雜談
1. 因為Mode是以stringModeName去對應相應的CFRunLoopModeRef,且CFRunLoopModeRef並沒有建立方式,所以我們能使用的只有兩種, KCFRunLoopDefaultMode 和 UITrackingRunLoopMode; 一般就是用的KcFRunLoopDefaultMode;
2. 關於kCFRunLoopCommonModes,
為什麼這個不算能使用中的一種,這種只是timer,source新增進Mode中的一種方式,被新增進來的timer,source會被具有common標記 的Mode所共有;即RunLoop 並不會有kCFRunLoopCommonModes這種執行狀態,只是將該Mode下注冊的timer,source0讓其他mode共有;
至於如何讓其他Mode具有common標誌,不用擔心,你知道的KCFRunLoopDefaultMode 和 UITrackingRunLoopMode都已經加上了common標誌;
3.關於定時器與頁面滑動:
很多人在不懂之前,寫的定時器與頁面滑動的事件相沖突,即頁面滑動時,定時器不工作: 主要原因是頁面滑動時,主執行緒的RunLoop會停止,並且以 UITrackingRunLoopMode形式啟動,這個時候schedule方式生成的NSTimer處在KCFRunLoopDefaultMode下,所以不會被執行;解決辦法時,將NSTimer註冊到kCFRunLoopCommonModes下,則NSTimer在Mode切換時仍然可以執行;
4.runLoop的執行流程:
(1) 以指定Mode啟動之後,根據ModeNameString生成對應Mode,檢查當前Mode中是否存在Item(source,timer),(observer不算),如果沒有,RunLoop直接返回;如果存在Item,則(2);
(2) 通知Observer,RunLoop即將進入Loop; 在 CFRunLoopRun()之後的程式表示式在RunLoop執行時不會指定到的,因為進入了loop;
(3) 通知Observer,即將觸發timer回撥;
(4) 通知observer,即將出發source0(非port)回撥;
(5) 執行source0的回撥;
(6) 檢查是否有source1(基於port)處於ready狀態,如果有就直接處理source1,然後跳轉去訊息處理;
(7) 如果runLoop沒有任何任務,就通知observer,RunLoop即將進入休眠(sleep),否則跳過休眠;
(8)在休眠的RunLoop被喚醒 , 比如CFRunLoopWakeup( self.runLoop );喚醒之後,處理該事件,並且開始新的一輪loop;
5. 主執行緒中的RunLoop狀態函式:
{
/// 1. 通知Observers,即將進入RunLoop
/// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do{
/// 2. 通知 Observers: 即將觸發 Timer 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發 Source0 (非基於port的) 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進入休眠
/// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,執行緒被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的,回撥Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的,執行所有呼叫 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while(...);
/// 10. 通知Observers,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
尤其注意 mach_msg() -> mach_msg_trap() 睡眠狀態;
6 關於定時器NSTimer, CFRunLoopTimerRef
NSTimer有個屬性叫做tolerance(寬容度),標記了當時間到點之後容許的最大誤差;如果這個時間點錯過了,那麼就需要等待下一個時間點到來了;
重點:
(1)關於UI介面重新整理:
當在操作UI時,比如改變frame,更新了UIView/CALayer的層次時,或手動呼叫了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法後,這個UIView/CALayer就被標 記為待處理,並被提交到一個全域性的容器中. 蘋果註冊了一個Observer監聽BeforeWaiting(即將進入休眠)和Exit(即將退出Loop)事件,回撥去執行一個很長的函式。
ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。
這個函式裡會便利所有待處理的UIView/CALayer以執行實際的繪製和調整,並且更新UI介面。( UIView/CALayer的實際繪製和調整必須在主執行緒中,但是待處理標記可以在其他執行緒中操作 );
(2)網路中的執行 NSRULConnection
source1 RunLoop CFMultiplexer CFHTTPCooklie storage
CFSocket —-—--------------------------------———-> NSRULconnectionLoader ——————————------------------------——> delegate
在使用NSURLConnection時,你會傳入一個Delegate,當呼叫[connection start]後,這個Delegate就會不停地收到事件回撥。 實際上,start這個函式的內部會獲取CurrentRunLoop,然後在其中的KCFDefaultMode中新增4個source0(需要手動觸發的source). CFMultiplexerSource是負責各種Delegate回撥的,CFHTTPCookieStorage是處理各種Cookie的.
當開始網路傳輸時,我們可以看到NSURLConnection建立了兩個新的執行緒Lcom.apple.NSURLConnectionLoader和com,apple,CFSocked.private.CFSocket負責最底層的socket連線,NSURLConnectionLoader這個執行緒內部會使用RunLoop來接受底層socket的事件,並通過之前新增的Source0通知上層的Delegate; NSURLConnectionLoader中的RunLoop通過一些基於mach port的source接受來自底層CFSocket的通知,當收到通知後,其會在適合的時機向CFMutliplexSource等source0傳送通知,同時喚醒Delegate執行緒的RunLoop來讓其處理這些通知.CFMultiplexerSource會在Delegate執行緒的RunLoop對Delegate執行實際回撥;
7. 當子執行緒進入runLoop,如何註冊事件或拋給該執行緒執行某些任務:
由於IOS禁止了基於埠port的通訊,所以只能用系統自帶的方法:
[self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];
[self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];
將任務拋給指定的執行緒,且會註冊到對應的RunLoop,這種方法會被立刻執行;