iOS刨根問底-深入理解RunLoop(轉(zhuǎn))

iOS刨根問底-深入理解RunLoop

2017-05-08 10:35 by KenshinCui

概述

RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系喧锦,同時也是很多常見技術(shù)的幕后功臣斋枢。盡管在平時多數(shù)開發(fā)者很少直接使用RunLoop,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程模型划鸽,同時也可以幫助開發(fā)者解答日常開發(fā)中的一些疑惑。本文將從RunLoop源碼著手增显,結(jié)合RunLoop的實(shí)際應(yīng)用來逐步解開它的神秘面紗疗涉。

開源的RunloopRef

通常所說的RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是純C的函數(shù)缸废,而NSRunloop僅僅是CFRunloopRef的OC封裝包蓝,并未提供額外的其他功能,因此下面主要分析CFRunloopRef企量,蘋果已經(jīng)開源了CoreFoundation源代碼测萎,因此很容易找到CFRunloop源代碼。從代碼可以看出CFRunloopRef其實(shí)就是__CFRunloop這個結(jié)構(gòu)體指針(按照OC的思路我們可以將RunLoop看成一個對象)梁钾,這個對象的運(yùn)行才是我們通常意義上說的運(yùn)行循環(huán)绳泉,核心方法是__CFRunloopRun(),為了便于閱讀就不再直接貼源代碼姆泻,放一段偽代碼方便大家閱讀:

int32_t __CFRunLoopRun()

{

?? // 通知即將進(jìn)入runloop

?? __CFRunLoopDoObservers(KCFRunLoopEntry);


?? do

?? {

? ? ?? // 通知將要處理timer和source

? ? ?? __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);

? ? ?? __CFRunLoopDoObservers(kCFRunLoopBeforeSources);


? ? ?? // 處理非延遲的主線程調(diào)用

? ? ?? __CFRunLoopDoBlocks();

? ? ?? // 處理Source0事件

? ? ?? __CFRunLoopDoSource0();


? ? ?? if (sourceHandledThisLoop) {

? ? ? ? ?? __CFRunLoopDoBlocks();

? ? ? ? }

? ? ?? /// 如果有 Source1 (基于port) 處于 ready 狀態(tài)零酪,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息冒嫡。

? ? ?? if (__Source0DidDispatchPortLastTime) {

? ? ? ? ?? Boolean hasMsg = __CFRunLoopServiceMachPort();

? ? ? ? ?? if (hasMsg) goto handle_msg;

? ? ?? }


? ? ?? /// 通知 Observers: RunLoop 的線程即將進(jìn)入休眠(sleep)。

? ? ?? if (!sourceHandledThisLoop) {

? ? ? ? ?? __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);

? ? ?? }


? ? ?? // GCD dispatch main queue

? ? ?? CheckIfExistMessagesInMainDispatchQueue();


? ? ?? // 即將進(jìn)入休眠

? ? ?? __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);


? ? ?? // 等待內(nèi)核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();


? ? ?? // 再次確保是否有同步的方法需要調(diào)用

? ? ?? __CFRunLoopDoBlocks();


?? } while (!stop && !timeout);


?? // 通知即將退出runloop

?? __CFRunLoopDoObservers(CFRunLoopExit);

}

源代碼盡管不算太長月腋,但是如果不太熟悉的話面對這么一堆不知道做什么的函數(shù)調(diào)用還是會給人一種神秘感蟀架。但是現(xiàn)在可以不用逐行閱讀,后面慢慢解開這層神秘面紗∮苌В現(xiàn)在只要了解上面的偽代碼知道核心的方法__CFRunLoopRun內(nèi)部其實(shí)是一個do while循環(huán)片拍,這也正是Runloop運(yùn)行的本質(zhì)。執(zhí)行了這個函數(shù)以后就一直處于“等待-處理”的循環(huán)之中妓肢,直到循環(huán)結(jié)束捌省。只是不同于我們自己寫的循環(huán)它在休眠時幾乎不會占用系統(tǒng)資源,當(dāng)然這是由于系統(tǒng)內(nèi)核負(fù)責(zé)實(shí)現(xiàn)的碉钠,也是Runloop精華所在纲缓。

隨著Swift的開源蘋果也維護(hù)了一個Swift版本的跨平臺CoreFoundation版本,除了mac平臺它還是適配了Linux和Windows平臺喊废。但是鑒于目前很多關(guān)于Runloop的討論都是以O(shè)C版展開的祝高,所以這里也主要分析OC版本。

下圖描述了Runloop運(yùn)行流程(基本描述了上面Runloop的核心流程污筷,當(dāng)然可以查看官方The Run Loop Sequence of Events描述):

整個流程并不復(fù)雜(需要注意的就是黃色區(qū)域的消息處理中并不包含source0工闺,因?yàn)樗谘h(huán)開始之初就會處理),整個流程其實(shí)就是一種Event Loop的實(shí)現(xiàn)瓣蛀,其他平臺均有類似的實(shí)現(xiàn)斤寂,只是這里叫做Runloop。但是既然RunLoop是一個消息循環(huán)揪惦,誰來管理和運(yùn)行Runloop遍搞?那么它接收什么類型的消息?休眠過程是怎么樣的器腋?如何保證休眠時不占用系統(tǒng)資源溪猿?如何處理這些消息以及何時退出循環(huán)?還有一系列問題需要解開纫塌。

