iOS底層原理 RunLoop基礎總結和隨心所欲掌握子執行緒RunLoop生命週期 --(9)

fgyong發表於2019-07-24

上篇文章講了runtime的簡單應用,使用鉤子實現了對字典和陣列的賦值的校驗,順便隨手擼了一個簡單的jsonToModel,iOS除了runtime還有一個東西的叫做runloop,各位看官老爺一定都有了解,那麼今天這篇文章初識一下runloop

什麼是runloop

簡單來講runloop就是一個迴圈,我們寫的程式,一般沒有迴圈的話,執行完就結束了,那麼我們手機上的APP是如何一直執行不停止的呢?APP就是用到了runloop,保證程式一直執行不退出,在需要處理事件的時候處理事件,不處理事件的時候進行休眠,跳出迴圈程式就結束。用虛擬碼實現一個runloop其實是這樣子的

int ret = 0;
do {
    //睡眠中等待訊息
    int messgae = sleep_and_wait();
    //處理訊息
    ret = process_message(messgae);
} while (ret == 0);
複製程式碼

獲取runloop

iOS中有兩套可以獲取runloop程式碼,一個是Foundation、一個是Core FoundationFoundation其實是對Core Foundation的一個封裝,

NSRunLoop * runloop1 = [NSRunLoop currentRunLoop];
NSRunLoop *mainloop1 = [NSRunLoop mainRunLoop];

CFRunLoopRef runloop2= CFRunLoopGetCurrent();
CFRunLoopRef mainloop2 = CFRunLoopGetMain();
NSLog(@"%p %p %p %p",runloop1,mainloop1,runloop2,mainloop2);
NSLog(@"%@",runloop1);
//列印
runlopp1:0x600001bc58c0 
mainloop1:0x600001bc58c0 
runloop2:0x6000003cc300 
mainloop1:0x6000003cc300

runloop1:<CFRunLoop 0x6000003cc300 [0x10b2e9ae8]>.....
複製程式碼

runloop1mainloop1地址一致,說明當前的runloopmainrunloop,runloop1作為物件輸出的結果其實也是runloop2的地址,證明Foundation runloop是對Core Foundation的一個封裝。

RunLoop底層我們猜測應該是結構體,我們都瞭解到其實OC就是封裝了c/c++,那麼c厲害之處就是指標和結構體基本解決常用的所有東西。我們窺探一下runloop的真是模樣,通過CFRunLoopRef *runloop = CFRunLoopGetMain();檢視CFRunlooptypedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;,我們常用的CFRunLoopRef__CFRunLoop *型別的,那麼再在原始碼(可以下載最新的原始碼)中搜尋一下 struct __CFRunLoop {runloop.c 637行如下所示:

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* model list 鎖 */
    __CFPort _wakeUpPort;			// 接受 CFRunLoopWakeUp的埠
    Boolean _unused;//是否使用
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread; //執行緒
    uint32_t _winthread;//win執行緒
    CFMutableSetRef _commonModes; //modes
    CFMutableSetRef _commonModeItems; //modeItems
    CFRunLoopModeRef _currentMode; //當前的mode
    CFMutableSetRef _modes; //所有的modes
    struct _block_item *_blocks_head; //待執行的block列表頭部
    struct _block_item *_blocks_tail; //待執行的block 尾部
    CFAbsoluteTime _runTime; //runtime
    CFAbsoluteTime _sleepTime; //sleeptime
    CFTypeRef _counterpart; //
};
複製程式碼

經過簡化之後:

struct __CFRunLoop {
    pthread_t _pthread; //執行緒
    CFMutableSetRef _commonModes; //modes
    CFMutableSetRef _commonModeItems; //modeItems
    CFRunLoopModeRef _currentMode; //當前的mode
    CFMutableSetRef _modes; //所有的modes
}
複製程式碼
  1. runloop中包含一個執行緒_pthread,一一對應的
  2. CFMutableSetRef _modes可以有多個mode
  3. CFRunLoopModeRef _currentMode當前mode只能有一個

那麼mode裡邊有什麼內容呢?我們猜測他應該和runloop類似,在原始碼中搜尋CFRuntimeBase _base看到在runloop.c line 524看到具體的內容:

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 */
};
複製程式碼

經過簡化之後是:

struct __CFRunLoopMode {
    CFStringRef _name;//當前mode的名字
    CFMutableSetRef _sources0;//souces0
    CFMutableSetRef _sources1;//sources1
    CFMutableArrayRef _observers;//observers
    CFMutableArrayRef _timers;//timers
}
複製程式碼

一個mode可以有多個timersouces0souces1observerstimers 那麼使用圖更直觀的來表示:

iOS底層原理 RunLoop基礎總結和隨心所欲掌握子執行緒RunLoop生命週期 --(9)
一個runloop包含多個mode,但是同時只能執行一個mode,這點和大家開車的駕駛模式類似,運動模式和環保模式同時只能開一個模式,不能又運動又環保,明顯相悖。多個mode被隔離開有點是處理事情更專一,不會因為多個同時處理事情造成卡頓或者資源競爭導致的一系列問題。

souces0

  • 觸控事件
  • performSelector:onThread:

測試下點選事件處理源

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	NSLog(@"%s",__func__);//此處斷點
}

(LLDB) bt //輸出當前呼叫棧
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x000000010c5bb66d CFRunloop`::-[ViewController touchesBegan:withEvent:](self=0x00007fc69ec087e0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000012a01b0) at ViewController.mm:22:2
    frame #1: 0x0000000110685a09 UIKitCore`forwardTouchMethod + 353
    frame #2: 0x0000000110685897 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
    frame #3: 0x0000000110694c48 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1869
    frame #4: 0x00000001106965d2 UIKitCore`-[UIWindow sendEvent:] + 4079
    frame #5: 0x0000000110674d16 UIKitCore`-[UIApplication sendEvent:] + 356
    frame #6: 0x0000000110745293 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3232
    frame #7: 0x0000000110747bb9 UIKitCore`__handleEventQueueInternal + 5911
    frame #8: 0x000000010d8eabe1 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    frame #9: 0x000000010d8ea463 CoreFoundation`__CFRunLoopDoSources0 + 243
    frame #10: 0x000000010d8e4b1f CoreFoundation`__CFRunLoopRun + 1231
    frame #11: 0x000000010d8e4302 CoreFoundation`CFRunLoopRunSpecific + 626
    frame #12: 0x0000000115ddc2fe GraphicsServices`GSEventRunModal + 65
    frame #13: 0x000000011065aba2 UIKitCore`UIApplicationMain + 140
    frame #14: 0x000000010c5bb760 CFRunloop`main(argc=1, argv=0x00007ffee3643f68) at main.m:14:13
    frame #15: 0x000000010f1cb541 libdyld.dylib`start + 1
    frame #16: 0x000000010f1cb541 libdyld.dylib`start + 1
複製程式碼

#1看到現在是在佇列queue = 'com.apple.main-thread'中,#10 Runloop啟動,#9進入到__CFRunLoopDoSources0,最終__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__呼叫了__handleEventQueueInternal->[UIApplication sendEvent:]->[UIWindow sendEvent:]->[UIWindow _sendTouchesForEvent:]->[UIResponder touchesBegan:withEvent:]->-[ViewController touchesBegan:withEvent:](self=0x00007fc69ec087e0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000012a01b0) at ViewController.mm:22:2,可以看到另外一個知識點,手勢的傳遞是從上往下的,順序是UIApplication -> UIWindow -> UIResponder -> ViewController

Source1

  • 基於Port的執行緒間通訊
  • 系統事件捕捉

