Apple documentphp
Run loops 是線程相關底層基礎的一部分。它的本質和字面意思同樣運行着的循環(事件處理的循環),做用:接受循環事件和安排線程的工做。目的:讓線程在有任務的時候忙於工做,而沒任務的時候處於休眠狀態。html
Run loop 的管理並不是徹底自動。你仍然須要設置線程代碼在合適的時候啓動 run loop
來幫助你處理輸入事件。iOS 中 Cocoa 和 CoreFoundation 框架中各有完整的一套關於 runloop
對象的操做api,在主線程中 run loop
是自動建立並運行(在子線程開啓RunLoop 須要手動建立且手動開啓
)。git
Runloop 儘管在平時多數開發者不多直接使用,可是理解 RunLoop 能夠幫助開發者更好的利用多線程編程模型,同時也能夠幫助開發者解答面試套路的一些疑惑,對於 iOS 編程 熟知它是必不可少的,下面是我對 Runloop 的整理,將以一勞永逸的心態,漸進式學習的目地,而且帶有幾個實戰開發場景。 --> 大神可選擇性路過「思想」。github
RunLoop 該模塊學習將續更 ~面試
在「時間 & 知識 」有限內,總結的文章不免有「未全、不足 」的地方,還望各位好友指出,可留言指正或是補充,以提升文章質量@白開水ln原著;編程
目錄api
- Runloop 概念
- Runloop 做用
- Runloop 開啓&退出
- Runloop 和線程關係
1.如何建立子線程對應的 Runloop ?- Runloop 獲取
- Runloop 源碼
- Runloop 相關5個類
- Runloop 相關類(Mode)
- Runloop 相關類(Source)
- Runloop 相關類(Timer)
- Runloop 相關類(Observer)
- Runloop 相關5個類代碼示例
- Runloop 應用\場景
1.Runloop 經典應用:常駐線程
2.AutoreleasePool 自動釋放池
3.UI更新
4.UIImageView 延遲加載圖片
5.UITableView 與 NSTimer 衝突- Runtime & Runloop 面試最常問到的題整理【建議看】
- Runloop 模塊博文推薦(❤️數量較多)
- Demo 重要的部分代碼中都有相應的註解和文字打印,運行程序能夠很直觀的表現。
- SourceCode 、 ToolsClass、WechatPublic-Codeidea
- Runtime 模塊詳解「面試、工做」看我就 🐒 了 _.
RunLoop
指的是 NSRunloop (Foundation框架)
或者 CFRunloopRef (CoreFoundation 框架)
,CFRunloopRef
是純C的函數,而 NSRunloop
僅僅是 CFRunloopRef
的一層OC封裝,並未提供額外的其餘功能,所以要了解 RunLoop
內部結構,須要多研究 CFRunLoopRef API(Core Foundation \ 更底層
)。按照OC的思路咱們能夠將RunLoop當作一個對象
),這個對象的運行纔是咱們一般意義上說的運行循環,核心方法是 __CFRunloopRun() 查看下(附:源碼)
。如:程序一啓動就會開啓一個主線程(中的 runloop 是自動建立並運行),runloop 保證主線程不會被銷燬,也就保證了程序的持續運行
)。如:touches 觸摸事件、NSTimer 定時器事件、Selector事件(選擇器 performSelector)
)。有事情就作事情,沒事情就休息 (其資源釋放)
)。附:CFRunLoop.c 源碼緩存
#【用DefaultMode啓動,具體實現查看 CFRunLoopRunSpecific Line2704】 #【RunLoop的主函數,是一個死循環 dowhile】 void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { /* 參數一:CFRunLoopRunSpecific 具體處理runloop的運行狀況 參數二:CFRunLoopGetCurrent() 當前runloop對象 參數三:kCFRunLoopDefaultMode runloop的運行模式的名稱 參數四:1.0e10 runloop默認的運行時間,即超時爲10的九次方 參數五:returnAfterSourceHandled 回調處理 */ result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); //【判斷】:若是runloop沒有中止 且 沒有結束則繼續循環,相反側退出。 } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); } #【直觀表現】 RunLoop 其實內部就是do-while循環,在這個循環內部不斷地處理各類任務(`好比Source、Timer、Observer`), 經過判斷result的值實現的。因此 能夠當作是一個死循環。 若是沒有RunLoop,UIApplicationMain 函數執行完畢以後將直接返回,就是說程序一啓動而後就結束;
咱們來驗證 Runloop 是在那開啓的?答案:UIApplicationMain 中開啓;多線程
#【驗證 Runloop 的開啓】。 # int 類型返回值 UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName); int main(int argc, char * argv[]) { @autoreleasepool { NSLog(@"開始"); int number = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); NSLog(@"結束"); return number; } } #【驗證結果】:只會打印開始,並不會打印結束。 ---- #【Runloop 的退出條件】。 App退出;線程關閉;設置最大時間到期;
【註解】:說明在UIApplicationMain函數內部開啓了一個和主線程相關的RunLoop (保證主線程不會被銷燬),致使 UIApplicationMain 不會返回,一直在運行中,也就保證了程序的持續運行。app
【附】:CFRunLoop.c 源碼
# NOTE: 得到runloop實現 (建立runloop) CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { if (pthread_equal(t, kNilPthreadT)) {// ✔️【主線程相關聯的RunLoop建立】,若是爲空,默認是主線程 t = pthread_main_thread_np(); } __CFLock(&loopsLock); if (!__CFRunLoops) { // 若是 RunLoop 不存在 __CFUnlock(&loopsLock); // 建立字典 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); // 建立主線程對應的runloop CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // 使用字典保存(KEY:線程 -- Value:線程對應的runloop), 以保證一一對應關係。 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } // ✔️【建立與子線程相關聯的RunLoop】,從字典中獲取 子線程的runloop CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); if (!loop) { // 若是子線程的runloop不存在,那麼就爲該線程建立一個對應的runloop CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 把當前子線程和對應的runloop保存到字典中 if (!loop) { CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop; } // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it __CFUnlock(&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 ?
runloop
,不是經過[alloc init]
方法建立,而是直接經過調用currentRunLoop
方法來建立。currentRunLoop
自己是懶加載的,當第一次調用currentRunLoop
方法得到該子線程對應的 Runloop
的時候,它會先去判斷(去字典中查找)這個線程的Runloop
是否存在,若是不存在就會本身建立而且返回,若是存在直接返回。// Foundation框架 NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop]; // 得到主線程對應的 runloop對象 NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop]; // 得到當前線程對應的runloop對象 // Core Foundation框架 CFRunLoopRef maiRunloop = CFRunLoopGetMain(); // 得到主線程對應的 runloop對象 CFRunLoopRef maiRunloop = CFRunLoopGetCurrent(); // 得到當前線程對應的runloop對象 // NSRunLoop <--> CFRunLoopRef 相互轉化 NSLog(@"NSRunLoop <--> CFRunloop == %p--%p",CFRunLoopGetMain() , [NSRunLoop mainRunLoop].getCFRunLoop); #【打印結果】:內存地址相同 0000-00-13 00:30:16.527 MultiThreading[57703:1217113] NSRunLoop <--> CFRunloop == 0x60000016a680--0x60000016a680
Runloop 相關內部實現源碼,代碼量甚多,其核心方法是 【__CFRunLoopRun】 ,爲了避免影響文章的可讀性,這裏就再也不直接貼源代碼,放一段僞代碼方便你們閱讀【轉】:
int32_t __CFRunLoopRun() { // 通知即將進入runloop __CFRunLoopDoObservers(KCFRunLoopEntry); do { // 通知將要處理timer和source __CFRunLoopDoObservers(kCFRunLoopBeforeTimers); __CFRunLoopDoObservers(kCFRunLoopBeforeSources); // 執行被加入的Block(處理非延遲的主線程調用) __CFRunLoopDoBlocks(); // 處理Source0事件 __CFRunLoopDoSource0(); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(); } // 若是有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 而後跳轉去處理消息。 if (__Source0DidDispatchPortLastTime) { Boolean hasMsg = __CFRunLoopServiceMachPort(); if (hasMsg) goto handle_msg; } // 通知 Observers: RunLoop 的線程即將進入休眠(sleep)。 if (!sourceHandledThisLoop) { __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); } // GCD dispatch main queue CheckIfExistMessagesInMainDispatchQueue(); // 即將進入休眠 __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); // 等待內核mach_msg事件 mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts(); // 等待。。。 // 從等待中醒來 __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); // 處理因timer的喚醒 if (wakeUpPort == timerPort) __CFRunLoopDoTimers(); // 處理異步方法喚醒,如dispatch_async else if (wakeUpPort == mainDispatchQueuePort) __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() // 處理Source1 else __CFRunLoopDoSource1(); // 再次確保是否有同步的方法須要調用 __CFRunLoopDoBlocks(); } while (!stop && !timeout); // 通知即將退出runloop __CFRunLoopDoObservers(CFRunLoopExit); }
線程執行了這個函數 (__CFRunLoopRun) 後,就會一直處於這個函數內部 "接受消息->等待->處理" 的循環中,直到這個循環結束函數才返回,固然Runloop精華在於在休眠時幾乎不會佔用系統資源(系統內核負責)。
面對上面的一段僞代碼不知道作什麼的函數調用 , 這裏你若是想結合上段的僞代碼看源碼的話,能夠 CFRunLoop.c 源碼 (c) 2015 上面的每一步都有相應的註解,打開對照查看能夠很直觀的表現。
【NOTE】: 固然我 也整理了一張圖,描述了 Runloop 內部實現流程(版1 & 版2 基本描述了 Runloop 的核心流程,固然可仍是對照查看官方文檔或源碼)。
runloop內部實現流程.png
【注意】:儘管 CFRunLoopPerformBlock 在上圖中做爲喚醒機制有所體現,但事實上執行 CFRunLoopPerformBlock 只是入隊,下次 RunLoop 運行纔會執行,而若是須要當即執行則必須調用 CFRunLoopWakeUp 。
Core Foundation 中關於 RunLoop 的5個類
CFRunLoop 的5個相關類關係圖解:
相關類關係圖解.gif
【圖解直觀得知】:
運行模式
)下。稱 CurrentMode
)運行, 若是須要切換 Mode,只能是退出 CurrentMode 切換到指定的 Mode 進入,目的以保證不一樣 Mode 下的 Source / Timer / Observer 互不影響。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; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart; }; struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; /* must have the run loop locked before locking this */ CFStringRef _name; // mode名 Boolean _stopped; char _padding[3]; CFMutableSetRef _sources0; // source0 源 CFMutableSetRef _sources1; // source1 源 CFMutableArrayRef _observers; // observer 源 CFMutableArrayRef _timers; // timer 源 CFMutableDictionaryRef _portToV1SourceMap;// mach port 到 mode的映射,爲了在runloop主邏輯中過濾runloop本身的port消息。 __CFPortSet _portSet;// 記錄了全部當前mode中須要監聽的port,做爲調用監聽消息函數的參數。 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;// 使用 mk timer, 用到的mach port,和source1相似,都依賴於mach port Boolean _mkTimerArmed; #endif #if DEPLOYMENT_TARGET_WINDOWS DWORD _msgQMask; void (*_msgPump)(void); #endif uint64_t _timerSoftDeadline; /* TSR timer觸發的理想時間*/ uint64_t _timerHardDeadline; /* TSR timer觸發的實際時間,理想時間加上tolerance(誤差*/ };
CFRunLoopModeRef 表明 RunLoop 的運行模式;系統默認提供了5個 Mode 。
1.【kCFRunLoopDefaultMode (NSDefaultRunLoopMode)】: App的默認Mode,一般主線程是在這個Mode下運行。
2.【UITrackingRunLoopMode】: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。
3.【UIInitializationRunLoopMode】: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。
4.【GSEventReceiveRunLoopMode】: 接受系統事件的內部 Mode,一般用不到。
5.【kCFRunLoopCommonModes (NSRunLoopCommonModes)】: 這個並非某種具體的 Mode, 能夠說是一個佔位用的Mode(一種模式組合)。
CFRunLoop 對外暴露的管理 Mode 接口:
# CFRunLoop CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode); CF_EXPORT CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled); # NSRunLoop.h FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode;// (默認):同一時間只能執行一個任務 FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes NS_AVAILABLE(10_5, 2_0); // (公用):能夠分配必定的時間處理定時器
【注】:對照上面貼的源碼,關於 CommonModes ;
【關於 _commonModes】:一個 mode
能夠標記爲 common
屬性(用於 CFRunLoopAddCommonMode函數
),而後它就會保存在_commonModes
。主線程 CommonModes
默認已有兩個modek CFRunLoopDefaultMode 和 UITrackingRunLoopMode
,固然你也能夠經過調用 CFRunLoopAddCommonMode()
方法將自定義mode
放到 kCFRunLoopCommonModes
組合)。
【關於 _commonModeItems】:_commonModeItems
裏面存放的source, observer, timer
等,在每次 runLoop
運行的時候都會被同步到具備 Common
標記的 Modes
裏。如:[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
就是把timer放到commonModeItems
裏。
更多系統或框架 Mode查看這裏
CFRunloopSourceRef 事件源 \ 輸入源,有兩種分類模式
【官方版】:
【Port-Based Sources】: 基於端口的源 (對應的是source1)
:與內核端口相關,只須要簡單的建立端口對象,並使用 NSPort
的方法將端口對象加入到runloop
,端口對象會處理建立以及配置輸入源;。
【Custom Input Sources】:自定義源:使用CFRunLoopSourceRef
類型相關的函數 (線程) 來建立自定義輸入源。
【Perform Selector Sources】:performSelector:OnThread:delay:
補充:Source1 事件在處理時會分發一些操做給 Source0 去處理。
CFRunLoopTimerRef是基於時間的觸發器。
基本上說的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響。
而與NSTimer相比,GCD定時器不會受Runloop影響。
相對來講CFRunloopObserverRef理解起來並不複雜,它至關於消息循環中的一個監聽器,隨時通知外部當前RunLoop的運行狀態(它包含一個函數指針_callout_將當前狀態及時告訴觀察者
)。具體的Observer狀態以下:
/* jianshu:白開水ln Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), //即將進入Runloop kCFRunLoopBeforeTimers = (1UL << 1), //即將處理NSTimer kCFRunLoopBeforeSources = (1UL << 2), //即將處理Sources kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠 kCFRunLoopAfterWaiting = (1UL << 6), //從休眠裝填中喚醒 kCFRunLoopExit = (1UL << 7), //退出runloop kCFRunLoopAllActivities = 0x0FFFFFFFU //全部狀態改變 };
摘錄:www.cnblogs.com/kenshincui/…
其實對於 Event Loop 而言 RunLoop 最核心的事情就是保證線程在沒有消息時休眠以免佔用系統資源,有消息時可以及時喚醒。 RunLoop 的這個機制徹底依靠系統內核來完成,具體來講是蘋果操做系統核心組件 Darwin 中的 Mach 來完成的。能夠從下圖最底層 Kernel 中找到 Mach:
Mach 是 Darwin 的核心,能夠說是內核的核心,提供了進程間通訊(IPC)、處理器調度等基礎服務。在 Mach 中,進程、線程間的通訊是以消息的方式來完成的,消息在兩個 Port 之間進行傳遞(這也正是 Source1 之因此稱之爲 Port-based Source 的緣由,由於它就是依靠系統發送消息到指定的Port來觸發的)。消息的發送和接收使用<mach/message.h>中的mach_msg()
函數(事實上蘋果提供的Mach API 不多,並不鼓勵咱們直接調用這些API):
/* * Routine: mach_msg * Purpose: * Send and/or receive a message. If the message operation * is interrupted, and the user did not request an indication * of that fact, then restart the appropriate parts of the * operation silently (trap version does not restart). */ __WATCHOS_PROHIBITED __TVOS_PROHIBITED extern mach_msg_return_t mach_msg( mach_msg_header_t *msg, mach_msg_option_t option, mach_msg_size_t send_size, mach_msg_size_t rcv_size, mach_port_name_t rcv_name, mach_msg_timeout_t timeout, mach_port_name_t notify);
而 mach_msg() 的本質是一個調用 mach_msg_trap()
,這至關於一個系統調用,會觸發內核狀態切換。當程序靜止時,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)
,而這個函數內部就是調用了mach_msg
讓程序處於休眠狀態。
Mode-Runloop的運行模式、Source-Runloop要處理的事件源、Timer-定時器事件
[NSTimer timerWithTimeInterval: ].gif
[NSTimer scheduledTimerWithTimeInterval: ].gif
GCD定時器不會受Runloop影響.png
一、NSTimer
二、ImageView顯示:控制方法在特定的模式下可用
三、PerformSelector
四、常駐線程:在子線程中開啓一個runloop
五、AutoreleasePool 自動釋放池
六、UI更新
【註解】:常駐線程:線程建立出來就處於等待狀態(有或無任務),想用它的時候就用它執行任務,不想用的時候就處於等待狀態。
【場景】:如:1.聊天發送語音消息,可能會專門開一個子線程來處理;2.在後臺記錄用戶的停留時間或某個按鈕點擊次數,這些用主線程作可能不太方便,可能會開啓一個子線程後臺默默收集;
【需求】:讓線程持續存在,能夠切換執行其餘任務。
【解決】:開啓 Runloop循環。
Demo & 效果圖:
常駐線程:讓線程持續存在,能夠切換執行其餘任務.gif
AutoreleasePool
是另外一個與 RunLoop
相關討論較多的話題。其實從RunLoop
源代碼分析,AutoreleasePool
與 RunLoop
並無直接的關係,之因此將兩個話題放到一塊兒討論最主要的緣由是由於在iOS應用啓動後會註冊兩個 Observer
管理和維護 AutoreleasePool。不妨在應用程序剛剛啓動時打印 currentRunLoop
能夠看到系統默認註冊了不少個Observer
,其中有兩個Observer
的 callout 都是 _ wrapRunLoopWithAutoreleasePoolHandler
,這兩個是和自動釋放池相關的兩個監聽。
<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}} '' <CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]> {type = mutable-small, count = 0, values = ()}}
第一個 Observer 會監聽 RunLoop 的進入,它會回調objc_autoreleasePoolPush() 向當前的 AutoreleasePoolPage 增長一個哨兵對象標誌建立自動釋放池。這個 Observer 的 order 是 -2147483647 優先級最高,確保發生在全部回調操做以前。
第二個 Observer 會監聽 RunLoop 的進入休眠和即將退出 RunLoop 兩種狀態,在即將進入休眠時會調用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根據狀況從最新加入的對象一直往前清理直到遇到哨兵對象。而在即將退出 RunLoop 時會調用objc_autoreleasePoolPop() 釋放自動自動釋放池內對象。這個Observer 的 order 是 2147483647 ,優先級最低,確保發生在全部回調操做以後。
主線程的其餘操做一般均在這個 AutoreleasePool 以內(main函數中),以儘量減小內存維護操做(固然你若是須要顯式釋放【例如循環】時能夠本身建立 AutoreleasePool 不然通常不須要本身建立)。
AutoreleasePool 自動釋放池.png
若是打印App啓動以後的主線程RunLoop能夠發現另一個callout爲_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
的 Observer,這個監聽專門負責UI變化後的更新,好比修改了frame、調整了UI層級(UIView/CALayer)或者手動設置了setNeedsDisplay/setNeedsLayout 以後就會將這些操做提交到全局容器。而這個Observer監聽了主線程RunLoop的即將進入休眠和退出狀態,一旦進入這兩種狀態則會遍歷全部的UI更新並提交進行實際繪製更新。
一般狀況下這種方式是完美的,由於除了系統的更新,還能夠利用 setNeedsDisplay 等方法手動觸發下一次 RunLoop 運行的更新。可是若是當前正在執行大量的邏輯運算可能UI的更新就會比較卡,所以facebook 推出了 AsyncDisplayKit 來解決這個問題。AsyncDisplayKit 實際上是將UI排版和繪製運算儘量放到後臺,將UI的最終更新操做放到主線程(這一步也必須在主線程完成),同時提供一套類 UIView 或 CALayer 的相關屬性,儘量保證開發者的開發習慣。這個過程當中 AsyncDisplayKit 在主線程 RunLoop 中增長了一個Observer 監聽即將進入休眠和退出 RunLoop 兩種狀態,收到回調時遍歷隊列中的待處理任務一一執行。
Demo & 效果圖
.gif
【描述】:因爲 UItabelView 在滑動的時候,會從當前的 RunLoop 默認的模式 kCFRunLoopDefaultMode (NSDefaultRunLoopMode)
自動切換到 UITrackingRunLoopMode
界面追蹤模式。這個時候,處於 NSDefaultRunLoopMode
裏面的 NSTimer
因爲切換了模式形成計時器沒法繼續運行。
【解決】:
NSRunLoopCommonModes
)[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil]; [thread start]; - (void)newThread{ @autoreleasepool{ //在當前Run Loop中添加timer,模式是默認的NSDefaultRunLoopMode timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(incrementCounter:) userInfo: nil repeats:YES]; //開始執行新線程的Run Loop,若是不啓動run loop,timer的事件是不會響應的 [[NSRunLoop currentRunLoop] run]; } }
【建議看】
說明:此面試題針對性的摘錄整理,只爲方便 在面試路上準備的你 ,會注有原文。
一、整理原文:2017年5月iOS招人心得(附面試題)
unrecognized selector
錯誤?iOS有哪些機制來避免走到這一步?weak
變量的自動置nil?mode
是用來作什麼的?有幾種mode
?NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
添加到主運行循環之後,滑動scrollview
的時候NSTimer
卻不動了?Autorelease Pool
的?//-------------------- 【我是分割線】 ---------------------//
整理原文:2017年iOS面試題總結,附上答案
01 |
---|
問題: objc在向一個對象發送消息時,發生了什麼? |
解答: 根據對象的 isa 指針找到類對象 id,在查詢類對象裏面的 methodLists 方法函數列表,若是沒有在好到,在沿着 superClass ,尋找父類,再在父類 methodLists 方法列表裏面查詢,最終找到 SEL ,根據 id 和 SEL 確認 IMP(指針函數),在發送消息; |
03 |
---|
問題: 何時會報unrecognized selector錯誤?iOS有哪些機制來避免走到這一步? |
解答: 當發送消息的時候,咱們會根據類裏面的 methodLists 列表去查詢咱們要動用的SEL,當查詢不到的時候,咱們會一直沿着父類查詢,當最終查詢不到的時候咱們會報 unrecognized selector 錯誤,當系統查詢不到方法的時候,會調用 +(BOOL)resolveInstanceMethod:(SEL)sel 動態解釋的方法來給我一次機會來添加,調用不到的方法。或者咱們能夠再次使用 -(id)forwardingTargetForSelector:(SEL)aSelector 重定向的方法來告訴系統,該調用什麼方法,一來保證不會崩潰。 |
04 |
---|
問題: 可否向編譯後獲得的類中增長實例變量?可否向運行時建立的類中添加實例變量?爲何? |
解答: 一、不能向編譯後獲得的類增長實例變量 二、能向運行時建立的類中添加實例變量。【解釋】:1. 編譯後的類已經註冊在 runtime 中,類結構體中的 objc_ivar_list 實例變量的鏈表和 instance_size 實例變量的內存大小已經肯定,runtime會調用 class_setvarlayout 或 class_setWeaklvarLayout 來處理strong weak 引用.因此不能向存在的類中添加實例變量。2. 運行時建立的類是能夠添加實例變量,調用class_addIvar函數. 可是的在調用 objc_allocateClassPair 以後,objc_registerClassPair 以前,緣由同上. |
05 |
---|
問題: runtime如何實現weak變量的自動置nil? |
解答: runtime 對註冊的類, 會進行佈局,對於 weak 對象會放入一個 hash 表中。 用 weak 指向的對象內存地址做爲 key,當此對象的引用計數爲0的時候會 dealloc,假如 weak 指向的對象內存地址是a,那麼就會以a爲鍵, 在這個 weak 表中搜索,找到全部以a爲鍵的 weak 對象,從而設置爲 nil。 |
06 |
---|
問題: 給類添加一個屬性後,在類結構體裏哪些元素會發生變化? |
解答: instance_size :實例的內存大小;objc_ivar_list *ivars:屬性列表 |
01 |
---|
問題: runloop是來作什麼的?runloop和線程有什麼關係?主線程默認開啓了runloop麼?子線程呢? |
解答: runloop: 從字面意思看:運行循環、跑圈,其實它內部就是do-while循環,在這個循環內部不斷地處理各類任務(好比Source、Timer、Observer)事件。runloop和線程的關係:一個線程對應一個RunLoop,主線程的RunLoop默認建立並啓動,子線程的RunLoop需手動建立且手動啓動(調用run方法)。RunLoop只能選擇一個Mode啓動,若是當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那麼就直接退出RunLoop。 |
02 |
---|
問題: runloop的mode是用來作什麼的?有幾種mode? |
解答: model:是runloop裏面的運行模式,不一樣的模式下的runloop處理的事件和消息有必定的差異。系統默認註冊了5個Mode:(1)kCFRunLoopDefaultMode: App的默認 Mode,一般主線程是在這個 Mode 下運行的。(2)UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。(3)UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。(4)GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到。(5)kCFRunLoopCommonModes: 這是一個佔位的 Mode,沒有實際做用。注意iOS 對以上5中model進行了封裝 NSDefaultRunLoopMode、NSRunLoopCommonModes |
03 |
---|
問題: 爲何把NSTimer對象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運行循環之後,滑動scrollview的時候NSTimer卻不動了? |
解答: nstime對象是在 NSDefaultRunLoopMode下面調用消息的,可是當咱們滑動scrollview的時候,NSDefaultRunLoopMode模式就自動切換到UITrackingRunLoopMode模式下面,卻不能夠繼續響應nstime發送的消息。因此若是想在滑動scrollview的狀況下面還調用nstime的消息,咱們能夠把nsrunloop的模式更改成NSRunLoopCommonModes. |
04 |
---|
問題: 蘋果是如何實現Autorelease Pool的? |
解答: Autorelease Pool做用:緩存池,能夠避免咱們常常寫relase的一種方式。其實就是延遲release,將建立的對象,添加到最近的autoreleasePool中,等到autoreleasePool做用域結束的時候,會將裏面全部的對象的引用計數器 - autorelease. |
後續遇到針對 runtime & runloop 常面相關,會及時在這裏補充;
分享者 | Runloop 模塊推薦閱讀博文 |
---|---|
xx_cc | 充滿靈性的死循環 www.jianshu.com/p/b9426458f… |
WeiHing | 原理探究及基本使用 www.jianshu.com/p/911549ae4… |
續更 | -- |
參考文章:
Runloop&Runtime.gif
轉載:https://juejin.im/entry/599c13bc6fb9a0248926a77d