注意的是盡管CFRunLoopPerformBlock在上圖中作為喚醒機(jī)制有所體現(xiàn)诊县,但事實(shí)上執(zhí)行CFRunLoopPerformBlock只是入隊(duì),下次RunLoop運(yùn)行才會執(zhí)行措左,而如果需要立即執(zhí)行則必須調(diào)用CFRunLoopWakeUp依痊。

Runloop Mode

從源碼很容易看出,Runloop總是運(yùn)行在某種特定的CFRunLoopModeRef下(每次運(yùn)行__CFRunLoopRun()函數(shù)時必須指定Mode)。而通過CFRunloopRef對應(yīng)結(jié)構(gòu)體的定義可以很容易知道每種Runloop都可以包含若干個Mode胸嘁,每個Mode又包含Source/Timer/Observer瓶摆。每次調(diào)用Runloop的主函數(shù)__CFRunLoopRun()時必須指定一種Mode,這個Mode稱為** _currentMode**性宏,當(dāng)切換Mode時必須退出當(dāng)前Mode群井,然后重新進(jìn)入Runloop以保證不同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;

? ? ?? 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 */

?? };

系統(tǒng)默認(rèn)提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)UITrackingRunLoopMode毫胜,需要切換到對應(yīng)的Mode時只需要傳入對應(yīng)的名稱即可书斜。前者是系統(tǒng)默認(rèn)的RunloopMode,例如進(jìn)入iOS程序默認(rèn)不做任何操作就處于這種Mode中酵使,此時滑動UIScrollView荐吉,主線程就切換Runloop到到UITrackingRunLoopMode,不再接受其他事件操作(除非你將其他Source/Timer設(shè)置到UITrackingRunLoopMode下)口渔。但是對于開發(fā)者而言經(jīng)常用到的Mode還有一個kCFRunLoopCommonModes(NSRunLoopCommonModes),其實(shí)這個并不是某種具體的Mode稍坯,而是一種模式組合,在iOS系統(tǒng)中默認(rèn)包含了 NSDefaultRunLoopModeUITrackingRunLoopMode(注意:并不是說Runloop會運(yùn)行在kCFRunLoopCommonModes這種模式下搓劫,而是相當(dāng)于分別注冊了 NSDefaultRunLoopModeUITrackingRunLoopMode。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合)混巧。

注意:我們常常還會碰到一些系統(tǒng)框架自定義Mode枪向,例如Foundation中NSConnectionReplyMode。還有一些系統(tǒng)私有Mode咧党,例如:GSEventReceiveRunLoopMode接受系統(tǒng)事件秘蛔,UIInitializationRunLoopMode App啟動過程中初始化Mode。更多系統(tǒng)或框架Mode查看這里

CFRunLoopRef和CFRunloopMode傍衡、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef關(guān)系如下圖:

那么CFRunLoopSourceRef深员、CFRunLoopTimerRef和CFRunLoopObserverRef究竟是什么?它們在Runloop運(yùn)行流程中起到什么作用呢蛙埂?

Source

首先看一下官方Runloop結(jié)構(gòu)圖(注意下圖的Input Source Port和前面流程圖中的Source0并不對應(yīng)倦畅,而是對應(yīng)Source1。Source1和Timer都屬于端口事件源绣的,不同的是所有的Timer都共用一個端口“Mode Timer Port”叠赐,而每個Source1都有不同的對應(yīng)端口):

再結(jié)合前面RunLoop核心運(yùn)行流程可以看出Source0(負(fù)責(zé)App內(nèi)部事件,由App負(fù)責(zé)管理觸發(fā)屡江,例如UITouch事件)和Timer(又叫Timer Source芭概,基于時間的觸發(fā)器,上層對應(yīng)NSTimer)是兩個不同的Runloop事件源(當(dāng)然Source0是Input Source中的一類惩嘉,Input Source還包括Custom Input Source罢洲,由其他線程手動發(fā)出),RunLoop被這些事件喚醒之后就會處理并調(diào)用事件處理方法(CFRunLoopTimerRef的回調(diào)指針和CFRunLoopSourceRef均包含對應(yīng)的回調(diào)指針)文黎。但是對于CFRunLoopSourceRef除了Source0之外還有另一個版本就是Source1惹苗,Source1除了包含回調(diào)指針外包含一個mach port殿较,和Source0需要手動觸發(fā)不同,Source1可以監(jiān)聽系統(tǒng)端口和其他線程相互發(fā)送消息鸽粉,它能夠主動喚醒RunLoop(由操作系統(tǒng)內(nèi)核進(jìn)行管理斜脂,例如CFMessagePort消息)。官方也指出可以自定義Source触机,因此對于CFRunLoopSourceRef來說它更像一種協(xié)議帚戳,框架已經(jīng)默認(rèn)定義了兩種實(shí)現(xiàn),如果有必要開發(fā)人員也可以自定義儡首,詳細(xì)情況可以查看官方文檔片任。