Timers

  • NSTimer
  • performSelector:withObject:afterDelay:
	timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
	static int count = 5;
	dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
	dispatch_source_set_event_handler(timer, ^{
		NSLog(@"-------:%d \n",count++);
	});
	dispatch_resume(timer);
	//log
	(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x0000000101f26457 CFRunloop`::__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x0000000101f28100) at ViewController.mm:72:33
    frame #1: 0x0000000104ac2db5 libdispatch.dylib`_dispatch_client_callout + 8
    frame #2: 0x0000000104ac5c95 libdispatch.dylib`_dispatch_continuation_pop + 552
    frame #3: 0x0000000104ad7e93 libdispatch.dylib`_dispatch_source_invoke + 2249
    frame #4: 0x0000000104acfead libdispatch.dylib`_dispatch_main_queue_callback_4CF + 1073
    frame #5: 0x00000001032568a9 CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
    frame #6: 0x0000000103250f56 CoreFoundation`__CFRunLoopRun + 2310
    frame #7: 0x0000000103250302 CoreFoundation`CFRunLoopRunSpecific + 626
	
複製程式碼

最終進入函式__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__呼叫了libdispatch_dispatch_main_queue_callback_4CF函式,具體實現有興趣的大佬可以看下原始碼的實現。

Observers

  • 用於監聽RunLoop的狀態
  • UI重新整理(BeforeWaiting)
  • Autorelease pool(BeforeWaiting)

Mode型別都多個,系統暴露在外的就兩個,

CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode;
CF_EXPORT const CFRunLoopMode kCFRunLoopCommonModes;
複製程式碼

那麼這兩個Mode都是在什麼情況下執行的呢?

  1. kCFRunLoopDefaultMode(NSDefaultRunLoopMode)App的預設Mode,通常主執行緒是在這個Mode下執行
  2. UITrackingRunLoopMode:介面跟蹤Mode,用於ScrollView 追蹤觸控滑動,保證介面滑動時不受其他Mode影響

進入到某個Mode,處理事情也應該有先後順序和休息的時間,那麼現在需要一個狀態來表示此時此刻的status,系統已經準備了CFRunLoopActivity來表示當前的狀態

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即將進入loop
    kCFRunLoopBeforeTimers = (1UL << 1),//即將處理timers
    kCFRunLoopBeforeSources = (1UL << 2), //即將處理sourcs
    kCFRunLoopBeforeWaiting = (1UL << 5),//即將進入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),//即將從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),//即將退出
    kCFRunLoopAllActivities = 0x0FFFFFFFU//所有狀態
};
複製程式碼

1UL表示無符號長整形數字1,再次看到這個(1UL << 1)我麼猜測用到了位域或者聯合體,達到省空間的目的。kCFRunLoopAllActivities = 0x0FFFFFFFU轉換成二進位制就是28個1,再進行mask的時候,所有的值都能取出來。

現在我們瞭解到:

  1. CFRunloopRef代表RunLoop的執行模式
  2. 一個Runloop包含若干個Mode,每個Mode包含若干個Source0/Source1/Timer/Obser
  3. Runloop啟動只能選擇一個Mode作為currentMode
  4. 如果需要切換Mode,只能退出當前Loop,再重新選擇一個Mode進入
  5. 不同組的Source0/Source1/Timer/Observer能分隔開來,互不影響
  6. 如果Mode沒有任何Source0/Source1/Timer/ObserverRunloop立馬退出。
runloop切換Mode
CFRunLoopObserverRef obs= CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
    	case kCFRunLoopEntry:{
    		CFRunLoopMode m = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
    		NSLog(@"即將進入 mode:%@",m);
    		CFRelease(m);
    		break;
    	}
    		
    	case kCFRunLoopExit:
    	{
    		CFRunLoopMode m = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
    		NSLog(@"即將退出 mode:%@",m);
    		CFRelease(m);
    		break;
    	}
    	default:
    		break;
    }
	});
	CFRunLoopAddObserver(CFRunLoopGetMain(), obs, kCFRunLoopCommonModes);
	CFRelease(obs);
	
	//當滑動tb的時候log
	
即將退出 mode:kCFRunLoopDefaultMode
即將進入 mode:UITrackingRunLoopMode
即將退出 mode:UITrackingRunLoopMode
即將進入 mode:kCFRunLoopDefaultMode
複製程式碼

runloop切換mode的時候,會退出當前kCFRunLoopDefaultMode,加入到其他的UITrackingRunLoopMode,當前UITrackingRunLoopMode完成之後再退出之後再加入到kCFRunLoopDefaultMode

我們再探究下runloop的迴圈的狀態到底是怎樣來變更的。

//	//獲取loop
	CFRunLoopRef ref = CFRunLoopGetMain();
	//獲取obs
	CFRunLoopObserverRef obs = CFRunLoopObserverCreate(kCFAllocatorDefault,kCFRunLoopAllActivities, YES, 0, callback, NULL);
	//新增監聽
	CFRunLoopAddObserver(ref, obs, CFRunLoopCopyCurrentMode(ref));
	CFRelease(obs);
	
	
