本文Demo傳送門 RunloopDemo
前言
OSX / iOS 系統中,提供了兩個這樣的物件:NSRunLoop 和 CFRunLoopRef。
-
CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函式的 API,所有這些 API 都是執行緒安全的。
-
NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了物件導向的 API,但是這些 API 不是執行緒安全的。
1. 如何檢視RunLoop原始碼
1.1 NSRunLoop原始碼
NSRunLoop是Foundation框架裡面的一個類,它的標頭檔案可以在工程裡面這樣檢視:
至於它的實現檔案,暫時沒有找到公開的資料。
1.2 CFRunLoopRef原始碼
CFRunLoopRef 的程式碼是開源的,你可以在這裡 opensource.apple.com/tarballs/CF… 下載到整個 CoreFoundation 的原始碼。為了方便跟蹤和檢視,你可以新建一個 Xcode 工程,把這堆原始碼拖進去看。
更多蘋果原始碼下載
蘋果公開的原始碼在這裡可以下載,opensource.apple.com/tarballs/
例如,其中,有兩個比較常見需要學習原始碼的下載地址:
- runtime的原始碼在opensource.apple.com/tarballs/ob…
- runloop(其實是整個 CoreFoundation)的原始碼在opensource.apple.com/tarballs/CF…
當然,如果你想在github上線上檢視原始碼,可以點這裡:runtime,runloop
2. 簡析RunLoop原始碼
2.1 Foundation相關Runloop的原始碼
NSRunLoop
@interface NSRunLoop : NSObject {
@private
id _rl;
id _dperf;
id _perft;
id _info;
id _ports;
void *_reserved[6];
}
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
@property (class, readonly, strong) NSRunLoop *mainRunLoop API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
@property (nullable, readonly, copy) NSRunLoopMode currentMode;
- (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED;
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (nullable NSDate *)limitDateForMode:(NSRunLoopMode)mode;
- (void)acceptInputForMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
@end
複製程式碼
2.2 Core Foundation相關Runloop的原始碼
__CFRunLoop
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
複製程式碼
__CFRunLoopMode
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
複製程式碼
__CFRunLoopSource
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
複製程式碼
__CFRunLoopObserver
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
複製程式碼
__CFRunLoopTimer
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
複製程式碼
3. Runloop的基本操作
3.1 如何建立執行緒對應的 Runloop?
蘋果不允許直接建立 RunLoop,它只提供了兩個自動獲取的函式:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 當然,Foudation 框架也有對應的API。
Foundation
NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop]; // 獲得主執行緒對應的 runloop物件
NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop]; // 獲得當前執行緒對應的runloop物件
複製程式碼
Core Foundation
CFRunLoopRef maiRunloop = CFRunLoopGetMain(); // 獲得主執行緒對應的 runloop物件
CFRunLoopRef maiRunloop = CFRunLoopGetCurrent(); // 獲得當前執行緒對應的runloop物件
複製程式碼
3.2 底層如何獲取RunLoop物件?
獲得runloop實現 (建立runloop)
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
複製程式碼
- 【由上原始碼可得】:RunLoop 和 執行緒關係
- 1.每條執行緒都有唯一的一個與之對應的RunLoop物件。
- 2.主執行緒的RunLoop已經自動建立,子執行緒的RunLoop需要主動建立。
- 3.RunLoop在第一次獲取時建立,線上程結束時銷燬。
Runloop 物件是利用字典來進行儲存,而且 Key:執行緒 -- Value:執行緒對應的 runloop。
3.3 RunLoop物件如何執行?
① CFRunLoopRun
RunLoop 其實內部就是do-while迴圈,在這個迴圈內部不斷地處理各種任務(比如Source、Timer、Observer
),通過判斷result的值實現的。所以 可以看成是一個死迴圈。如果沒有RunLoop,UIApplicationMain 函式執行完畢之後將直接返回,就是說程式一啟動然後就結束;
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
複製程式碼
原始碼得知:
- kCFRunLoopDefaultMode,預設情況下,runLoop是在這個mode下執行的,
- runLoop的執行主體是一個do..while迴圈,除非停止或者結束,否則runLoop會一直執行下去
② CFRunLoopRunInMode
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
複製程式碼
該方法,可以設定runLoop執行在哪個mode下modeName,超時時間seconds,以及是否處理完事件就返回returnAfterSourceHandled。
這兩個方法實際呼叫的是同一個方法CFRunLoopRunSpecific,其返回是一個SInt32型別的值,根據返回值,來決定runLoop的執行狀況。
4. RunLoop與執行緒
首先,iOS 開發中能遇到兩個執行緒物件: pthread_t 和 NSThread。過去蘋果有份文件標明瞭 NSThread 只是 pthread_t 的封裝,但那份文件已經失效了,現在它們也有可能都是直接包裝自最底層的 mach thread。蘋果並沒有提供這兩個物件相互轉換的介面,但不管怎麼樣,可以肯定的是 pthread_t 和 NSThread 是一一對應的。比如,你可以通過 pthread_main_np()
或 [NSThread mainThread]
來獲取 主執行緒;也可以通過 pthread_self()
或 [NSThread currentThread]
來獲取 當前執行緒。CFRunLoop 是基於 pthread 來管理的。
蘋果不允許直接建立 RunLoop,它只提供了兩個自動獲取的函式:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。從上面的程式碼(第3.2節)可以看出,執行緒和 RunLoop 之間是一一對應的,其關係是儲存在一個全域性的 Dictionary 裡。執行緒剛建立時並沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生線上程結束時。你只能在一個執行緒的內部獲取其 RunLoop(主執行緒除外)。
5. RunLoop的五個類
在 Core Foundation 裡面關於 RunLoop 有5個類:
序號 | 類 | 說明 |
---|---|---|
1 | CFRunloopRef | 【RunLoop本身】 |
2 | CFRunloopModeRef | 【Runloop的執行模式】 |
3 | CFRunloopSourceRef | 【Runloop要處理的事件源】 |
4 | CFRunloopTimerRef | 【Timer事件】 |
5 | CFRunloopObserverRef | 【Runloop的觀察者(監聽者)】 |
他們的關係如下:
5.1 CFRunLoop
① 大致結構
CFRunLoop 的結構大致如下:
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
複製程式碼
② CommonModes
如上,有個概念叫 CommonModes:一個 Mode 可以將自己標記為"Common"屬性:通過將其 ModeName 新增到 RunLoop 的 commonModes 中。例如:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
複製程式碼
③ CommonModeItems
如上所示,新增 source 的時候,如果 modeName 傳入kCFRunLoopCommonModes 或者 NSRunLoopCommonModes,則該 source 會被儲存到 RunLoop 的 _commonModeItems 中,而且,會被新增到 commonModes 中的所有mode中去。
其實,每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 裡的 Source/Observer/Timer 同步到具有 Common 標記的所有Mode裡。
④ 場景舉例
主執行緒的 RunLoop 裡有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為 Common 屬性。
DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你建立一個 Timer 並加到 DefaultMode 時,Timer 會得到重複回撥,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回撥,並且也不會影響到滑動操作。
有時你需要一個 Timer,在兩個 Mode 中都能得到回撥,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 commonModeItems 中。commonModeItems 被 RunLoop 自動更新到所有具有 Common 屬性的 Mode 裡去。
⑤ 特點
一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個Source/Timer/Observer。但是,執行的時候,一條執行緒對應一個 Runloop,Runloop 總是執行在某種特定的CFRunLoopModeRef(執行模式)下。
這是因為,在 Runloop 中有多個執行模式,每次呼叫 RunLoop 的主函式__CFRunloopRun()
時,只能指定其中一個 Mode(稱 CurrentMode)執行, 如果需要切換 Mode,只能是退出 CurrentMode 切換到指定的 Mode 進入,目的以保證不同 Mode 下的 Source / Timer / Observer
互不影響。
每次呼叫 RunLoop 的主函式時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。
Runloop 要有效,mode 裡面 至少 要有一個 timer (定時器事件) 或者是 source (源);
5.2 CFRunLoopMode
① 大致結構
CFRunLoopMode 的結構大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
複製程式碼
② Mode 及操作介面
CFRunLoopModeRef 類並沒有對外暴露,只是通過 CFRunLoopRef 的介面進行了封裝。CFRunLoopRef 獲取 Mode 的介面:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
複製程式碼
我們沒有辦法直接建立一個CFRunLoopMode物件,但是我們可以呼叫CFRunLoopAddCommonMode
傳入一個字串向 RunLoop 中新增 Mode,傳入的字串即為 Mode 的名字,Mode物件應該是此時在RunLoop內部建立的。
這裡看一下CFRunLoopAddCommonMode
原始碼。
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
//看rl中是否已經有這個mode,如果有就什麼都不做
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
//把modeName新增到RunLoop的_commonModes中
CFSetAddValue(rl->_commonModes, modeName);
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
/* add all common-modes items to new mode */
//這裡呼叫CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的時候會呼叫
//__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode物件在這個時候被建立
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
__CFRunLoopUnlock(rl);
}
複製程式碼
可以看得出:
- modeName不能重複,modeName是mode的唯一識別符號
- RunLoop的_commonModes陣列存放所有被標記為common的mode的名稱
- 新增commonMode會把commonModeItems陣列中的所有source同步到新新增的mode中
- CFRunLoopMode物件在CFRunLoopAddItemsToCommonMode函式中呼叫CFRunLoopFindMode時被建立
③ mode item 及操作介面
Source/Timer/Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重複加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入迴圈。
Mode 暴露的管理 mode item 的介面有下面幾個,通過他們我們可以為Run Loop 新增 Source(ModeItem)。
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
複製程式碼
你只能通過 mode name 來操作內部的 mode,當你傳入一個新的 mode name 但 RunLoop 內部沒有對應 mode 時,RunLoop會自動幫你建立對應的 CFRunLoopModeRef。對於一個 RunLoop 來說,其內部的 mode 只能增加不能刪除。
這裡只分析其中 CFRunLoopAddSource
的原始碼
//新增source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rls)) return;
Boolean doVer0Callout = false;
__CFRunLoopLock(rl);
//如果是kCFRunLoopCommonModes
if (modeName == kCFRunLoopCommonModes) {
//如果runloop的_commonModes存在,則copy一個新的複製給set
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
//如果runl _commonModeItems為空
if (NULL == rl->_commonModeItems) {
//先初始化
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
//把傳入的CFRunLoopSourceRef加入_commonModeItems
CFSetAddValue(rl->_commonModeItems, rls);
//如果剛才set copy到的陣列裡有資料
if (NULL != set) {
CFTypeRef context[2] = {rl, rls};
/* add new item to all common-modes */
//則把set裡的所有mode都執行一遍__CFRunLoopAddItemToCommonModes函式
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
//以上分支的邏輯就是,如果你往kCFRunLoopCommonModes裡面新增一個source,那麼所有_commonModes裡的mode都會新增這個source
} else {
//根據modeName查詢mode
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
//如果_sources0不存在,則初始化_sources0,_sources0和_portToV1SourceMap
if (NULL != rlm && NULL == rlm->_sources0) {
rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
}
//如果_sources0和_sources1中都不包含傳入的source
if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
//如果version是0,則加到_sources0
if (0 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources0, rls);
//如果version是1,則加到_sources1
} else if (1 == rls->_context.version0.version) {
CFSetAddValue(rlm->_sources1, rls);
__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
if (CFPORT_NULL != src_port) {
//此處只有在加到source1的時候才會把souce和一個mach_port_t對應起來
//可以理解為,source1可以通過核心向其埠傳送訊息來主動喚醒runloop
CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
__CFPortSetInsert(src_port, rlm->_portSet);
}
}
__CFRunLoopSourceLock(rls);
//把runloop加入到source的_runLoops中
if (NULL == rls->_runLoops) {
rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
}
CFBagAddValue(rls->_runLoops, rl);
__CFRunLoopSourceUnlock(rls);
if (0 == rls->_context.version0.version) {
if (NULL != rls->_context.version0.schedule) {
doVer0Callout = true;
}
}
}
if (NULL != rlm) {
__CFRunLoopModeUnlock(rlm);
}
}
__CFRunLoopUnlock(rl);
if (doVer0Callout) {
// although it looses some protection for the source, we have no choice but
// to do this after unlocking the run loop and mode locks, to avoid deadlocks
// where the source wants to take a lock which is already held in another
// thread which is itself waiting for a run loop/mode lock
rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */
}
}
複製程式碼
通過新增source的這段程式碼可以得出如下結論:
- 如果modeName傳入kCFRunLoopCommonModes,則該source會被儲存到RunLoop的_commonModeItems中
- 如果modeName傳入kCFRunLoopCommonModes,則該source會被新增到所有commonMode中
- 如果modeName傳入的不是kCFRunLoopCommonModes,則會先查詢該Mode,如果沒有,會建立一個
- 同一個source在一個mode中只能被新增一次
④ mode name
蘋果公開提供的 Mode 有兩個:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用這兩個 Mode Name 來操作其對應的 Mode。
蘋果還提供了一個操作 Common 標記的字串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用這個字串來操作 Common Items,或標記一個 Mode 為 "Common"。使用時注意區分這個字串和其他 mode name。
更完整的mode name如下表所示:
mode name | 說明 |
---|---|
kCFRunLoopDefaultMode | App的預設Mode,通常主執行緒是在這個Mode下執行 |
UITrackingRunLoopMode | 介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響 |
UIInitializationRunLoopMode | 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用 |
GSEventReceiveRunLoopMode | 接受系統事件的內部 Mode,通常用不到 |
kCFRunLoopCommonModes | 這是一個佔位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並不是一種真正的Mode |
5.3 CFRunLoopSourceRef (輸入源)
CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。
資料結構(source0/source1):
// source0 (manual): order(優先順序),callout(回撥函式)
CFRunLoopSource {order =..., {callout =... }}
// source1 (mach port):order(優先順序),port:(埠), callout(回撥函式)
CFRunLoopSource {order = ..., {port = ..., callout =...}
複製程式碼
-
Source0:只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動呼叫 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
-
Source1:包含了一個 mach_port 和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息。這種 Source 能主動喚醒 RunLoop 的執行緒,其原理在下面會講到。
5.4 CFRunLoopTimerRef (定時源)
CFRunLoopTimerRef 是基於時間的觸發器,它和 NSTimer 是 Toll-Free Bridged 的,可以混用。其包含一個時間長度和一個回撥(函式指標)。當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。
5.5 CFRunLoopObserverRef (觀察者)
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
};
複製程式碼
6. 實戰
6.1 設定輸入源
① performSelector
performSelector同樣是觸發Source0事件。selector也是特殊的基於自定義的源.理論上來說,允許在當前執行緒向任何執行緒上執行傳送訊息,和基於埠的源一樣,執行selector請求會在目標執行緒上序列化,減緩許多線上程上允許多個方法容易引起的同步問題.不像基於埠的源,一個selector執行完後會自動從run loop裡面移除.
- 主執行緒執行
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
複製程式碼
- 當前執行緒延時執行
// 內部會建立一個Timer到當前執行緒的runloop中(如果當前執行緒沒runloop則方法無效;performSelector:onThread: 方法放到指定執行緒runloop中)
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
複製程式碼
當呼叫上述API,實際上其內部會建立一個 Timer 並新增到當前執行緒的 RunLoop 中。所以如果當前執行緒沒有 RunLoop,則這個方法會失效。
- 指定執行緒執行
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
複製程式碼
當呼叫 performSelector:onThread: 時,實際上其會建立一個Timer加到對應的執行緒去,同樣的,如果對應執行緒沒有 RunLoop 該方法也會失效.
- 當前執行緒指定mode name並延時執行
// 只在NSDefaultRunLoopMode下執行(重新整理圖片)
[self.myImageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@""] afterDelay:ti inModes:@[NSDefaultRunLoopMode]];
複製程式碼
② 自定義輸入源
自定義源:使用CFRunLoopSourceRef 型別相關的函式 (執行緒) 來建立自定義輸入源。
- 呼叫VC
-(void)test {
NSThread* aThread = [[NSThread alloc] initWithTarget:self selector:@selector(testForCustomSource) object:nil];
self.aThread = aThread;
[aThread start];
}
-(void)testForCustomSource{
NSLog(@"starting thread.......");
NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];
// 設定Run Loop observer的執行環境
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
// 建立Run loop observer物件
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer){
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
_source = [[ZXRunLoopSource alloc] init];
[_source addToCurrentRunLoop];
while (!self.aThread.isCancelled)
{
NSLog(@"We can do other work");
[myRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5.0f]];
}
[_source invalidate];
NSLog(@"finishing thread.........");
}
複製程式碼
- 自定義輸入源
- (id)init
{
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,
&RunLoopSourceScheduleRoutine,
RunLoopSourceCancelRoutine,
RunLoopSourcePerformRoutine};
_runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
_commands = [[NSMutableArray alloc] init];
return self;
}
- (void)addToCurrentRunLoop
{
//獲取當前執行緒的runLoop(輔助執行緒)
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, _runLoopSource, kCFRunLoopDefaultMode);
}
/**
* 排程例程
* 當將輸入源安裝到run loop後,呼叫這個協調排程例程,將源註冊到客戶端(可以理解為其他執行緒)
*
*/
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
ZXRunLoopSource *obj = (__bridge ZXRunLoopSource*)info;
// AppDelegate* delegate = [[AppDelegate sharedAppDelegate];
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
RunLoopContext *theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
//傳送註冊請求
[delegate performSelectorOnMainThread:@selector(registerSource:) withObject:theContext waitUntilDone:YES];
}
/**
* 處理例程
* 在輸入源被告知(signal source)時,呼叫這個處理例程,這兒只是簡單的呼叫了 [obj sourceFired]方法
*
*/
void RunLoopSourcePerformRoutine (void *info)
{
ZXRunLoopSource* obj = (__bridge ZXRunLoopSource*)info;
[obj sourceFired];
// [NSTimer scheduledTimerWithTimeInterval:1.0 target:obj selector:@selector(timerAction:) userInfo:nil repeats:YES];
}
/**
* 取消例程
* 如果使用CFRunLoopSourceInvalidate/CFRunLoopRemoveSource函式把輸入源從run loop裡面移除的話,系統會呼叫這個取消例程,並且把輸入源從註冊的客戶端(可以理解為其他執行緒)裡面移除
*
*/
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
ZXRunLoopSource* obj = (__bridge ZXRunLoopSource*)info;
AppDelegate* delegate = [AppDelegate sharedAppDelegate];
RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
[delegate performSelectorOnMainThread:@selector(removeSource:) withObject:theContext waitUntilDone:NO];
}
- (void)sourceFired
{
NSLog(@"Source fired: do some work, dude!");
NSThread *thread = [NSThread currentThread];
[thread cancel];
//既然執行緒沒了,就把AppDelegate快取的runloop也給刪了,以免下次呼叫CFRunLoopWakeUp(runloop);會崩潰,因為只有runloop沒了執行緒
[[AppDelegate sharedAppDelegate].sources removeObjectAtIndex:0];
}
複製程式碼
③ 埠輸入源
配置 NSMachPort 物件
為了和 NSMachPort 物件建立穩定的本地連線,你需要建立埠物件並將之加入相應的執行緒的 run loop。當執行輔助執行緒的時候,你傳遞埠物件到執行緒的主體入口點。輔助執行緒可以使用相同的埠物件將訊息返回給原執行緒。
- VC呼叫
- (void)launchThreadForPort
{
NSPort* myPort = [NSMachPort port];
if (myPort)
{
//這個類持有即將到來的埠訊息
[myPort setDelegate:self];
//將埠作為輸入源安裝到當前的 runLoop
[[NSThread currentThread] setName:@"launchThreadForPort---Thread"];
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
//當前執行緒去調起工作執行緒
MyWorkerClass *work = [[MyWorkerClass alloc] init];
[NSThread detachNewThreadSelector:@selector(launchThreadWithPort:) toTarget:work withObject:myPort];
}
}
複製程式碼
為了線上程間建立雙向的通訊,你需要讓工作執行緒在簽到的訊息中傳送自己的本地埠到主執行緒。主執行緒接收到簽到訊息後就可以知道輔助執行緒執行正常,並且供了傳送訊息給輔助執行緒的方法。
以下程式碼顯示了主執行緒的 handlePortMessage:
方法。當由資料到達執行緒的本地埠時,該方法被呼叫。當簽到訊息到達時,此方法可以直接從輔助執行緒裡面檢索埠並儲存下來以備後續使用。
- VC實現代理
//NSPortDelegate
#define kCheckinMessage 100
//處理從工作執行緒返回的響應
- (void) handlePortMessage: (id)portMessage {
//訊息的 id
unsigned int messageId = (int)[[portMessage valueForKeyPath:@"msgid"] unsignedIntegerValue];
if (messageId == kCheckinMessage) {
//1. 當前主執行緒的port
NSPort *localPort = [portMessage valueForKeyPath:@"localPort"];
//2. 接收到訊息的port(來自其他執行緒)
NSPort *remotePort = [portMessage valueForKeyPath:@"remotePort"];
//3. 獲取工作執行緒關聯的埠,並設定給遠端埠,結果同2
NSPort *distantPort = [portMessage valueForKeyPath:@"sendPort"];
NSMutableArray *arr = [[portMessage valueForKeyPath:@"components"] mutableCopy];
if ([arr objectAtIndex:0]) {
NSData *data = [arr objectAtIndex:0];
NSString * str =[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"");
}
NSLog(@"");
//為了以後的使用儲存工作埠
// [self storeDistantPort: distantPort];
} else {
//處理其他的訊息
}
}
複製程式碼
對於輔助工作執行緒,你必須配置執行緒使用特定的埠以傳送訊息返回給主要執行緒。
以下顯示瞭如何設定工作執行緒的程式碼。建立了執行緒的自動釋放池後,緊接著建立工作物件驅動執行緒執行。工作物件的 sendCheckinMessage:
方法建立了工作執行緒的本地埠併傳送簽到訊息回主執行緒。
- MyWorkerClass.m
- (void)launchThreadWithPort:(NSPort *)port {
@autoreleasepool {
//1. 儲存主執行緒傳入的port
remotePort = port;
//2. 設定子執行緒名字
[[NSThread currentThread] setName:@"MyWorkerClassThread"];
//3. 開啟runloop
[[NSRunLoop currentRunLoop] run];
//4. 建立自己port
myPort = [NSPort port];
//5.
myPort.delegate = self;
//6. 將自己的port新增到runloop
//作用1、防止runloop執行完畢之後推出
//作用2、接收主執行緒傳送過來的port訊息
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
//7. 完成向主執行緒port傳送訊息
[self sendPortMessage];
}
}
複製程式碼
當使用 NSMachPort 的時候,本地和遠端執行緒可以使用相同的埠物件線上程間進行單邊通訊。換句話說,一個執行緒建立的本地埠物件成為另一個執行緒的遠端埠物件。
以下程式碼輔助執行緒的簽到例程,該方法為之後的通訊設定自己的本地埠,然後傳送簽到訊息給主執行緒。它使用 LaunchThreadWithPort:
方法中收到的埠物件做為目標訊息。
- MyWorkerClass.m
- (void)sendPortMessage {
NSString *str1 = @"aaa111";
NSString *str2 = @"bbb222";
arr = [[NSMutableArray alloc] initWithArray:@[[str1 dataUsingEncoding:NSUTF8StringEncoding],[str2 dataUsingEncoding:NSUTF8StringEncoding]]];
//傳送訊息到主執行緒,操作1
[remotePort sendBeforeDate:[NSDate date]
msgid:kMsg1
components:arr
from:myPort
reserved:0];
//傳送訊息到主執行緒,操作2
// [remotePort sendBeforeDate:[NSDate date]
// msgid:kMsg2
// components:nil
// from:myPort
// reserved:0];
}
複製程式碼
注意:上述的一個API中
components
不能直接裝NSString等資料,必須是NSData或者NSPort及其子類的例項物件。完整的API宣告如下所示:
- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;
// The components array consists of a series of instances
// of some subclass of NSData, and instances of some
// subclass of NSPort; since one subclass of NSPort does
// not necessarily know how to transport an instance of
// another subclass of NSPort (or could do it even if it
// knew about the other subclass), all of the instances
// of NSPort in the components array and the 'receivePort'
// argument MUST be of the same subclass of NSPort that
// receives this message. If multiple DO transports are
// being used in the same program, this requires some care.
複製程式碼
實驗驗證
- macOS特殊情況(iOS開發者可忽略)
為了和 NSMeaasgePort 的建立穩定的本地連線,你不能簡單的線上程間傳遞埠物件。遠端訊息埠必須通過名字來獲得。在 Cocoa 中這需要你給本地埠指定一個名字,並將名字傳遞到遠端執行緒以便遠端執行緒可以獲得合適的埠物件用於通訊。以下程式碼顯示埠建立,註冊到你想要使用訊息埠的程式。
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runLoop
{
//當手動呼叫此方法的時候,將會觸發 RunLoopSourceContext的performCallback
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(runLoop);
NSPort *localPort = [[NSMessagePort alloc] init];
// configure the port and add it to the current run loop
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
// register the port using the specific name, and The name is unique
NSString *localPortName = [NSString stringWithFormat:@"MyPortName"];
// there is only NSMessagePortNameServer in the mac os x system
[[NSMessagePortNameServer sharedInstance] registerPort:localPort name:localPortName];
}
複製程式碼
需要注意的是,只能在一個裝置內程式間通訊,不能在不同裝置間通訊。將埠名稱註冊到NSMessagePortNameServer裡面,其他執行緒通過這個埠名稱從NSMessagePortNameServer來獲取這個埠物件。
根據name獲取port的API為:
- (NSPort *)portForName:(NSString *)name;
複製程式碼
- (NSPort *)portForName:(NSString *)name host:(NSString *)host;
複製程式碼
區分:NSPort,NSMessagePort,NSMachPort,NSPortMessage
❶ iOS和macOS都有的類: 在NSPort.h中可找到
- NSPort
@interface NSPort : NSObject <NSCopying, NSCoding>
+ (NSPort *)port;
- (void)invalidate;
@property (readonly, getter=isValid) BOOL valid;
- (void)setDelegate:(nullable id <NSPortDelegate>)anObject;
- (nullable id <NSPortDelegate>)delegate;
- (void)scheduleInRunLoop:(NSRunLoop *)runLoop forMode:(NSRunLoopMode)mode;
- (void)removeFromRunLoop:(NSRunLoop *)runLoop forMode:(NSRunLoopMode)mode;
@property (readonly) NSUInteger reservedSpaceLength;
- (BOOL)sendBeforeDate:(NSDate *)limitDate components:(nullable NSMutableArray *)components from:(nullable NSPort *) receivePort reserved:(NSUInteger)headerSpaceReserved;
- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_WIN32)
- (void)addConnection:(NSConnection *)conn toRunLoop:(NSRunLoop *)runLoop forMode:(NSRunLoopMode)mode NS_SWIFT_UNAVAILABLE("Use NSXPCConnection instead") API_DEPRECATED("Use NSXPCConnection instead", macosx(10.0, 10.13), ios(2.0,11.0), watchos(2.0,4.0), tvos(9.0,11.0));
- (void)removeConnection:(NSConnection *)conn fromRunLoop:(NSRunLoop *)runLoop forMode:(NSRunLoopMode)mode NS_SWIFT_UNAVAILABLE("Use NSXPCConnection instead") API_DEPRECATED("Use NSXPCConnection instead", macosx(10.0, 10.13), ios(2.0,11.0), watchos(2.0,4.0), tvos(9.0,11.0));
#endif
@end
複製程式碼
- NSMessagePort
@interface NSMessagePort : NSPort {
@private
void *_port;
id _delegate;
}
複製程式碼
- NSMachPort
@interface NSMachPort : NSPort {
@private
id _delegate;
NSUInteger _flags;
uint32_t _machPort;
NSUInteger _reserved;
}
複製程式碼
❷ 僅macOS支援的類: 在NSPortMessage.h中可找到
- NSPortMessage
#import <Foundation/NSObject.h>
@class NSPort, NSDate, NSArray, NSMutableArray;
NS_ASSUME_NONNULL_BEGIN
@interface NSPortMessage : NSObject {
@private
NSPort *localPort;
NSPort *remotePort;
NSMutableArray *components;
uint32_t msgid;
void *reserved2;
void *reserved;
}
- (instancetype)initWithSendPort:(nullable NSPort *)sendPort receivePort:(nullable NSPort *)replyPort components:(nullable NSArray *)components NS_DESIGNATED_INITIALIZER;
@property (nullable, readonly, copy) NSArray *components;
@property (nullable, readonly, retain) NSPort *receivePort;
@property (nullable, readonly, retain) NSPort *sendPort;
- (BOOL)sendBeforeDate:(NSDate *)date;
@property uint32_t msgid;
@end
NS_ASSUME_NONNULL_END
複製程式碼
6.2 設定定時源
6.2.1 使用系統Timer
我們的定時器Timer是怎麼寫的呢?一般的做法是,在主執行緒(可能是某控制器的viewDidLoad方法)中,建立Timer。
可能會有兩種寫法,但是都有上面的問題,下面先看下Timer的兩種寫法:
// 第一種寫法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[timer fire];
// 第二種寫法
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
複製程式碼
上面的兩種寫法其實是等價的。第二種寫法,預設也是將timer新增到 NSDefaultRunLoopMode 下的,並且會自動fire。
可能的問題: 1.我們經常會在應用中看到tableView 的header 上是一個橫向ScrollView,一般我們使用NSTimer,每隔幾秒切換一張圖片。可是當我們滑動tableView的時候,頂部的scollView並不會切換圖片,這可怎麼辦呢? 2.介面上除了有tableView,還有顯示倒數計時的Label,當我們在滑動tableView時,倒數計時就停止了,這又該怎麼辦呢?
要如何解決這一問題呢? 解決方法很簡單,我們只需要在新增timer 時,將mode 設定為NSRunLoopCommonModes即可。
- (void)timerTest
{
// 第一種寫法
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[timer fire];
// 第二種寫法,因為是固定新增到defaultMode中,就不要用了
}
複製程式碼
還有一種方案,在子執行緒中新增Timer,也可以解決上面的問題,但是需要注意的是把timer加入到當前runloop後,必須讓runloop 執行起來,否則timer僅執行一次。
//首先是建立一個子執行緒
- (void)createThread
{
NSThread *subThread = [[NSThread alloc] initWithTarget:self selector:@selector(timerTest) object:nil];
[subThread start];
self.subThread = subThread;
}
// 建立timer,並新增到runloop的mode中
- (void)timerTest
{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
NSLog(@"啟動RunLoop前--%@",runLoop.currentMode);
NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
// 第一種寫法,改正前
// NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// [timer fire];
// 第二種寫法
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerUpdate) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}
//更新label
- (void)timerUpdate
{
NSLog(@"當前執行緒:%@",[NSThread currentThread]);
NSLog(@"啟動RunLoop後--%@",[NSRunLoop currentRunLoop].currentMode);
NSLog(@"currentRunLoop:%@",[NSRunLoop currentRunLoop]);
dispatch_async(dispatch_get_main_queue(), ^{
self.count ++;
NSString *timerText = [NSString stringWithFormat:@"計時器:%ld",self.count];
self.timerLabel.text = timerText;
});
}
複製程式碼
timer確實被新增到NSDefaultRunLoopMode中了。可是新增到子執行緒中的NSDefaultRunLoopMode裡,無論如何滾動,timer都能夠很正常的運轉。這又是為啥呢?
這就是多執行緒與runloop的關係了,每一個執行緒都有一個與之關聯的RunLoop,而每一個RunLoop可能會有多個Mode。CPU會在多個執行緒間切換來執行任務,呈現出多個執行緒同時執行的效果。執行的任務其實就是RunLoop去各個Mode裡執行各個item。因為RunLoop是獨立的兩個,相互不會影響,所以在子執行緒新增timer,滑動檢視時,timer能正常執行。
6.2.2 使用自定義Timer
使用下面關鍵兩行即可自定義Timer的事件
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &timerContext);
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
複製程式碼
下面是一個例子:
-(void)testCustomTimer{
// 獲得當前thread的Run loop
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
// 建立Run loop observer物件
// 第一個引數用於分配該observer物件的記憶體
// 第二個引數用以設定該observer所要關注的的事件,詳見回撥函式myRunLoopObserver中註釋
// 第三個引數用於標識該observer是在第一次進入run loop時執行還是每次進入run loop處理時均執行
// 第四個引數用於設定該observer的優先順序
// 第五個引數用於設定該observer的回撥函式
// 第六個引數用於設定該observer的執行環境
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer){
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext timerContext = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
&myCFTimerCallback, &timerContext);
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
NSInteger loopCount = 2;
do{
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
}while (loopCount);
}
void myCFTimerCallback(){
NSLog(@"-----++++-------");
}
複製程式碼
6.3 設定監聽
- 新增監聽
// 設定Run Loop observer的執行環境
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
// 建立Run loop observer物件
// 第一個引數用於分配該observer物件的記憶體
// 第二個引數用以設定該observer所要關注的的事件,詳見回撥函式myRunLoopObserver中註釋
// 第三個引數用於標識該observer是在第一次進入run loop時執行還是每次進入run loop處理時均執行
// 第四個引數用於設定該observer的優先順序
// 第五個引數用於設定該observer的回撥函式
// 第六個引數用於設定該observer的執行環境
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer){
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
複製程式碼
- 監聽回撥
void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
switch(activity)
{
// 即將進入Loop
case kCFRunLoopEntry:
NSLog(@"run loop entry");
break;
case kCFRunLoopBeforeTimers://即將處理 Timer
NSLog(@"run loop before timers");
break;
case kCFRunLoopBeforeSources://即將處理 Source
NSLog(@"run loop before sources");
break;
case kCFRunLoopBeforeWaiting://即將進入休眠
NSLog(@"run loop before waiting");
break;
case kCFRunLoopAfterWaiting://剛從休眠中喚醒
NSLog(@"run loop after waiting");
break;
case kCFRunLoopExit://即將退出Loop
NSLog(@"run loop exit");
break;
default:
break;
}
}
複製程式碼
7. 總結:Runloop與三個類的關係
7.1 CoreFoudation
對於三種mode,新增到runloop的API分別如下:
CF_EXPORT Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
CF_EXPORT Boolean CFRunLoopContainsObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
CF_EXPORT Boolean CFRunLoopContainsTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
CF_EXPORT void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
複製程式碼
7.2 Foundation
對於 埠輸入源 和 定時源 的mode,新增到NSRunloop的API分別如下:
- (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED;
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (nullable NSDate *)limitDateForMode:(NSRunLoopMode)mode;
- (void)acceptInputForMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
複製程式碼
還有一種,對於NSTimer,有一個特別的API,這個API會預設把Timer加到 當前執行緒 中去。
[NSTimer scheduledTimerWithTimeInterval:5.1 target:self selector:@selector(printMessage:) userInfo:nil repeats:YES];
複製程式碼
所以說,當且僅當加到當前執行緒,下面兩個新增NSTimer的方案方可等效:
- (void)defalutTimer {
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doTime) userInfo:nil repeats:YES];
}
- (void)commonTimer {
NSTimer *timer =[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(doTime) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
複製程式碼