Observer

?? 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 */

?? };

相對來說CFRunloopObserverRef理解起來并不復(fù)雜,它相當(dāng)于消息循環(huán)中的一個監(jiān)聽器蔬胯,隨時通知外部當(dāng)前RunLoop的運(yùn)行狀態(tài)(它包含一個函數(shù)指針callout將當(dāng)前狀態(tài)及時告訴觀察者)对供。具體的Observer狀態(tài)如下:

?? /* Run Loop Observer Activities */

?? typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

? ? ?? kCFRunLoopEntry = (1UL << 0), // 進(jìn)入RunLoop

? ? ?? kCFRunLoopBeforeTimers = (1UL << 1), // 即將開始Timer處理

? ? ?? kCFRunLoopBeforeSources = (1UL << 2), // 即將開始Source處理

? ? ?? kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠

? ? ?? kCFRunLoopAfterWaiting = (1UL << 6), //從休眠狀態(tài)喚醒

? ? ?? kCFRunLoopExit = (1UL << 7), //退出RunLoop

? ? ?? kCFRunLoopAllActivities = 0x0FFFFFFFU

?? };

Call out

在開發(fā)過程中幾乎所有的操作都是通過Call out進(jìn)行回調(diào)的(無論是Observer的狀態(tài)通知還是Timer、Source的處理)氛濒,而系統(tǒng)在回調(diào)時通常使用如下幾個函數(shù)進(jìn)行回調(diào)(換句話說你的代碼其實(shí)最終都是通過下面幾個函數(shù)來負(fù)責(zé)調(diào)用的产场,即使你自己監(jiān)聽Observer也會先調(diào)用下面的函數(shù)然后間接通知你,所以在調(diào)用堆棧中經(jīng)澄韪停看到這些函數(shù)):

?? static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();

?? static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();

?? static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();

?? static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();

?? static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();

?? static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

例如在控制器的touchBegin中打入斷點(diǎn)查看堆棧(由于UIEvent是Source0京景,所以可以看到一個Source0的Call out函數(shù)CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION調(diào)用):

RunLoop休眠

其實(shí)對于Event Loop而言RunLoop最核心的事情就是保證線程在沒有消息時休眠以避免占用系統(tǒng)資源,有消息時能夠及時喚醒骗奖。RunLoop的這個機(jī)制完全依靠系統(tǒng)內(nèi)核來完成确徙,具體來說是蘋果操作系統(tǒng)核心組件Darwin中的Mach來完成的(Darwin是開源的)≈醋溃可以從下圖最底層Kernel中找到Mach:

Mach是Darwin的核心鄙皇,可以說是內(nèi)核的核心,提供了進(jìn)程間通信(IPC)仰挣、處理器調(diào)度等基礎(chǔ)服務(wù)伴逸。在Mach中,進(jìn)程膘壶、線程間的通信是以消息的方式來完成的违柏,消息在兩個Port之間進(jìn)行傳遞(這也正是Source1之所以稱之為Port-based Source的原因,因?yàn)樗褪且揽肯到y(tǒng)發(fā)送消息到指定的Port來觸發(fā)的)香椎。消息的發(fā)送和接收使用中的mach_msg()函數(shù)(事實(shí)上蘋果提供的Mach API很少漱竖,并不鼓勵我們直接調(diào)用這些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()的本質(zhì)是一個調(diào)用mach_msg_trap(),這相當(dāng)于一個系統(tǒng)調(diào)用,會觸發(fā)內(nèi)核狀態(tài)切換畜伐。當(dāng)程序靜止時馍惹,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而這個函數(shù)內(nèi)部就是調(diào)用了mach_msg讓程序處于休眠狀態(tài)。

Runloop和線程的關(guān)系

Runloop是基于pthread進(jìn)行管理的,pthread是基于c的跨平臺多線程操作底層API万矾。它是mach thread的上層封裝(可以參見Kernel Programming Guide)悼吱,和NSThread一一對應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)良狈。

蘋果開發(fā)的接口中并沒有直接創(chuàng)建Runloop的接口后添,如果需要使用Runloop通常CFRunLoopGetMain()CFRunLoopGetCurrent()兩個方法來獲取(通過上面的源代碼也可以看到薪丁,核心邏輯在CFRunLoopGet當(dāng)中),通過代碼并不難發(fā)現(xiàn)其實(shí)只有當(dāng)我們使用線程的方法主動get Runloop時才會在第一次創(chuàng)建該線程的Runloop遇西,同時將它保存在全局的Dictionary中(線程和Runloop二者一一對應(yīng)),默認(rèn)情況下線程并不會創(chuàng)建Runloop(主線程的Runloop比較特殊严嗜,任何線程創(chuàng)建之前都會保證主線程已經(jīng)存在Runloop)粱檀,同時在線程結(jié)束的時候也會銷毀對應(yīng)的Runloop。

