將Runloop的理解都寫到這裏

項目代碼

runloopDemophp

CFCoreFoundation源碼git

目錄

  • 從主線程runloop啥時候開啓
  • runloop對象是怎麼存儲的
  • runloop怎麼跑起來的,又是怎麼退出的
  • Runloop do-while作了什麼
  • 監聽Runloop的狀態
  • 常駐線程以及怎麼銷燬常駐線程
  • runloopperformSelector
  • 網絡請求主線程回調,實現同步
  • runloop優化tableview滾動坑點
  • runloop卡頓監測
  • runloopautoreleasepool
  • 界面更新

從主線程runloop啥時候開啓

appmain函數中的UIApplicationMain走進去,就一直在裏面循環了,NSLog(@"會走嗎");是不會被調用的github

這裏我就有一個疑惑:那爲啥這個main還要return int類型呢?既然都死循環,return不了緩存

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    int i = UIApplicationMain(argc, argv, nil, appDelegateClassName);
    
    NSLog(@"會走嗎");
    return i;
}

複製代碼

進入UIApplicationMain後,就會接着調用application:didFinishLaunchingWithOptions:方法,在這個方法裏就開啓runloop,經過監聽runloop狀態,在***即將進入runloop***回調打上斷點,看堆棧便可得知性能優化


runloop對象是怎麼存儲的

runloop跑起來,得先獲取runloop對象,咱們從CFRunloop.c的源碼中,找到CFRunLoopGetCurrentbash

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());
}
複製代碼

從這兩個方法,獲取Runloop的入參是線程對象,能夠斷定,線程與runloop一一對應的關係,具體,咱們再看下_CFRunLoopGet0的實現markdown

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    // 若是參數爲空,那麼就默認是主線程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    
    __CFLock(&loopsLock);
    
    // static CFMutableDictionaryRef __CFRunLoops = NULL;
    // 存放Runloop對象的字典
    // 先判斷這個Runloop字典存在不,不存在就建立一個,並加主線程Runloop加進入
    if (!__CFRunLoops) {
        __CFUnlock(&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);
        
        __CFLock(&loopsLock);
    }
    
    // 根據線程去這個字段取Runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    
    __CFUnlock(&loopsLock);
    
    // 若是不存在,就建立一個Runloop,並加到字典中
    if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&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 __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; } 複製代碼

大概的代碼邏輯就是網絡

實現思路
1.先判斷這個全局字典存不存在,不存在,建立一個,並將主線程的runloop加進去
2.直接去字典裏取這個loop
3.若是loop不存在,就建立一個loop加入到全局字典中
// 僞代碼
if(!__CFRunLoops) {
      1.建立全局字典
      2.建立主線程loop,並加入到全局字典中
}
根據線程pthread_t爲key,去字典取對應的loop
if(!loop) {
      1.建立loop
      2.加入到字典中
}
return loop
複製代碼

因此:app

  • runloop對象和線程是一一對應的關係
  • runloop對象是儲存在一個全局字典中的,這個全局字段的key是線程對象,valuerunloop對象

runloop怎麼跑起來的,又是怎麼退出的

先說下runloop跑一圈是作了什麼事情框架

首先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
};

複製代碼

因此,當啓動runloop的時候,就是監聽輸入源(端口port、source0、source1)、定時器、若是有事件,處理事件,沒有就休眠

可是實際上並非這樣的,而是一直在重複的進入runloop(使用run方法啓動runloop的狀況)

咱們先從開啓runloop的函數入手,從CFRunLoopRun函數,咱們看到了runloop確實是一個do-while操做,那麼裏面的CFRunLoopRunSpecific每走一次,就算runloop迭代一次,那麼這個runloop迭代一次後,會退出runloop,退出runloop後,由於CFRunLoopRun函數有do-while操做,因此又會從新進入runloop