int count = 0;//定義全域性變數來計算一個mode中狀態切換的統計資料
void callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
	printf("- ");
	count ++;
	printf("%d",count);
	switch (activity) {
		case kCFRunLoopEntry:
			printf("即將進入 \n");
			count = 0;
			break;
		case kCFRunLoopExit:
			printf("即將退出 \n");
			break;
		case kCFRunLoopAfterWaiting:
			printf("即將從休眠中喚醒 \n");
			break;
		case kCFRunLoopBeforeTimers:
			printf("即將進入處理 timers \n");
			break;
		case kCFRunLoopBeforeSources:
			printf("即將進入 sources \n");
			break;
		case kCFRunLoopBeforeWaiting:
			printf("即將進入 休眠 \n");
			count = 0;
			break;
		default:
			break;
	}
}

//點選的時候 會出發loop來處理觸控事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	NSLog(@"%s",__func__);
}

//log

- 1即將從休眠中喚醒 
- 2即將進入處理 timers 
- 3即將進入 sources 
-[ViewController touchesBegan:withEvent:]
- 4即將進入處理 timers 
- 5即將進入 sources 
- 6即將進入處理 timers 
- 7即將進入 sources 
- 8即將進入處理 timers 
- 9即將進入 sources 
- 10即將進入 休眠 
- 1即將從休眠中喚醒 
- 2即將進入處理 timers 
- 3即將進入 sources 
- 4即將進入處理 timers 
- 5即將進入 sources 
- 6即將進入 休眠 
- 1即將從休眠中喚醒 
- 2即將進入處理 timers 
- 3即將進入 sources 
- 4即將進入 休眠 
複製程式碼

runloop喚醒之後不是立馬處理事件的,而是看看timer有沒有事情,然後是sources,發現有觸控事件就處理了,然後又迴圈檢視timersources一般迴圈2次進入休眠狀態,處理source之後是迴圈三次。

RunLoop在不獲取的時候不存在,獲取才生成

RunLoop是在主動獲取的時候才會生成一個,主執行緒是系統自己呼叫生成的,子執行緒開發者呼叫,我們看下CFRunLoopGetCurrent

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}
複製程式碼

看到到這裡相信大家已經對runloop有了基本的認識,那麼我們再探究一下底層runloop是怎麼運轉的。

首先看官方給的圖:

iOS底層原理 RunLoop基礎總結和隨心所欲掌握子執行緒RunLoop生命週期 --(9)
那我又整理了一個表格來更直觀的瞭解狀態運轉

步驟 任務
1 通知Observers:進入Loop
2 通知Observers:即將處理Timers
3 通知Observers:即將處理Sources
4 處理blocks
5 處理Source0(可能再處理Blocks)
6 如果存在Source1,跳轉第8步
7 通知Observers:開始休眠
8 通知Observers:結束休眠1.處理Timer2.處理GCD Asyn To Main Queue 3.處理Source1
9 處理Blocks
10 根據前面的執行結果,決定如何操作1.返回第2步,2退出loop
11 通知Observers:退出Loop

檢視runloop原始碼runloop.c2333行

//入口函式
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();

    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
	return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
	rlm->_stopped = false;
	return kCFRunLoopRunStopped;
    }
    
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    } else if (seconds <= TIMER_INTERVAL_LIMIT) {
	dispatch_queue_t queue = pthread_main_np() ? __CFDispatchQueueGetGenericMatchingMain() : __CFDispatchQueueGetGenericBackground();
	timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
	timeout_context->ds = timeout_timer;
	timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
	timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
	dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
	dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    } else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_state_t voucherState = VOUCHER_MACH_MSG_STATE_UNCHANGED;
        voucher_t voucherCopy = NULL;
#endif
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#endif
	__CFPortSet waitSet = rlm->_portSet;

        __CFRunLoopUnsetIgnoreWakeUps(rl);
//通知即將處理Timers
        if (rlm->_observerMask & kCFRunLoopBeforeTimers)
			__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知即將處理Sources
        if (rlm->_observerMask & kCFRunLoopBeforeSources)
			__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//處理Blocks
	__CFRunLoopDoBlocks(rl, rlm);
//處理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
	//處理Block
            __CFRunLoopDoBlocks(rl, rlm);
	}
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            msg = (mach_msg_header_t *)msg_buffer;
	//y判斷是否有Source1
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
	//有則去 handle_msg
                goto handle_msg;
            }