iOS開發(fā)過程中對于開發(fā)者而言更多的使用的是NSRunloop,它默認(rèn)提供了三個常用的run方法:

? ? - (void)run;

? ? - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

? ? - (void)runUntilDate:(NSDate *)limitDate;

run方法對應(yīng)上面CFRunloopRef中的CFRunLoopRun并不會退出漫玄,除非調(diào)用CFRunLoopStop();通常如果想要永遠(yuǎn)不會退出RunLoop才會使用此方法茄蚯,否則可以使用runUntilDate。

runMode:beforeDate:則對應(yīng)CFRunLoopRunInMode(mode,limiteDate,true)方法,只執(zhí)行一次睦优,執(zhí)行完就退出渗常;通常用于手動控制RunLoop(例如在while循環(huán)中)。

runUntilDate:方法其實(shí)是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)汗盘,執(zhí)行完并不會退出皱碘,繼續(xù)下一次RunLoop直到timeout。

RunLoop應(yīng)用

NSTimer

前面一直提到Timer Source作為事件源衡未,事實(shí)上它的上層對應(yīng)就是NSTimer(其實(shí)就是CFRunloopTimerRef)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer實(shí)現(xiàn)),甚至很多開發(fā)者接觸RunLoop還是從NSTimer開始的家凯。其實(shí)NSTimer定時器的觸發(fā)正是基于RunLoop運(yùn)行的缓醋,所以使用NSTimer之前必須注冊到RunLoop,但是RunLoop為了節(jié)省資源并不會在非常準(zhǔn)確的時間點(diǎn)調(diào)用定時器绊诲,如果一個任務(wù)執(zhí)行時間較長送粱,那么當(dāng)錯過一個時間點(diǎn)后只能等到下一個時間點(diǎn)執(zhí)行,并不會延后執(zhí)行(NSTimer提供了一個tolerance屬性用于設(shè)置寬容度掂之,如果確實(shí)想要使用NSTimer并且希望盡可能的準(zhǔn)確抗俄,則可以設(shè)置此屬性)。

NSTimer的創(chuàng)建通常有兩種方式世舰,盡管都是類方法动雹,一種是timerWithXXX,另一種scheduedTimerWithXXX跟压。

?? + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

?? + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

?? + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;

?? + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

?? + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;

?? + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

二者最大的區(qū)別就是后者除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopModeMode添加到當(dāng)前線程RunLoop中胰蝠,不添加到RunLoop中的NSTimer是無法正常工作的。例如下面的代碼中如果timer2不加入到RunLoop中是無法正常工作的。同時注意如果滾動UIScrollView(UITableView茸塞、UICollectionview是類似的)二者是無法正常工作的躲庄,但是如果將NSDefaultRunLoopMode改為NSRunLoopCommonModes則可以正常工作,這也解釋了前面介紹的Mode內(nèi)容钾虐。

?? #import "ViewController1.h"

?

?? @interface ViewController1 ()

?? @property (nonatomic,weak) NSTimer *timer1;

?? @property (nonatomic,weak) NSTimer *timer2;

?? @end


?? @implementation ViewController1


?? - (void)viewDidLoad {

? ? ?? [super viewDidLoad];

? ? ?? self.view.backgroundColor = [UIColor blueColor];

? ? ?? // timer1創(chuàng)建后會自動以NSDefaultRunLoopMode默認(rèn)模式添加到當(dāng)前RunLoop中噪窘,所以可以正常工作

? ? ?? self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];

? ? ?? NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];

? ? ?? // 如果不把timer2添加到RunLoop中是無法正常工作的(注意如果想要在滾動UIScrollView時timer2可以正常工作可以將NSDefaultRunLoopMode改為NSRunLoopCommonModes)

? ? ?? [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];

? ? ?? self.timer2 = tempTimer;


? ? ?? CGRect rect = [UIScreen mainScreen].bounds;

? ? ?? UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];

? ? ?? [self.view addSubview:scrollView];


? ? ?? UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];

? ? ?? contentView.backgroundColor = [UIColor redColor];

? ? ?? [scrollView addSubview:contentView];

? ? ?? scrollView.contentSize = contentView.frame.size;

?? }


?? - (void)timeInterval:(NSTimer *)timer {

? ? ?? if (self.timer1 == timer) {

? ? ? ? ?? NSLog(@"timer1...");

? ? ?? } else {

? ? ? ? ?? NSLog(@"timer2...");

? ? ?? }

?? }


?? - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

? ? ?? [self dismissViewControllerAnimated:true completion:nil];

?? }


?? - (void)dealloc {

? ? ?? NSLog(@"ViewController1 dealloc...");

?? }

?? @end