void CFRunLoopRun(void) {	/* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

複製代碼

CFRunLoopRunSpecific中作了一些前置判斷,好比判斷當前Mode爲空,直接return,這個也能夠說明一點***啓動runloop以前,runloop中必定要有輸入源或者定時器***

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    ...
    
    //  前置判斷,好比判斷當前`Mode`爲空,直接`return`
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    
   ...
   
    // 回調即將進入runloop
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 進入runloop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 即將退出runloop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    ...
    
    return result;
}
複製代碼

接下來再看下__CFRunLoopRun函數

// 簡化代碼,詳細直接搜源碼
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
	do {
		// 監聽source、timer
		if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
       // 處理source0
       Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
       
       // 即將進入休眠
       if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);

		...
		
		 // 退出runloop的條件
		 if (sourceHandledThisLoop && stopAfterHandle) {
		 	  // 處理完sourcesourceHandledThisLoop會爲YES
		 	  // stopAfterHandle,若是是CFRunloop調用的話,是爲NO
		 	  // 能夠回頭看下CFRunLoopRun函數
		 	  // 
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            // 自身超時時間到了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            // 被外部調用CFRunloop中止
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            // 被 _CFRunLoopStopMode 中止
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // 檢查上一個 mode 有沒有執行完全部事件源
            retVal = kCFRunLoopRunFinished;
        }
       
	} while(0 = retVal);
}

複製代碼

退出runloop有四個條件

  • 入參stopAfterHandle爲YES的時候,那麼處理完source就會退出runloop
  • 自身超時時間到了
  • 被外部調用CFRunloop中止
  • _CFRunLoopStopMode 中止

CFRunLoopRun指定stopAfterHandleNO,說明使用run方法開啓runloop,處理完source後不會退出runloop

若是是使用CFRunLoopRunInMode則能夠指定是否須要處理完source後就退出runloop


Runloop do-while作了什麼

do-while的過程當中,作了如下操做

  • 監聽source(source1是基於port的線程通訊(觸摸/鎖屏/搖晃等),source0是不基於port的,包括:UIEvent、performSelector),監聽到就處理
  • 監聽timer的事件,監聽到就處理
  • 沒有source和timer的時候,就休眠,休眠不是不監聽,仍是保持監聽的,只是當有事件的時候,才喚醒,繼續處理

當咱們觸發了事件(觸摸/鎖屏/搖晃等)後,由IOKit.framework生成一個 IOHIDEvent事件,而IOKit是蘋果的硬件驅動框架,由它進行底層接口的抽象封裝與系統進行交互傳遞硬件感應的事件,並專門處理用戶交互設備,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的,它會將事件封裝成IOHIDEvents對象,接着用mach port轉發給須要的App進程,隨後 Source1就會接收IOHIDEvent,以後再回調__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()內觸發Source0,Source0 再觸發 _UIApplicationHandleEventQueue()。因此觸摸事件看到是在 Source0 內的。

總結:觸摸事件先經過 mach port 發送,封裝爲 source1,以後又轉換爲 source0

1.一個runloop對應一個線程,多個mode,一個mode下對應多個sourceobservertimer

struct __CFRunLoop {
    pthread_t _pthread; // 線程對象
    CFMutableSetRef _commonModes; // 
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
    // 簡化
};

struct __CFRunLoopMode {
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ...
    // 簡化
};

複製代碼

2.常見有五種mode

  • kCFRunLoopDefaultMode: App的默認 Mode,一般主線程是在這個 Mode 下運行的。
  • UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。
  • UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用。
  • GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到。
  • kCFRunLoopCommonModes: 這是一個佔位的 Mode,沒有實際做用。

除了以上5個mode,還有其餘mode,可是不多碰見這裏

4.子線程不自動開啓runloop,手動開啓runloop前,必須得有輸入源和定時器(輸入源就是經過監聽端口,能夠獲取不一樣的事件),經過CFRunloop源碼中的CFRunLoopRunSpecific函數,其中判斷了當modenull或者modeItem爲空,直接return

監聽Runloop的狀態

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
};
複製代碼

能夠經過這就代碼監聽這六個狀態

CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
複製代碼

其中的參數分別爲

CFRunLoopObserverCreate參數

1.不懂

2.監聽runloop變化狀態

3.是否重複監聽

4.不懂,傳0

5.回調的函數指針(須要本身寫一個函數)

6.CFRunLoopObserverContext對象

定義函數指針

static void runLoopOserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
   //void *info正是咱們要用來與OC傳值的,這邊能夠轉成OC對象,前面咱們傳進來的時候是self
    RunloopObserver *target = (__bridge RunloopObserver *)(info);//void *info便是咱們前面傳遞的self(ViewController)
    
    if (target.callback) {
        target.callback(observer, activity);
    }
}
複製代碼

定義CFRunLoopObserverContext對象,其實這個參數是用於通訊的

// 從CFRunLoopObserverRef點進去找
    
    typedef struct {
        CFIndex    version; // 傳0,不知道是什麼
        void *    info; // 數據傳遞用的,void *,指就是能夠接受全部指針
        const void *(*retain)(const void *info); // 引用
        void    (*release)(const void *info); // 回收
        CFStringRef    (*copyDescription)(const void *info); // 描述,沒用到
    } CFRunLoopObserverContext;
複製代碼

建立監聽

//建立一個監聽
static CFRunLoopObserverRef observer;
    
// CFRunLoopObserverCreate參數。1.不懂  2.監聽runloop變化狀態  3.是否重複監聽  4.不懂,傳0 5.函數指針  6.CFRunLoopObserverContext對象
observer = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, &runLoopOserverCallBack, &context);
    
//註冊監聽
CFRunLoopAddObserver(runLoopRef, observer, kCFRunLoopCommonModes);
    
//銷燬
CFRelease(observer);
    
複製代碼

常駐線程以及怎麼銷燬常駐線程

先說下performSelector和子線程的,perform...AfterDelayperform..onThread都須要在開啓runloop的線程執行

由於其實現原理,都是往runloop添加一個不重複的定時器

- (void)test1
{
    [self.myThread setName:@"StopRunloopThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    
    // performSelector:afterDelay:的原理是往runloop添加不重複執行的定時器
    [self performSelector:@selector(performSelAferDelayClick) withObject:nil afterDelay:1];
    
    [self.myRunloop run];
    
    NSLog(@"我會走嗎");
}
複製代碼

若是將開啓runloop的代碼,寫到perform前,那麼會開啓不成功,由於開啓runloop須要有輸入源或者定時器的狀況才能夠開啓

獲取runloop會調用CFRunLoopRunSpecific函數(具體搜下CFRunloop.c

從這個函數中找到如下代碼,當currentMode爲空的時候,(也就沒有輸入源或者定時器),直接return kCFRunLoopRunFinished,開啓失敗

if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
	Boolean did = false;
	if (currentMode) __CFRunLoopModeUnlock(currentMode);
	__CFRunLoopUnlock(rl);
	return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
複製代碼

如下代碼實現了一個常駐線程

原理就是往當前線程的runloop中添加一個端口,讓其監聽這個端口(理解爲監聽某個端口的輸入源,好比系統內核端口,監聽一些系統事件),由於能夠一直監聽這個端口,那麼runloop就不會退出

其實就是保持runloop不退出,就達到常駐線程的效果了,那麼要讓runloop不退出,就得有輸入源或者重複的定時器讓其監聽

- (void)test2
{
    [self.myThread setName:@"StopRunloopThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    [self.myRunloop run];
    
    // 由於run以後,這個線程就一直在作do-while操做
    // 至關上面的代碼被do-while包圍了,那麼如下代碼不會走
    NSLog(@"我會走嗎");
}
複製代碼

當開啓一個線程,就會對應建立一個runloop對象嗎?

不是的,調用獲取當前runloop的方法,內部實現:若是當前runloop不存在就建立一個,存在就返回當前runloop

因此走這句代碼self.myRunloop = [NSRunLoop currentRunLoop];就生成當前線程對應的runloop


怎麼銷燬常駐線程?

1.要銷燬常駐線程,首先得先把runloop退出?

當沒有輸入源或者定時器能夠監聽的時候,退出runloop

若是咱們調用[NSThread exit];,這時候線程是銷燬了,可是線程中的代碼仍是不會執行,好比NSLog(@"我會走嗎");,

說明這時候的runloop並無退出,那麼這樣會致使一些問題,例如如下代碼

- (void)test2
{
    [self.myThread setName:@"TestThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    // 添加監聽NSMachPort的端口(這個端口能夠理解爲輸入源,由於能夠一直監聽這個,因此這時候的runloop不會退出,會一直在作do-while)
    [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    [self.myRunloop run];
    
    // [self.myRunloop run]; 會致使如下代碼無法走,由於runloop就是一個do-while的循環,do-while監聽源,處理源
    [self.testPtr release];
}
複製代碼

由於runloop沒有退出,[self.testPtr release];不會執行,那麼就會致使testPtr對象無法釋放

2.怎麼退出runloop呢

若是常駐線程是經過監聽端口實現的,那麼就調用[self.myRunloop removePort:self.myPort forMode:NSDefaultRunLoopMode];,移除端口,就能夠銷燬了

其實這時候還不必定能成功銷燬,由於可能系統加入了一些其餘源的監聽

若是是經過添加劇復定時器,實現常駐線程(這種方式不可取,由於比添加監聽端口耗性能,須要一次又一次的喚醒runloop)

- (void)test11
{
    [self.myThread setName:@"TestThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    // 首先,須要有一點,當runloop監視輸入源或者定時器的時候,纔不會退出
    // 開啓runloop以前,須要有輸入源或者定時器
    // 定時器(若是是添加定時器,不重複,那麼監聽一次就退出了)
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer執行");
    }];
    
    [self.myRunloop addTimer:timer forMode:NSDefaultRunLoopMode];
    
    
    
    [self.myRunloop run];
    
    // [self.myRunloop run]; 會致使如下代碼無法走,由於runloop就是一個do-while的循環,do-while監聽源,處理源
    NSLog(@"我會走嗎");
    
}
複製代碼

若是NSTimerrepeatsNO,那麼執行一次timer的事件後,就會退出runloop

以上,若是經過移除端口,結束timer,反正以移除已知的輸入源或者定時器來退出runloop都是不太靠譜的,由於系統內部有可能會在當前線程的runloop中添加一些輸入源,也就是還有未知的輸入源,咱們沒有移除

3.使用CFRunLoopStop退出Runloop

- (void)test3
{
    [self.myThread setName:@"TestThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    // 添加監聽NSMachPort的端口(這個端口能夠理解爲輸入源,由於能夠一直監聽這個,因此這時候的runloop不會退出,會一直在作do-while)
    [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
    [self performSelector:@selector(runloopStop) withObject:nil afterDelay:1];

    [self.myRunloop run];
    
    // [self.myRunloop run]; 會致使如下代碼無法走,由於runloop就是一個do-while的循環,do-while監聽源,處理源
    NSLog(@"我會走嗎");
}

- (void)runloopStop
{
    NSLog(@"執行stop");
    CFRunLoopStop(self.myRunloop.getCFRunLoop);
}
複製代碼

輸出:

2020-05-03 20:10:12.614130+0800 Runloop[60465:2827474] 即將進入Loop,
2020-05-03 20:10:12.614465+0800 Runloop[60465:2827474] 即將處理 Timer,
2020-05-03 20:10:12.615214+0800 Runloop[60465:2827474] 即將處理 Source,
2020-05-03 20:10:12.615634+0800 Runloop[60465:2827474] 即將進入休眠,
2020-05-03 20:10:13.615638+0800 Runloop[60465:2827474] 剛從休眠中喚醒,
2020-05-03 20:10:13.616005+0800 Runloop[60465:2827474] 執行stop
2020-05-03 20:10:13.616194+0800 Runloop[60465:2827474] 即將退出Loop,
2020-05-03 20:10:13.616360+0800 Runloop[60465:2827474] 即將進入Loop,
2020-05-03 20:10:13.616511+0800 Runloop[60465:2827474] 即將處理 Timer,
2020-05-03 20:10:13.616648+0800 Runloop[60465:2827474] 即將處理 Source,
2020-05-03 20:10:13.616765+0800 Runloop[60465:2827474] 即將進入休眠,

複製代碼

確實是退出了runloop,可是又立刻進入了

緣由是:

開啓線程有三種方式

// 不會退出runloop
- (void)run; 

// 超時時候到退出runloop
- (void)runUntilDate:(NSDate *)limitDate; 

// 處理完source會退出或者時間到也會退出
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

// 上三個方法分別對應CFRunloop

void CFRunLoopRun(void)

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) //  returnAfterSourceHandled爲NO

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) //  returnAfterSourceHandled爲YES



複製代碼

runrunUntilDate:都會重複的調runMode:beforeDate:

具體的解釋看NSRunLoop的退出方式

因此剛纔stop以後,確實是退出runloop了,可是由於咱們是用run啓動的,因此會重複的調用runMode:beforeDate:又啓動了

3.用runMode:beforeDate:啓動runloop,再用CFRunLoopStop退出runloop試試

將上一段代碼[self.myRunloop run];替換成[self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

成功退出runloop而且線程run後的代碼也走了,這時候經過打個暫停斷點,看堆棧,發現咱們的線程不在了,說明已經被銷燬了(runloop退出後,線程沒有任務,天然就銷燬了)

2020-05-03 20:21:30.330067+0800 Runloop[60593:2834891] 即將進入Loop,
2020-05-03 20:21:30.330303+0800 Runloop[60593:2834891] 即將處理 Timer,
2020-05-03 20:21:30.330639+0800 Runloop[60593:2834891] 即將處理 Source,
2020-05-03 20:21:30.330906+0800 Runloop[60593:2834891] 即將進入休眠,
2020-05-03 20:21:31.330956+0800 Runloop[60593:2834891] 剛從休眠中喚醒,
2020-05-03 20:21:31.331329+0800 Runloop[60593:2834891] 執行stop
2020-05-03 20:21:31.331591+0800 Runloop[60593:2834891] 即將退出Loop,
2020-05-03 20:21:31.331783+0800 Runloop[60593:2834891] 我會走嗎

複製代碼

雖然使用self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]能夠成功退出runloop,可是仍是有問題,當runloop處理完source後,就退出runloop了,並且這時候,也不會想調用run方法那樣,從新進入runloop

因此這種方式仍是不行

最後一個最佳方式,既能手動退出runloop,有不會處理完source就退出runloop,再也不進來

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning) {
	// runMode是有返回值的,當啓動runloop後,是不會返回的,因此不會一直在調這個方法,runloop退出了,纔會再調
	[theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
}
複製代碼

當想退出runloop的時候,將shouldKeepRunning置爲NO就能夠了


runloopperformSelector

performSelector:withObject:afterDelay:原理,往runloop添加一個不重複的定時器

因此子線程調用這個方法,是須要開啓runloop纔有效的

順便看看performSelector:onThread:withObject:waitUntilDone:

// myThread是常駐線程
self.myThread = [[PermanentThread alloc] initWithTarget:self selector:@selector(myThreadStart) object:nil];

[self.myThread start];
    
NSLog(@"1");
[self performSelector:@selector(performWait) onThread:self.myThread withObject:nil waitUntilDone:NO];
NSLog(@"2");

- (void)performWait
{
    NSLog(@"3");
}

複製代碼

若是waitUntilDone爲NO,那麼就是不等待sel執行完,才往下走

輸出爲一、二、3

若是爲YES,那麼就是會卡住當前線程,等待sel執行完才走

輸出爲一、三、2


網絡請求主線程回調,實現同步

需求描述:

給你一個接口,這個接口是網絡請求,回調是主線程回來的,如今要求調用這個接口後,須要等待回調回來後,後面的代碼才能夠接着往下走

- (void)netRequestComplete:(void(^)(void))complete
{
    // 模擬網絡請求,主線程回調
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            if (complete) {
                complete();
            }
        });
    });
}

複製代碼

使用信號量,會致使死鎖

dispatch_semaphore_t sema = dispatch_semaphore_create(0);
[self netRequestComplete:^{
    NSLog(@"3");
    // 由於主線程被卡住,這裏不會走了,因此死鎖
    dispatch_semaphore_signal(sema);
}];
    
// 卡住主線程
dispatch_semaphore_wait(sema, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10.0 * NSEC_PER_SEC)));
NSLog(@"2");   
複製代碼

正確方式使用CFRunloopRun

[self netRequestComplete:^{
    NSLog(@"3");
    // stop,退出runloop,主線程runloop退出後,又會自動加入,就像前面講的,開啓runloop是使用run的方法
    CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);
}];
    
// CFRunLoopRun()至關加了do-while,這時候下面的代碼執行不了
CFRunLoopRun();
NSLog(@"2");
    
複製代碼

runloop優化tableview滾動坑點

這個點是從這個文章得知的UITableView性能優化-中級篇

作了一個實驗

首先,用perform確實能夠滑動tableview滾動的時候,不加載圖片,達到優化的效果

可是經過這個實驗發現,當我中止滾動的時候,前面滑過的indexPath,都會觸發logIndexRow:方法

若是這時候是加載圖片,那麼是多餘的了,由於cell都劃出界面了,沒有必要加載

由這個現象,能夠大概的判斷,performSelector:inModes,若是是在defaultmode下調用,雖然如今是在滾動,不會觸發方法,可是perform就往runloop的defaultMode添加輸入源,但滾動結束的時候,切換回defaultMode,這些輸入源都會被觸發

// 這個selector,能夠是loadImg的方法
[self performSelector:@selector(logIndexRow:)
               withObject:indexPath
               afterDelay:0
                  inModes:@[NSDefaultRunLoopMode]];
複製代碼
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"123"];
    
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"123"];
    }
    
    // 滑動的時候,不會調用logIndexRow:,由於這時候mode是滑動,可是perform也是屬於輸入源,這些事件會被積累在NSDefaultRunLoopMode下,當切換到NSDefaultRunLoopMode下後,就會執行這些輸入源事件
    [self performSelector:@selector(logIndexRow:)
               withObject:indexPath
               afterDelay:0
                  inModes:@[NSDefaultRunLoopMode]];
    
    cell.textLabel.text = @"123";
    cell.textLabel.textColor = [UIColor redColor];
    
    return cell;
}
複製代碼

runloop卡頓監測

引用源碼__CFRunLoopRun分析中的說法

從 kCFRunLoopBeforeSources 爲起點到 kCFRunLoopBeforeWaiting 休眠前,這其中處理了大量的工做————執行 block,處理 source0,更新界面…作完這些以後 RunLoop 就休眠了,直到 RunLoop 被 timer、source、libdispatch 喚醒,喚醒後會發送休眠結束的 kCFRunLoopAfterWaiting 通知。咱們知道屏幕的刷新率是 60fps,即 1/60s ≈ 16ms,假如一次 RunLoop 超過了這個時間,UI 線程有可能出現了卡頓,BeforeSources 到 AfterWaiting 能夠粗略認爲是一次 RunLoop 的起止。至於其餘狀態,譬如 BeforeWaiting,它在更新完界面以後有可能休眠了,此時 APP 已經是不活躍的狀態,不太可能形成卡頓;而 kCFRunLoopExit,它在 RunLoop 退出以後觸發,主線程的 RunLoop 除了換 mode 又不太可能主動退出,這也不能用做卡頓檢測。

監聽***即將處理source***,到***結束睡眠***,若是這個過程超過一幀的時間,就可能出現丟幀的狀況(丟幀就會致使卡頓)

那麼爲何這個過程若是一幀的時間,就可能卡頓呢?

首先咱們要理解屏幕顯示原理,大概就是CPU計算文本、佈局、繪製、圖片解碼,以後就提交位圖到GPU,GPU就進行渲染,渲染完成後,根據V-sync信號,更新緩衝區,同時,視頻控制器的指針,也會根據V-sync信號去緩衝區讀取一幀的緩存,顯示到屏幕上

也就是說從cpu繪製->GPU渲染,要在16ms內完成,才能保證在指定時間內,給視頻控制器讀取,不然,視頻控制器就會讀到上一幀的畫面,這就致使卡頓了

因此在即將處理source,到結束睡眠這段時間內,若是CPU一直在處理一件任務,若是超過了16ms,那麼可能就來不及在16ms內完成一幀畫面的渲染

runloopautoreleasepool

App啓動後,蘋果在主線程 RunLoop 裏註冊了兩個 Observer,其回調都是 _wrapRunLoopWithAutoreleasePoolHandler()。 第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush() 建立自動釋放池。其 order 是-2147483647,優先級最高,保證建立釋放池發生在其餘全部回調以前。 第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池並建立新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其餘全部回調以後。

設置_wrapRunLoopWithAutoreleasePoolHandler符號斷點,能夠從彙編代碼,看到autoreleasepush、pop

界面更新

不知道怎麼證實。。。。


參考文章

NSRunLoop的退出方式

戴銘寫的Runloop

NSURLConnection的執行過程

不少runloop的問題和例子

深刻理解RunLoop(YYModel做者)

源碼__CFRunLoopRun分析

相關文章
相關標籤/搜索