iOS開發進階:RunLoop相關分析總結

什麼是Runloop?
Runloop是通過內部維護的事件循環來對事件和消息進行管理的一種機制。當沒有消息需要處理的時候,線程進入休眠以避免佔用資源,有消息需要處理時,立即被喚醒。

runloop循環不是單獨的do-while循環,而是發生一個用戶態到內核態切換,以及內核態到用戶態切換。它維護的事件循環可以用來不斷的處理消息和事件,當沒有消息和事件需要處理時會從用戶態切換到內核態,由此可以用來休眠線程,避免資源佔用。當有消息需要處理時會從內核態切換到用戶態,當前線程會被喚醒,所以狀態切換纔是runloop的關鍵。

iOS中提供了兩套Runloop接口,一個是NSRunLoop基於Objective-C,在Foundation框架中,另一個是CFRunLoopRef基於C,在CoreFoundation中。而NSRunLoop是對CFRunLoopRef的封裝,兩者接口基本都是對應的。CFRunLoopRef runloop = [nsrunloop getCFRunLoop]可以獲取對應的CFRunLoopRef。通過一個表格來對比一下:

特徵 NSRunLoop CFRunLoopRef
所屬框架 Objective-C/Foundation C/CoreFoundation
獲取Runloop [NSRunLoop currentRunLoop]
[NSRunLoop mainRunLoop]
CFRunLoopGetCurrent()
CFRunLoopGetMain()
Source事件 addPort:forMode:
removePort:forMode:
CFRunLoopAddSource(...)
CFRunLoopRemoveSource(...)
Timer事件 addTimer:forMode: CFRunLoopAddTimer(...)
Observer事件 CFRunLoopAddObserver(...)
CFRunLoopRemoveObserver(...)
run run
runUntilDate:
runMode:beforeDate:
CFRunLoopRun()
CFRunLoopRunInMode(...)
CFRunLoopRunSpecific(...)

1. __CFRunLoop相關數據結構

struct __CFRunLoop {
    ...
    pthread_t _pthread;//對應的線程
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
};

__CFRunLooprunloop本身:typedef struct __CFRunLoop *CFRunLoopRef__CFRunLoop對應多個__CFRunLoopMode

struct __CFRunLoopMode {
    ...
    CFStringRef _name;
    CFMutableSetRef _sources0;//
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ...
};

__CFRunLoopMode是runloop的運行模式:typedef struct __CFRunLoopMode *CFRunLoopModeRef。每一個__CFRunLoopMode又包含多個_sources0、_sources1、_observers、_timers事件。_sources0:非基於Port的,也就是用戶主動發出的事件。_sources1:基於Port的,也就是系統內部的消息事件。_observers:觀察者。
_timers:定時器事件。
系統默認註冊了5中類型的Mode

系統註冊的mode 說明
kCFRunLoopDefaultMode App的默認Mode,通常主線程是在這個Mode下運行
UITrackingRunLoopMode 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就不再使用
GSEventReceiveRunLoopMode 接受系統事件的內部 Mode,通常用不到
kCFRunLoopCommonModes 這是一個佔位用的Mode,不是一種真正的Mode

主線程默認運行在kCFRunLoopDefaultMode下,滑動scrollView,就變成了UITrackingRunLoopMode,手指離開又變成了kCFRunLoopDefaultMode

相關的類的成員變量與關係:



如上圖時Runloop中用到的基礎結構,再對應關係方面:一個__CFRunLoop實例可以包含多個__CFRunLoopMode;一個__CFRunLoopMode又包含多個CFRunLoopSourceRef、CFRunLoopObserverRef、CFRunLoopTimerRef事件。一個Runloop要想跑起來,內部必須要有一個Mode,並且這個Mode裏邊必須包含一個Source/Observer/Timer事件。

CFRunLoop的狀態:

名稱 說明
kCFRunLoopEntry 即將進入runloop
kCFRunLoopBeforeTimers 即將處理timer事件
kCFRunLoopBeforeSources 即將處理source事件
kCFRunLoopBeforeWaiting 即將進入睡眠
kCFRunLoopAfterWaiting 被喚醒
kCFRunLoopExit runloop退出

2._CFRunLoop的創建、運行、退出

-創建

[NSRunLoop mainRunLoop]對應底層的CFRunLoopGetMain(),[NSRunLoop currentRunLoop]對應底層的CFRunLoopGetCurrent(),內部都是通過CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)獲取的runloop,分別傳入pthread_main_thread_np()pthread_self()也就是主線程和當前線程的id。

CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
        //如果外部傳入無效的0,則將主線程ID賦值給t。
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        //如果__CFRunLoops爲空,則創建主線程對應的runloop
        __CFUnlock(&loopsLock);
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //__CFRunLoopCreate做線程的初始化
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); 
        // 將mainLoop保存到dict中,以線程id爲key,mainLoop爲value
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        //將dict中的內容複製到__CFRunLoops地址上
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    //通過線程id獲取runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        //如果獲取到的爲空,則直接創建
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //如果創建後還不能獲取到則使用剛纔創建的,並將newLoop保存到__CFRunLoops中
            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;
}

__CFRunLoopCreate中通過_CFRuntimeCreateInstance實例華再進行其他變量的一些初始化。其中loop->_pthread = t;將線程id綁定到了runloop上。runloop通過線程id去查找,如果沒有則進行創建並將線程id綁定到runloop上,通過這個規則我們知道:線程與runloop一一對應;線程不一定都有runloop,首個runloop創建時會檢查__CFRunLoops是否爲空,爲空則先創建主線程的runloop,再創建指定線程的runloop

-運行

啓動Runloop,調用CFRunLoopRun()即可,Runloop進入運行循環,運行狀態只要不是kCFRunLoopRunStoppedkCFRunLoopRunFinished就會一直運行下去不退出。

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

//將runloop的運行狀態切換到指定的Mode

//代碼較長,只列出重要步驟的
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
    ...
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    //如果沒有獲取到mode或者mode中的事件爲空(無sources0/sources1等)返回kCFRunLoopRunFinished
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        return kCFRunLoopRunFinished;
    }
    //保存previousPerRun、previousMode
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    int32_t result = kCFRunLoopRunFinished;

    //通知Observer即將進入循環
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知Observer即將退出循環
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    //恢復previousPerRun,previousMode
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    rl->_currentMode = previousMode;
    return result;
}

3.Runloop的使用

3.1.main函數爲何能保持不退出?

main函數中,會調用UIApplicationMain函數,在內部會啓動主線程的Runloop,可以不斷的接收消息,比如點擊屏幕事件,滑動列表以及處理網絡請求的返回等接收消息後對事件進行處理,處理完之後,就會繼續等待。

3.2.NSTimer相關案例

案例1:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

我們主線程執行如下代碼,我們Timer能夠正常運行,但是如果我們在進行scrollView滑動的時候定時器會停止,這是什麼原因呢?在新開子線程調用它運行不起來,這是什麼原因呢?

這裏我們需要明白:1.scheduledTimerWithTimeInterval:方法會自動把當前初始化的Timer加入到currentRunLoopkCFRunLoopDefaultMode模式下,主線程的runloop已經在run狀態了,所以定時器會立即啓動。如果手動滑動scrollView,則主線程的runloop的狀態切換爲UITrackingRunLoopMode模式了,添加在kCFRunLoopDefaultMode模式的Timer自然就沒有回調了。解決辦法:將Timer手動添加到UITrackingRunLoopMode模式或者kCFRunLoopCommonModes模式即可。

如果在新開的子線程執行上面的代碼,由於新開的子線程並不會主動創建runloop,所以定時器自然運行不起來。解決辦法:手動將Timer加入到currentRunLoopkCFRunLoopCommonModes模式,並且執行run方法。

3.3監聽runloop的運行狀態
-(void)addObserver{
    /*1.創建監聽者
      第一個參數:怎麼分配存儲空間
      第二個參數:要監聽的狀態 。kCFRunLoopAllActivities表示所有的狀態
      第三個參數:是否持續監聽
      第四個參數:優先級 總是傳0
      第五個參數:當狀態改變時候的回調
      */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"即將進入runloop"); break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即將處理timer事件"); break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即將處理source事件");break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即將進入睡眠"); break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"被喚醒"); break;
            case kCFRunLoopExit:
                NSLog(@"runloop退出");break;
            default:
                break;
        }
    });
    /*2.添加監聽者
       第一個參數:要監聽哪個runloop
       第二個參數:觀察者
       第三個參數:運行模式
       */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
3.4常駐線程

一個線程要想跑起來,則需要至少一個mode,一個事件。所以我們可以使用Timer或者Source事件。
第一步創建一個新的線程

- (void)createThread {
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(task1) object:nil];
    [self.thread start];
}

第二步在新開的線程添加Timer或者Source事件。

- (void)task1{
  [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
  [[NSRunLoop currentRunLoop] run];
}

第三部測試線程是否正常運行。可以通過點擊等連續觸發,也可以在主線程多次調用test來測試

- (void)test {
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)task2{
    NSLog(@"task2---%@",[NSThread currentThread]);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章