注意上面代碼中UIViewController1對timer1和timer2并沒有強(qiáng)引用,對于普通的對象而言效扫,執(zhí)行完viewDidLoad方法之后(準(zhǔn)確的說應(yīng)該是執(zhí)行完viewDidLoad方法后的的一個RunLoop運(yùn)行結(jié)束)二者應(yīng)該會被釋放倔监,但事實(shí)上二者并沒有被釋放。原因是:為了確保定時器正常運(yùn)轉(zhuǎn)荡短,當(dāng)加入到RunLoop以后系統(tǒng)會對NSTimer執(zhí)行一次retain操作(特別注意:timer2創(chuàng)建時并沒直接賦值給timer2丐枉,原因是timer2是weak屬性,如果直接賦值給timer2會被立即釋放掘托,因?yàn)閠imerWithXXX方法創(chuàng)建的NSTimer默認(rèn)并沒有加入RunLoop瘦锹,只有后面加入RunLoop以后才可以將引用指向timer2)。但是即使使用了弱引用闪盔,上面的代碼中ViewController1也無法正常釋放弯院,原因是在創(chuàng)建NSTimer2時指定了target為self,這樣一來造成了timer1和timer2對ViewController1有一個強(qiáng)引用泪掀。解決這個問題的方法通常有兩種:一種是將target分離出來獨(dú)立成一個對象(在這個對象中創(chuàng)建NSTimer并將對象本身作為NSTimer的target)听绳,控制器通過這個對象間接使用NSTimer;另一種方式的思路仍然是轉(zhuǎn)移target异赫,只是可以直接增加NSTimer擴(kuò)展(分類)椅挣,讓NSTimer自身做為target,同時可以將操作selector封裝到block中塔拳。后者相對優(yōu)雅鼠证,也是目前使用較多的方案(目前有大量類似的封裝,例如:NSTimer+Block)靠抑。顯然Apple也認(rèn)識到了這個問題量九,如果你可以確保代碼只在iOS 10下運(yùn)行就可以使用iOS 10新增的系統(tǒng)級block方案(上面的代碼中已經(jīng)貼出這種方法)。當(dāng)然使用上面第二種方法可以解決控制器無法釋放的問題颂碧,但是會發(fā)現(xiàn)即使控制器被釋放了兩個定時器仍然正常運(yùn)行荠列,要解決這個問題就需要調(diào)用NSTimer的invalidate方法(注意:無論是重復(fù)執(zhí)行的定時器還是一次性的定時器只要調(diào)用invalidate方法則會變得無效,只是一次性的定時器執(zhí)行完操作后會自動調(diào)用invalidate方法)载城。修改后的代碼如下:

?? #import "ViewController1.h"


?? @interface ViewController1 ()

?? @property (nonatomic,weak) NSTimer *timer1;

?? @property (nonatomic,weak) NSTimer *timer2;

?? @end


?? @implementation ViewController1


?? - (void)viewDidLoad {

? ? ?? [super viewDidLoad];

? ? ?? self.view.backgroundColor = [UIColor blueColor];


? ? ?? self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

? ? ? ? ?? NSLog(@"timer1...");

? ? ?? }];

? ? ?? NSTimer *tempTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

? ? ? ? ?? NSLog(@"timer2...");

? ? ?? }];

? ? ?? [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];

? ? ?? self.timer2 = tempTimer;


? ? ?? CGRect rect = [UIScreen mainScreen].bounds;

? ? ?? UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];

? ? ?? [self.view addSubview:scrollView];


? ? ?? UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];

? ? ?? contentView.backgroundColor = [UIColor redColor];

? ? ?? [scrollView addSubview:contentView];

? ? ?? scrollView.contentSize = contentView.frame.size;

?? }


?? - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

? ? ?? [self dismissViewControllerAnimated:true completion:nil];

?? }


?? - (void)dealloc {

? ? ?? [self.timer1 invalidate];

? ? ?? [self.timer2 invalidate];

? ? ?? NSLog(@"ViewController1 dealloc...");

?? }

?? @end

其實(shí)和定時器相關(guān)的另一個問題大家也經(jīng)常碰到肌似,那就是NSTimer不是一種實(shí)時機(jī)制,官方文檔明確說明在一個循環(huán)中如果RunLoop沒有被識別(這個時間大概在50-100ms)或者說當(dāng)前RunLoop在執(zhí)行一個長的callout(例如執(zhí)行某個循環(huán)操作)則NSTimer可能就會存在誤差诉瓦,RunLoop在下一次循環(huán)中繼續(xù)檢查并根據(jù)情況確定是否執(zhí)行(NSTimer的執(zhí)行時間總是固定在一定的時間間隔锈嫩,例如1:00:00受楼、1:00:01、1:00:02呼寸、1:00:05則跳過了第4艳汽、5次運(yùn)行循環(huán))。要演示這個問題請看下面的例子(注意:有些示例中可能會讓一個線程中啟動一個定時器对雪,再在主線程啟動一個耗時任務(wù)來演示這個問河狐,如果實(shí)際測試可能效果不會太明顯,因?yàn)楝F(xiàn)在的iPhone都是多核運(yùn)算的瑟捣,這樣一來這個問題會變得相對復(fù)雜馋艺,因此下面的例子選擇在同一個RunLoop中即加入定時器和執(zhí)行耗時任務(wù))

?? #import "ViewController.h"


?? @interface ViewController ()

?? @property (nonatomic,weak) NSTimer *timer1;

?? @property (nonatomic,strong) NSThread *thread1;