#endif
        }
        didDispatchPortLastTime = false;
//即將進入休眠
	if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
	//開始休眠
	__CFRunLoopSetSleeping(rl);

    __CFPortSetInsert(dispatchPort, waitSet);
        
	__CFRunLoopModeUnlock(rlm);
	__CFRunLoopUnlock(rl);

        CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();

#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        do {
            if (kCFUseCollectableAllocator) {

                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            //等待訊息來喚醒當前執行緒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
			
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
          (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {

                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        if (kCFUseCollectableAllocator) {
            memset(msg_buffer, 0, sizeof(msg_buffer));
        }
        msg = (mach_msg_header_t *)msg_buffer;
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);

        rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));

        __CFPortSetRemove(dispatchPort, waitSet);
        
        __CFRunLoopSetIgnoreWakeUps(rl);

        // user callouts now OK again
	__CFRunLoopUnsetSleeping(rl);
	if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))
	//結束休眠
		__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//標籤 handle_msg
        handle_msg:;
        __CFRunLoopSetIgnoreWakeUps(rl);
		
        if (MACH_PORT_NULL == livePort) {
            CFRUNLOOP_WAKEUP_FOR_NOTHING();
            // handle nothing
        } else if (livePort == rl->_wakeUpPort) {
            CFRUNLOOP_WAKEUP_FOR_WAKEUP();
			
        }
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
	//被timer喚醒
			CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
#if USE_MK_TIMER_TOO
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
	//被GCD換醒
        else if (livePort == dispatchPort) {
            CFRUNLOOP_WAKEUP_FOR_DISPATCH();
            __CFRunLoopModeUnlock(rlm);
            __CFRunLoopUnlock(rl);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
	//處理GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            _CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
            __CFRunLoopLock(rl);
            __CFRunLoopModeLock(rlm);
            sourceHandledThisLoop = true;
            didDispatchPortLastTime = true;
        } else {
	//處理Source1
            CFRUNLOOP_WAKEUP_FOR_SOURCE();
			
            voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);

            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
            if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
		mach_msg_header_t *reply = NULL;
		sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
		if (NULL != reply) {
		    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
		    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
		}
#endif
	    }
            
            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
        }
        //處理bBlock
	__CFRunLoopDoBlocks(rl, rlm);
        
//設定返回值
	if (sourceHandledThisLoop && stopAfterHandle) {
	    retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
	} else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
	    retVal = kCFRunLoopRunStopped;
	} else if (rlm->_stopped) {
	    rlm->_stopped = false;
	    retVal = kCFRunLoopRunStopped;
	} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
	    retVal = kCFRunLoopRunFinished;
	}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
#endif
    } while (0 == retVal);
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    } else {
        free(timeout_context);
    }
    return retVal;
}
複製程式碼

經過及進一步精簡

//入口函式
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();

    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
	return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
	rlm->_stopped = false;
	return kCFRunLoopRunStopped;
    }

    Boolean didDispatchPortLastTime = true;
    int32_t retVal = 0;
    do {
        __CFRunLoopUnsetIgnoreWakeUps(rl);
//通知即將處理Timers
        if (rlm->_observerMask & kCFRunLoopBeforeTimers)
			__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//通知即將處理Sources
        if (rlm->_observerMask & kCFRunLoopBeforeSources)
			__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//處理Blocks
	__CFRunLoopDoBlocks(rl, rlm);
//處理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
	//處理Block
            __CFRunLoopDoBlocks(rl, rlm);
	}
            msg = (mach_msg_header_t *)msg_buffer;
	//y判斷是否有Source1
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
	//有則去 handle_msg
                goto handle_msg;
            }

//即將進入休眠
	if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
	//開始休眠
	__CFRunLoopSetSleeping(rl);
        do {
    //等待訊息來喚醒當前執行緒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
#else
	if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))
	//結束休眠
		__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//標籤 handle_msg
        handle_msg:;
	//被timer喚醒
			CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                __CFArmNextTimerInMode(rlm, rl);
            }

#if USE_MK_TIMER_TOO
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                __CFArmNextTimerInMode(rlm, rl);
            }
        }
