RunLoop終極解析:輸入源,定時源,觀察者,線程間通訊,端口通訊,NSPort,NSMessagePort,NSMachPort,NSPortMessage

本文Demo傳送門 RunloopDemohtml

前言

OSX / iOS 系統中,提供了兩個這樣的對象:NSRunLoopCFRunLoopRefios

  • CFRunLoopRef 是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,全部這些 API 都是線程安全的。git

  • NSRunLoop 是基於 CFRunLoopRef 的封裝,提供了面向對象的 API,可是這些 API 不是線程安全的。github

1. 如何查看RunLoop源代碼


1.1 NSRunLoop源代碼

NSRunLoop是Foundation框架裏面的一個類,它的頭文件能夠在工程裏面這樣查看:macos

至於它的實現文件,暫時沒有找到公開的資料。數組

1.2 CFRunLoopRef源代碼

CFRunLoopRef 的代碼是開源的,你能夠在這裏 opensource.apple.com/tarballs/CF… 下載到整個 CoreFoundation 的源碼。爲了方便跟蹤和查看,你能夠新建一個 Xcode 工程,把這堆源碼拖進去看。緩存

更多蘋果源代碼下載

蘋果公開的源代碼在這裏能夠下載,opensource.apple.com/tarballs/安全

例如,其中,有兩個比較常見須要學習源碼的下載地址:bash

固然,若是你想在github上在線查看源代碼,能夠點這裏:runtimerunloop數據結構

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

源碼得知:

  1. kCFRunLoopDefaultMode,默認狀況下,runLoop是在這個mode下運行的,
  2. 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_tNSThread。過去蘋果有份文檔標明瞭 NSThread 只是 pthread_t 的封裝,但那份文檔已經失效了,如今它們也有可能都是直接包裝自最底層的 mach thread。蘋果並無提供這兩個對象相互轉換的接口,但無論怎麼樣,能夠確定的是 pthread_tNSThread 是一一對應的。好比,你能夠經過 pthread_main_np()[NSThread mainThread] 來獲取 主線程;也能夠經過 pthread_self()[NSThread currentThread] 來獲取 當前線程CFRunLoop 是基於 pthread 來管理的。

蘋果不容許直接建立 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain()CFRunLoopGetCurrent()。從上面的代碼(第3.2節)能夠看出,線程和 RunLoop 之間是一一對應的,其關係是保存在一個全局的 Dictionary 裏。線程剛建立時並無 RunLoop,若是你不主動獲取,那它一直都不會有。RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)。

Demo中如何查看線程棧

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:kCFRunLoopDefaultModeUITrackingRunLoopMode。這兩個 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。可是,運行的時候,一條線程對應一個 RunloopRunloop 老是運行在某種特定的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
        //做用一、防止runloop執行完畢以後推出
        //做用二、接收主線程發送過來的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];
}
複製代碼
相關文章
相關標籤/搜索