?? @end


?? @implementation ViewController


?? - (void)viewDidLoad {

? ? ?? [super viewDidLoad];

? ? ?? self.view.backgroundColor = [UIColor redColor];


? ? ?? // 由于下面的方法無法拿到NSThread的引用,也就無法控制線程的狀態(tài)

? ? ?? //[NSThread detachNewThreadSelector:@selector(performTask) toTarget:self withObject:nil];

? ? ?? self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(performTask) object:nil];

? ? ?? [self.thread1 start];

?? }


?? - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

? ? ?? [self.thread1 cancel];

? ? ?? [self dismissViewControllerAnimated:YES completion:nil];

?? }


?? - (void)dealloc {

? ? ?? [self.timer1 invalidate];

? ? ?? NSLog(@"ViewController dealloc.");

?? }


?? - (void)performTask {

? ? ?? // 使用下面的方式創(chuàng)建定時器雖然會自動加入到當(dāng)前線程的RunLoop中迈套,但是除了主線程外其他線程的RunLoop默認(rèn)是不會運(yùn)行的捐祠,必須手動調(diào)用

? ? ?? __weak typeof(self) weakSelf = self;

? ? ?? self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

? ? ? ? ?? if ([NSThread currentThread].isCancelled) {

? ? ? ? ? ? ?? //[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(caculate) object:nil];

? ? ? ? ? ? ?? //[NSThread exit];

? ? ? ? ? ? ?? [weakSelf.timer1 invalidate];

? ? ? ? ?? }

? ? ? ? ?? NSLog(@"timer1...");

? ? ?? }];


? ? ?? NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);


? ? ?? // 區(qū)分直接調(diào)用和「performSelector:withObject:afterDelay:」區(qū)別,下面的直接調(diào)用無論是否運(yùn)行RunLoop一樣可以執(zhí)行,但是后者則不行桑李。

? ? ?? //[self caculate];

? ? ?? [self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];


? ? ?? // 取消當(dāng)前RunLoop中注冊測selector(注意:只是當(dāng)前RunLoop踱蛀,所以也只能在當(dāng)前RunLoop中取消)

? ? ?? // [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];

? ? ?? NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);


? ? ?? // 非主線程RunLoop必須手動調(diào)用

? ? ?? [[NSRunLoop currentRunLoop] run];


? ? ?? NSLog(@"注意:如果RunLoop不退出(運(yùn)行中),這里的代碼并不會執(zhí)行贵白,RunLoop本身就是一個循環(huán).");



?? }


?? - (void)caculate {

? ? ?? for (int i = 0;i < 9999;++i) {

? ? ? ? ?? NSLog(@"%i,%@",i,[NSThread currentThread]);

? ? ? ? ?? if ([NSThread currentThread].isCancelled) {

? ? ? ? ? ? ?? return;

? ? ? ? ?? }

? ? ?? }

?? }


?? @end

如果運(yùn)行并且不退出上面的程序會發(fā)現(xiàn)率拒,前兩秒NSTimer可以正常執(zhí)行,但是兩秒后由于同一個RunLoop中循環(huán)操作的執(zhí)行造成定時器跳過了中間執(zhí)行的機(jī)會一直到caculator循環(huán)完畢禁荒,這也正說明了NSTimer不是實(shí)時系統(tǒng)機(jī)制的原因猬膨。

但是以上程序還有幾點(diǎn)需要說明一下:

NSTimer會對Target進(jìn)行強(qiáng)引用直到任務(wù)結(jié)束或exit之后才會釋放。如果上面的程序沒有進(jìn)行線程cancel而終止任務(wù)則及時關(guān)閉控制器也無法正確釋放呛伴。

非主線程的RunLoop并不會自動運(yùn)行(同時注意默認(rèn)情況下非主線程的RunLoop并不會自動創(chuàng)建勃痴,直到第一次使用),RunLoop運(yùn)行必須要在加入NSTimer或Source0热康、Sourc1沛申、Observer輸入后運(yùn)行否則會直接退出。例如上面代碼如果run放到NSTimer創(chuàng)建之前則既不會執(zhí)行定時任務(wù)也不會執(zhí)行循環(huán)運(yùn)算褐隆。

performSelector:withObject:afterDelay:執(zhí)行的本質(zhì)還是通過創(chuàng)建一個NSTimer然后加入到當(dāng)前線程RunLoop(通而過前后兩次打印RunLoop信息可以看到此方法執(zhí)行之后RunLoop的timer會增加1個污它。類似的還有performSelector:onThread:withObject:afterDelay:剖踊,只是它會在另一個線程的RunLoop中創(chuàng)建一個Timer)庶弃,所以此方法事實(shí)上在任務(wù)執(zhí)行完之前會對觸發(fā)對象形成引用,任務(wù)執(zhí)行完進(jìn)行釋放(例如上面會對ViewController形成引用德澈,注意:performSelector: withObject:等方法則等同于直接調(diào)用歇攻,原理與此不同)。