#endif
	//被GCD換醒
        else if (livePort == dispatchPort) {
	//處理GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {
            CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
	//處理Source1
		sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
            // Restore the previous voucher
            _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
        }
        //處理bBlock
	__CFRunLoopDoBlocks(rl, rlm);
        
    //設定返回值
	if (sourceHandledThisLoop && stopAfterHandle) {
	    retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
	} else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
	    retVal = kCFRunLoopRunStopped;
	} else if (rlm->_stopped) {
	    rlm->_stopped = false;
	    retVal = kCFRunLoopRunStopped;
	} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
	    retVal = kCFRunLoopRunFinished;
	}
    } while (0 == retVal);
    return retVal;
}
複製程式碼

精簡到這裡基本都能看懂了,還寫了很多註釋,基本和上面整理的表格一致。 這裡的執行緒休眠__CFRunLoopServiceMachPort是呼叫核心函式mach_msg()進行休眠,和我們平時while(1)大不同,while(1)叫死迴圈,其實系統每時每刻都在判斷是否符合條件,耗費很高的CPU,核心則不同,Mach核心提供面向訊息,基於基礎的程式間通訊。

保活機制

一個程式執行完畢結束了就死掉了,timer和變數也一樣,執行完畢就結束了,那麼我們怎麼可以保證timer一直活躍和執行緒不結束呢?

timer保活和多mode執行

timer可以新增到self的屬性保證一直活著,只要self不死,timer就不死。timer預設是新增到NSDefaultRunLoopMode模式中,因為RunLoop同時執行只能有一個模式,那麼在滑動scroller的時候怎Timer會卡頓停止直到再次切換回來,那麼如何保證同時兩個模式都可以執行呢? Foundation提供了一個API(void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode新增上,mode值為NSRunLoopCommonModes可以保證同時兼顧2種模式。

測試程式碼:

static int i = 0;
NSTimer *timer=[NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
	NSLog(@"%d",++i);
}];
//NSRunLoopCommonModes 並不是一個真正的模式,它這還是一個標記
//timer在設定為common模式下能執行
//NSRunLoopCommonModes 能在 _commentModes中陣列中的模式都可以執行
//[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];//預設的模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

//log
	
2019-07-23 15:14:31 CFRunloop[62358:34093079] 1
2019-07-23 15:14:32 CFRunloop[62358:34093079] 2
2019-07-23 15:14:33 CFRunloop[62358:34093079] 3
2019-07-23 15:14:34 CFRunloop[62358:34093079] 4
2019-07-23 15:14:35 CFRunloop[62358:34093079] 5
2019-07-23 15:14:36 CFRunloop[62358:34093079] 6
2019-07-23 15:14:37 CFRunloop[62358:34093079] 7
2019-07-23 15:14:38 CFRunloop[62358:34093079] 8
複製程式碼

當滑動的時候timer的時候,timer還是如此絲滑,沒有一點停頓。 沒有卡頓之後我們VC -> dealloctimer還是在執行,那麼需要在dealloc中去下和刪除觀察者

-(void)dealloc{
	NSLog(@"%s",__func__);
	CFRunLoopRemoveObserver(CFRunLoopGetMain(), obs, m);
	dispatch_source_cancel(timer);
}
複製程式碼

退出vc之後dealloc照常執行,日誌只有-[ViewController dealloc],而且數字沒有繼續輸出,說明刪除觀察者和取消source都成功了。

那麼NSRunLoopCommonModes是另外一種模式嗎?

通過原始碼檢視得知,在runloop.c line:1632 line:2608

if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
    doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
    } else {
    doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode));
    }
複製程式碼

還有很多地方均可以看出,當是currentMode需要和_mode相等才去執行,當是kCFRunLoopCommonModes的時候,只需要包含curMode即可執行。可見kCFRunLoopCommonModes其實是一個集合,不是某個特定的mode

執行緒保活

執行緒為什麼需要保活?效能其實很大的瓶頸是在於空間的申請和釋放,當我們執行一個任務的時候建立了一個執行緒,任務結束就釋放掉該執行緒,如果任務頻率比較高,那麼一個一直活躍的執行緒來執行我們的任務就省去申請和釋放空間的時間和效能。上邊已經講過了 runloop需要有任務才能不退出,總不可能直接讓他執行while(1)吧,這種方法明顯不對的,由原始碼得知,當有監測埠的時候,也不會退出,也不會影響應能。所以線上程初始化的時候使用