同時上面的代碼也充分說明了RunLoop是一個循環(huán)事實(shí)梆造,run方法之后的代碼不會立即執(zhí)行缴守,直到RunLoop退出葬毫。

上面程序的運(yùn)行過程中如果突然dismiss,則程序的實(shí)際執(zhí)行過程要分為兩種情況考慮:如果循環(huán)任務(wù)caculate還沒有開始則會在timer1中停止timer1運(yùn)行(停止了線程中第一個任務(wù))屡穗,然后等待caculate執(zhí)行并break(停止線程中第二個任務(wù))后線程任務(wù)執(zhí)行結(jié)束釋放對控制器的引用贴捡;如果循環(huán)任務(wù)caculate執(zhí)行過程中dismiss則caculate任務(wù)執(zhí)行結(jié)束,等待timer1下個周期運(yùn)行(因?yàn)楫?dāng)前線程的RunLoop并沒有退出村砂,timer1引用計(jì)數(shù)器并不為0)時檢測到線程取消狀態(tài)則執(zhí)行invalidate方法(第二個任務(wù)也結(jié)束了)烂斋,此時線程釋放對于控制器的引用。

CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器础废,它也需要加入到RunLoop才能執(zhí)行汛骂。與NSTimer類似,CADisplayLink同樣是基于CFRunloopTimerRef實(shí)現(xiàn)评腺,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)帘瞭。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度),不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象蒿讥。通常情況下CADisaplayLink用于構(gòu)建幀動畫蝶念,看起來相對更加流暢,而NSTimer則有更廣泛的用處诈悍。

AutoreleasePool

AutoreleasePool是另一個與RunLoop相關(guān)討論較多的話題祸轮。其實(shí)從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系侥钳,之所以將兩個話題放到一起討論最主要的原因是因?yàn)樵趇OS應(yīng)用啟動后會注冊兩個Observer管理和維護(hù)AutoreleasePool适袜。不妨在應(yīng)用程序剛剛啟動時打印currentRunLoop可以看到系統(tǒng)默認(rèn)注冊了很多個Observer,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**舷夺,這兩個是和自動釋放池相關(guān)的兩個監(jiān)聽苦酱。

?? {valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = {type = mutable-small, count = 0, values = ()}}

?? '' {valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = {type = mutable-small, count = 0, values = ()}}

第一個Observer會監(jiān)聽RunLoop的進(jìn)入,它會回調(diào)objc_autoreleasePoolPush()向當(dāng)前的AutoreleasePoolPage增加一個哨兵對象標(biāo)志創(chuàng)建自動釋放池给猾。這個Observer的order是-2147483647優(yōu)先級最高疫萤,確保發(fā)生在所有回調(diào)操作之前。第二個Observer會監(jiān)聽RunLoop的進(jìn)入休眠和即將退出RunLoop兩種狀態(tài)敢伸,在即將進(jìn)入休眠時會調(diào)用objc_autoreleasePoolPop()objc_autoreleasePoolPush() 根據(jù)情況從最新加入的對象一直往前清理直到遇到哨兵對象扯饶。而在即將退出RunLoop時會調(diào)用objc_autoreleasePoolPop() 釋放自動自動釋放池內(nèi)對象。這個Observer的order是2147483647池颈,優(yōu)先級最低尾序,確保發(fā)生在所有回調(diào)操作之后。主線程的其他操作通常均在這個AutoreleasePool之內(nèi)(main函數(shù)中)躯砰,以盡可能減少內(nèi)存維護(hù)操作(當(dāng)然你如果需要顯式釋放【例如循環(huán)】時可以自己創(chuàng)建AutoreleasePool否則一般不需要自己創(chuàng)建)每币。其實(shí)在應(yīng)用程序啟動后系統(tǒng)還注冊了其他Observer(例如即將進(jìn)入休眠時執(zhí)行注冊回調(diào)UIGestureRecognizerUpdateObserver用于手勢處理、回調(diào)為ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面實(shí)時繪制更新)和多個Source1(例如context為CFMachPort的Source1用于接收硬件事件響應(yīng)進(jìn)而分發(fā)到應(yīng)用程序一直到UIEvent)琢歇,這里不再一一詳述兰怠。

UI更新

如果打印App啟動之后的主線程RunLoop可以發(fā)現(xiàn)另外一個callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer梦鉴,這個監(jiān)聽專門負(fù)責(zé)UI變化后的更新,比如修改了frame揭保、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器肥橙。而這個Observer監(jiān)聽了主線程RunLoop的即將進(jìn)入休眠和退出狀態(tài),一旦進(jìn)入這兩種狀態(tài)則會遍歷所有的UI更新并提交進(jìn)行實(shí)際繪制更新秸侣。通常情況下這種方式是完美的快骗,因?yàn)槌讼到y(tǒng)的更新,還可以利用setNeedsDisplay等方法手動觸發(fā)下一次RunLoop運(yùn)行的更新塔次。但是如果當(dāng)前正在執(zhí)行大量的邏輯運(yùn)算可能UI的更新就會比較卡方篮,因此facebook推出了AsyncDisplayKit來解決這個問題。AsyncDisplayKit其實(shí)是將UI排版和繪制運(yùn)算盡可能放到后臺励负,將UI的最終更新操作放到主線程(這一步也必須在主線程完成)藕溅,同時提供一套類UIView或CALayer的相關(guān)屬性,盡可能保證開發(fā)者的開發(fā)習(xí)慣继榆。這個過程中AsyncDisplayKit在主線程RunLoop中增加了一個Observer監(jiān)聽即將進(jìn)入休眠和退出RunLoop兩種狀態(tài),收到回調(diào)時遍歷隊(duì)列中的待處理任務(wù)一一執(zhí)行巾表。

NSURLConnection

在前面的網(wǎng)絡(luò)開發(fā)的文章中已經(jīng)介紹過NSURLConnection的使用,一旦啟動NSURLConnection以后就會不斷調(diào)用delegate方法接收數(shù)據(jù)略吨,這樣一個連續(xù)的的動作正是基于RunLoop來運(yùn)行集币。一旦NSURLConnection設(shè)置了delegate會立即創(chuàng)建一個線程com.apple.NSURLConnectionLoader,同時內(nèi)部啟動RunLoop并在NSDefaultMode模式下添加4個Source0翠忠。其中CFHTTPCookieStorage用于處理cookie;CFMultiplexerSource負(fù)責(zé)各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來執(zhí)行實(shí)際操作鞠苟。早期版本的AFNetworking庫也是基于NSURLConnection實(shí)現(xiàn),為了能夠在后臺接收delegate回調(diào)AFNetworking內(nèi)部創(chuàng)建了一個空的線程并啟動了RunLoop秽之,當(dāng)需要使用這個后臺線程執(zhí)行任務(wù)時AFNetworking通過performSelector: onThread: 將這個任務(wù)放到后臺線程的RunLoop中当娱。

GCD和RunLoop的關(guān)系

在RunLoop的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容,但是RunLoop本身和GCD并沒有直接的關(guān)系考榨。當(dāng)調(diào)用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發(fā)送消息喚醒RunLoop跨细,RunLoop從消息中獲取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回調(diào)里執(zhí)行這個block河质。不過這個操作僅限于主線程冀惭,其他線程dispatch操作是全部由libDispatch驅(qū)動的。

更多RunLoop使用

前面看了很多RunLoop的系統(tǒng)應(yīng)用和一些知名第三方庫使用掀鹅,那么除了這些究竟在實(shí)際開發(fā)過程中我們自己能不能適當(dāng)?shù)氖褂肦unLoop幫我們做一些事情呢散休?

思考這個問題其實(shí)只要看RunLoopRef的包含關(guān)系就知道了,RunLoop包含多個Mode淫半,而它的Mode又是可以自定義的溃槐,這么推斷下來其實(shí)無論是Source1匣砖、Timer還是Observer開發(fā)者都可以利用科吭,但是通常情況下不會自定義Timer昏滴,更不會自定義一個完整的Mode,利用更多的其實(shí)是Observer和Mode的切換对人。例如很多人都熟悉的使用perfromSelector在默認(rèn)模式下設(shè)置圖片谣殊,防止UITableView滾動卡頓([[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。還有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空閑狀態(tài)下計(jì)算出UITableViewCell的高度并進(jìn)行緩存牺弄。再有老譚的PerformanceMonitor關(guān)于iOS實(shí)時卡頓監(jiān)控姻几,同樣是利用Observer對RunLoop進(jìn)行監(jiān)視。

關(guān)于如何自定義一個Custom Input Source官網(wǎng)給出了詳細(xì)的流程势告。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蛇捌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子咱台,更是在濱河造成了極大的恐慌络拌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件回溺,死亡現(xiàn)場離奇詭異春贸,居然都是意外死亡忽冻,警方通過查閱死者的電腦和手機(jī)叠穆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟀拷,“玉大人车要,你說我怎么就攤上這事允粤。” “怎么了翼岁?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵维哈,是天一觀的道長。 經(jīng)常有香客問我登澜,道長阔挠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任脑蠕,我火速辦了婚禮购撼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谴仙。我一直安慰自己迂求,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布晃跺。 她就那樣靜靜地躺著揩局,像睡著了一般。 火紅的嫁衣襯著肌膚如雪掀虎。 梳的紋絲不亂的頭發(fā)上凌盯,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天付枫,我揣著相機(jī)與錄音,去河邊找鬼驰怎。 笑死阐滩,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的县忌。 我是一名探鬼主播掂榔,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼症杏!你這毒婦竟也來了装获?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤厉颤,失蹤者是張志新(化名)和其女友劉穎饱溢,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體走芋,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绩郎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了翁逞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肋杖。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖挖函,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情必怜,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布欺栗,位于F島的核電站消请,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一吗伤、第九天 我趴在偏房一處隱蔽的房頂上張望巧号。 院中可真熱鬧卜高,春花似錦疼进、人聲如沸减拭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽可霎。三九已至魄鸦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間癣朗,已是汗流浹背拾因。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旷余,地道東北人绢记。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像正卧,于是被迫代替她去往敵國和親庭惜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容