[[NSRunLoop currentRunLoop] addPort:[NSPort port] 
                            forMode:NSRunLoopCommonModes];
複製程式碼

來保活。 在主執行緒使用是沒有意義的,系統已經在APP啟動的時候進行了呼叫,則已經加入到全域性的字典中了。

驗證執行緒保活

@property (nonatomic,strong) FYThread *thread;


- (void)viewDidLoad {
	[super viewDidLoad];
	self.thread=[[FYThread alloc]initWithTarget:self selector:@selector(test) object:nil];
	_thread.name = @"test thread";
	[_thread start];
}
- (void)test {
//新增埠
	[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
	
	NSLog(@"%@",[NSThread currentThread]);
	NSLog(@"--start--");
	[[NSRunLoop currentRunLoop] run];
	NSLog(@"--end--");
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	NSLog(@"%s",__func__);
	[self performSelector:@selector(alive) onThread:self.thread withObject:nil waitUntilDone:NO];
	NSLog(@"執行完畢了子執行緒");//不執行 因為子執行緒保活了 不會執行完畢
}
//測試子執行緒是否還活著
- (void)alive{
	NSLog(@"我還活著呢->%@",[NSThread currentThread]);
}
//log
//註釋掉新增埠程式碼
<FYThread: 0x6000013a9540>{number = 3, name = test thread}
--start--
--end--
-[ViewController touchesBegan:withEvent:]
執行完畢了子執行緒



//註釋放開的時候點選觸發log
<FYThread: 0x6000013a9540>{number = 3, name = test thread}
--start--

-[ViewController touchesBegan:withEvent:]
執行完畢了子執行緒
我還活著呢-><FYThread: 0x6000017e5c80>{number = 3, name = test thread}
複製程式碼

[[NSRunLoop currentRunLoop] addPort:[NSPort port]forMode:NSDefaultRunLoopMode]新增埠註釋掉,直接執行了--end--,執行緒雖然strong強引用,但是runloop已經退出了,所以函式alive沒有執行,不註釋的話,alive還會執行,end一直不會執行,因為進入了runloop,而且沒有退出,程式碼就不會向下執行。

那我們測試下該執行緒宣告週期多長?

- (void)viewDidLoad {
	[super viewDidLoad];
	self.thread=[[FYThread alloc]initWithTarget:self selector:@selector(test) object:nil];
	_thread.name = @"test thread";
	[_thread start];
}
- (void)test {
	[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
	//獲取obs
	NSLog(@"%@",[NSThread currentThread]);
	NSLog(@"--start--");
	/*
	 If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
	 */
	[[NSRunLoop currentRunLoop] run];
	NSLog(@"--end--");
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	NSLog(@"%s",__func__);
	[self performSelector:@selector(alive) onThread:self.thread withObject:nil waitUntilDone:NO];
	NSLog(@"執行完畢了子執行緒");//不執行 因為子執行緒保活了 不會執行完畢
}
//返回上頁
- (IBAction)popVC:(id)sender {
	[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
//測試子執行緒是否還活著
- (void)alive{
	NSLog(@"我還活著呢->%@",[NSThread currentThread]);
}
//停止子執行緒執行緒
- (void)stop{
	CFRunLoopStop(CFRunLoopGetCurrent());
	NSLog(@"%s",__func__);
}
- (void)dealloc{
	NSLog(@"%s",__func__);
}

//log

<FYThread: 0x600003394780>{number = 3, name = test thread}
--start--
-[ViewController stop]
-[ViewController stop]

複製程式碼

擁有該執行緒的是VC,點選pop的時候,但是VCthread沒釋放掉,好像threadVC建立的迴圈引用,當self.thread=[[FYThread alloc]initWithTarget:self selector:@selector(test) object:nil];註釋了,則VC可以進行正常釋放。

通過測試瞭解到 這個執行緒達到了永生,就是你殺不死他,簡直了死待。查詢了不少資料才發現官方文件才是最穩的。有對這句[[NSRunLoop currentRunLoop] run]的解釋

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

就是系統寫了以一個死迴圈但是沒有阻止他的引數,相當於一直在迴圈呼叫 runMode:beforeDate:,那麼該怎麼辦呢? 官方文件給出瞭解決方案

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
複製程式碼

將程式碼改成下面的成功將死待殺死了。

- (void)test {
	[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
	//獲取obs
	NSLog(@"%@",[NSThread currentThread]);
	NSLog(@"--start--");
	self.shouldKeepRunning = YES;//預設執行
	NSRunLoop *theRL = [NSRunLoop currentRunLoop];
	while (_shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
	NSLog(@"--end--");
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
	NSLog(@"%s",__func__);
	[self performSelector:@selector(alive) onThread:self.thread withObject:nil waitUntilDone:NO];
	NSLog(@"執行完畢了子執行緒");//不執行 因為子執行緒保活了 不會執行完畢
}
//返回上頁
- (IBAction)popVC:(id)sender {
	self.shouldKeepRunning = NO;
	[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
//測試子執行緒是否還活著
- (void)alive{
	NSLog(@"我還活著呢->%@",[NSThread currentThread]);
}
//停止子執行緒執行緒
- (void)stop{
	CFRunLoopStop(CFRunLoopGetCurrent());
	NSLog(@"%s",__func__);
	[self performSelectorOnMainThread:@selector(pop) withObject:nil waitUntilDone:NO];
}
- (void)pop{
	[self.navigationController popViewControllerAnimated:YES];

}
- (void)dealloc{
	NSLog(@"%s",__func__);
}

//log

<FYThread: 0x600002699fc0>{number = 3, name = test thread}
--start--
-[ViewController stop]
--end--
-[ViewController dealloc]
-[FYThread dealloc]
複製程式碼

點選popVC:首先將self.shouldKeepRunning = NO,然後子執行緒執行CFRunLoopStop(CFRunLoopGetCurrent()),然後在主執行緒執行pop函式,最終返回上級頁面而且成功殺死VC死待。 當然這個死待其實也是有用處的,當使用單例模式作為下載器的時候使用死待也沒問題。這樣子處理比較複雜,我們可以放在VCdealloc看看是否能成功。 關鍵函式稍微更改:

//停止子執行緒執行緒
- (void)stop{
    if (self.thread == nil) {
        return;
    }
	NSLog(@"%s",__func__);
        [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)stopThread{
    self.shouldKeepRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

- (void)dealloc{
    [self stop];
	NSLog(@"%s",__func__);
}
複製程式碼

當點選返回按鈕VC和執行緒都沒死,原來他們形成了強引用無法釋放,就是VC始終無法執行dealloc。將函式改成block實現

    __weak typeof(self) __weakSelf = self;
    self.thread = [[FYThread alloc]initWithBlock:^{
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        NSLog(@"%@",[NSThread currentThread]);
        NSLog(@"--start--");
        __weakSelf.shouldKeepRunning = YES;//預設執行
        NSRunLoop *theRL = [NSRunLoop currentRunLoop];
        while (__weakSelf && __weakSelf.shouldKeepRunning  ){
            [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        };
        NSLog(@"--end--");
    }];
複製程式碼

測試下崩潰了,崩潰到了:

while (__weakSelf.shouldKeepRunning  ){
        [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];//崩潰的地方
    };
複製程式碼

怎麼想感覺不對勁啊,怎麼會不行呢?VC銷燬的時候呼叫子執行緒stop,最後打斷點發現到了崩潰的地方self已經不存在了,說明是非同步執行的,往前查詢使用非同步的函式最後出現在了[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];,表示不用等待stopThread函式執行時間,直接向前繼續執行,所以VC釋放掉了,while (__weakSelf.shouldKeepRunning )true,還真進去了,訪問了exe_bad_access,所以改成while (__weakSelf&&__weakSelf.shouldKeepRunning )再跑一下

//log

--start--
-[ViewController stop]
-[ViewController dealloc]
--end--
-[FYThread dealloc]
複製程式碼

如牛奶般絲滑,解決了釋放問題,也解決了複雜操作。本文章所有程式碼均在底部連結可以下載。 使用這個思路自己封裝了一個簡單的功能,大家可以自己封裝一下然後對比一下我的思路,說不定有驚喜!

資料參考

資料下載


最怕一生碌碌無為,還安慰自己平凡可貴。

廣告時間

iOS底層原理 RunLoop基礎總結和隨心所欲掌握子執行緒RunLoop生命週期 --(9)

相關文章