RunLoop從字面上解析,就是一直循環的跑,實際上它也是在一直在跑。一般來講,一個線程執行完一個任務後,線程就會退出銷燬。可是咱們能夠經過RunLoop操做,使該線程常駐,在有任務的時候喚醒線程執行相應的任務,在沒有任務執行的時候保存睡眠狀態,時刻準備着任務的呼喚。想一想爲何一個程序可以隨時響應操做事件,原理也是同樣的。html
在iOS系統中,提供了NSRunLoop
和CFRunLoopRef
兩種對象來供咱們操做。因爲CFRunLoopRef
是CoreFoundation提供的純C函數的API,全部的這些都是線程安全的,能夠被任何線程調用。而NSRunLoop
是基於CFRunLoopRef
封裝的提供面向對象的API,這是API不是線程安全的,僅僅在run loop對應的那個線程上操做,若是添加一個輸入源或者定時器給非當前線程的run loop會致使崩潰現象發生。ios
//獲取當前線程的RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void);
//獲取主線程的RunLoop
CFRunLoopRef CFRunLoopGetMain(void);複製代碼
//無限期的在默認模式下運行RunLoop,直到運行循環中止,或者將全部源和定時器從默認運行循環模式中移除
void CFRunLoopRun(void);
/* 在特定的模式運行RunLoop * * mode 運行的模式 * seconds 運行循環的時間 * returnAfterSourceHandled 處理一個源的循環後是否應該退出 */
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
//喚醒等待的RunLoop,當處於睡眠時,若是沒有輸入源或者定時器的觸發,將一直處於睡眠,假若運行循環被修改,如添加新的輸入源,則須要喚醒RunLoop處理事件
void CFRunLoopWakeUp(CFRunLoopRef rl);
//中止運行RunLoop
void CFRunLoopStop(CFRunLoopRef rl);
//運行循環是否在等待事件
Boolean CFRunLoopIsWaiting(CFRunLoopRef rl);複製代碼
//將輸入源添加到RunLoop
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
/* 是否包含輸入源 * * source 匹配的輸入源 * mode 特有的模式下 */
Boolean CFRunLoopContainsSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
//從RunLoop中移除輸入源
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);複製代碼
//將一個模式添加到一組運行循環,一旦添加到運行循環模式中,將沒法刪除
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
//返回一個模式數組
CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl);
//複製當前的模式
CFRunLoopMode CFRunLoopCopyCurrentMode(CFRunLoopRef rl);複製代碼
//將定時器添加到runLoop
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
//返回定時器下一個觸發時間
CFAbsoluteTime CFRunLoopGetNextTimerFireDate(CFRunLoopRef rl, CFRunLoopMode mode);
//移除定時器
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
//是否包含定時器
Boolean CFRunLoopContainsTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);複製代碼
CFTypeID CFRunLoopGetTypeID(void);複製代碼
一個RunLoop包含N個CFRunLoopModeRef
,每一個Mode中又包含了N個CFRunLoopSourceRef
、CFRunLoopTimerRef
、CFRunLoopObserverRef
。可是每次調用的時候,只能指定某一個Mode類型,若想切換Mode類型,必須退出RunLoop再建立RunLoop。git
在NSRunLoop中,只提供了兩種類型的模式github
CFRunLoopSourceRef是產生事件的地方,分爲兩種:api
/* 建立CFRunLoopSource對象 * * allocator 分配內存適配器 使用NULL或者kCFAllocatorDefault * order 處理運行循環的優先級 * context runLoopSource上下文信息 */
CFRunLoopSourceRef CFRunLoopSourceCreate(CFAllocatorRef allocator, CFIndex order, CFRunLoopSourceContext *context);
/* 獲取RunLoopSource上下文信息 * * source 輸入源 * context 上下文信息的指針 */
void CFRunLoopSourceGetContext(CFRunLoopSourceRef source, CFRunLoopSourceContext *context);
/* 獲取RunLoopSource優先級 * * source 輸入源 */
CFIndex CFRunLoopSourceGetOrder(CFRunLoopSourceRef source);
//返回類型標識符
CFTypeID CFRunLoopSourceGetTypeID(void);
//移除輸入源,阻止再次觸發
void CFRunLoopSourceInvalidate(CFRunLoopSourceRef source);
//判斷輸入源是否有效
Boolean CFRunLoopSourceIsValid(CFRunLoopSourceRef source);
//發送輸入源信息,並標記爲準備啓動
void CFRunLoopSourceSignal(CFRunLoopSourceRef source);複製代碼
//當從循環中移除輸入源時回調
typedef void (*CFRunLoopCancelCallBack) (
void *info, //CFRunLoopSourceContext
CFRunLoopRef rl, //正在移除輸入源的RunLoop
CFStringRef mode //循環模式
);
//兩個輸入源是否相等的回調
typedef Boolean (*CFRunLoopEqualCallBack) (
const void *info1, // CFRunLoopSourceContext or CFRunLoopSourceContext1
const void *info2 // CFRunLoopSourceContext or CFRunLoopSourceContext1
);
//獲取source1 輸入源的mach端口
typedef mach_port_t (*CFRunLoopGetPortCallBack) (
void *info //CFRunLoopSourceContext1
);
//info對象hash值的回調
typedef CFHashCode (*CFRunLoopHashCallBack) (
const void *info
);
//接收到mach端口信息時回調
typedef void *(*CFRunLoopMachPerformCallBack) (
void *msg, //mach信息
CFIndex size, //信息的大小
CFAllocatorRef allocator, //內存分配
void *info //CFRunLoopSourceContext1
);
//收到RunLoopSource對象須要處理信息時回調
typedef void (*CFRunLoopPerformCallBack) (
void *info //CFRunLoopSourceContext
);
//將輸入源添加到RunLoop時回調
typedef void (*CFRunLoopScheduleCallBack) (
void *info, //CFRunLoopSourceContext
CFRunLoopRef rl, //正在循環的RunLoop
CFStringRef mode //模式
);複製代碼
typedef struct {
CFIndex version;//版本號必須爲0
void * info;//指向數據的任意指針
const void *(*retain)(const void *info);//保留info指針時回調,能夠爲NULL
void (*release)(const void *info);//定義info指針執行是回調,能夠爲NULL
CFStringRef (*copyDescription)(const void *info);//定義info指針複製的回調,能夠爲NULL
Boolean (*equal)(const void *info1, const void *info2);//比較的回調,,能夠爲NULL
CFHashCode (*hash)(const void *info);//定義info指正hash的回調,能夠爲NULL
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);//添加輸入源時回調,能夠爲NULL
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);//移除輸入源的回調,能夠爲NULL
void (*perform)(void *info);//運行循環執行觸發時回調
} CFRunLoopSourceContext;複製代碼
CFRunLoopTimerRef
主要是定時器,將定時器加入RunLoop中,到時間點到的時候,RunLoop喚醒定時器的執行方法數組
/* 使用塊初始化RunLoop對象 * * allocator 內存適配器,使用NULL或者kCFAllocatorDefault * fireDate 首次觸發的時間 * interval 定時器觸發的間隔 * flags 該字段目前被忽略,0 * order 處理的優先級,定時器可忽略,值爲0 * block 定時器觸發的塊 */
CFRunLoopTimerRef CFRunLoopTimerCreateWithHandler(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, void (^block)(CFRunLoopTimerRef timer));
/* 建立RunLoopTimer對象 * * allocator 內存適配器,使用NULL或者kCFAllocatorDefault * fireDate 首次觸發的時間 * interval 定時器觸發的間隔 * flags 該字段目前被忽略,0 * order 處理的優先級,定時器可忽略,值爲0 * callout 定時器觸發時的回調函數 * context 上下文信息,不須要時可爲NULL */
CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator, CFAbsoluteTime fireDate, CFTimeInterval interval, CFOptionFlags flags, CFIndex order, CFRunLoopTimerCallBack callout, CFRunLoopTimerContext *context);
//判斷RunLoopTimer是否重複
Boolean CFRunLoopTimerDoesRepeat(CFRunLoopTimerRef timer);
//聯繫上下文
void CFRunLoopTimerGetContext(CFRunLoopTimerRef timer, CFRunLoopTimerContext *context);
//返回時間間隔
CFTimeInterval CFRunLoopTimerGetInterval(CFRunLoopTimerRef timer);
//下一個觸發時間
CFAbsoluteTime CFRunLoopTimerGetNextFireDate(CFRunLoopTimerRef timer);
//返回優先級
CFIndex CFRunLoopTimerGetOrder(CFRunLoopTimerRef timer);
//返回標識符
CFTypeID CFRunLoopTimerGetTypeID(void);
//移除RunLoopTimer,防止再次觸發
void CFRunLoopTimerInvalidate(CFRunLoopTimerRef timer);
//判斷是否有效
Boolean CFRunLoopTimerIsValid(CFRunLoopTimerRef timer);
//設置下一個啓動日期
void CFRunLoopTimerSetNextFireDate(CFRunLoopTimerRef timer, CFAbsoluteTime fireDate);
/*定時器觸發時回調 * * info CFRunLoopTimerContext */
typedef void (*CFRunLoopTimerCallBack)(CFRunLoopTimerRef timer, void *info);
typedef struct {
CFIndex version; //版本號,必須爲0
void * info; //任意指針
const void *(*retain)(const void *info);//retain指針,能夠爲NULL
void (*release)(const void *info);//release指針能夠爲NULL
CFStringRef (*copyDescription)(const void *info); //定義info指針的描述回調,能夠爲NULL
} CFRunLoopTimerContext;複製代碼
CFRunLoopObserverRef
CFRunLoopObserverRef
是觀察者,關注着RunLoop的狀態變化,RunLoop主要有六個狀態安全
/* 建立RunLoopObserver觀察者 * * allocator 內存分配器,使用NULL或kCFAllocatorDefault * activities 活動的類型 * repeats 一個或者多個經過運行循環調用的標誌,若是爲no,即便在多個階段被調用也無效 * order 優先級別 * block 運行時調用 */
CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block)(CFRunLoopObserverRef observer, CFRunLoopActivity activity));
/* 建立RunLoopObserver觀察者 * * allocator 內存分配器,使用NULL或kCFAllocatorDefault * activities 活動的類型 * repeats 一個或者多個經過運行循環調用的標誌 * order 優先級別 * callout 運行時回調 * context 聯繫上下文 */
CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
//觀察者是否重複
Boolean CFRunLoopObserverDoesRepeat(CFRunLoopObserverRef observer);
//返回循環的運行階段
CFOptionFlags CFRunLoopObserverGetActivities(CFRunLoopObserverRef observer);
//聯繫上下文
void CFRunLoopObserverGetContext(CFRunLoopObserverRef observer, CFRunLoopObserverContext *context);
//獲取優先級
CFIndex CFRunLoopObserverGetOrder(CFRunLoopObserverRef observer);
//獲取類型標識符
CFTypeID CFRunLoopObserverGetTypeID(void);
//移除觀察者
void CFRunLoopObserverInvalidate(CFRunLoopObserverRef observer);
//觀察者是否可以觸發
Boolean CFRunLoopObserverIsValid(CFRunLoopObserverRef observer);複製代碼
/* 觀察者對象被觸發時回調 * * observer 觀察者 * activity 活動的階段 * info CFRunLoopObserverContext */
typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);複製代碼
typedef struct {
CFIndex version; //版本號必須爲0
void * info; //定義數據的任意指針
const void *(*retain)(const void *info); //retain指針,能夠爲NULL
void (*release)(const void *info); //release指針,能夠爲NULL
CFStringRef (*copyDescription)(const void *info); //info指針複製的回調
} CFRunLoopObserverContext;複製代碼
- (void)createObserver{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer) {
CFRunLoopRef cfLoop = [runLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
}
void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
NSString *threadName = [NSThread currentThread].name;
if (activity & kCFRunLoopEntry) {
NSLog(@"%@進入runloop",threadName);
}else if (activity & kCFRunLoopBeforeTimers){
NSLog(@"%@即將處理Timers",threadName);
}else if (activity & kCFRunLoopBeforeSources){
NSLog(@"%@即將處理Sources",threadName);
}else if (activity & kCFRunLoopBeforeWaiting){
NSLog(@"%@即將處理進入睡眠",threadName);
}else if (activity & kCFRunLoopAfterWaiting){
NSLog(@"%@從睡眠中喚醒",threadName);
}else if (activity & kCFRunLoopExit){
NSLog(@"%@退出runloop",threadName);
}
}複製代碼
蘋果並不容許咱們建立RunLoop對象,只能經過api去獲取當前的RunLoop對象。
在NSRunLoop
中,提供了兩個屬性幫助咱們讀取RunLoop對象bash
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
@property (class, readonly, strong) NSRunLoop *mainRunLoop;複製代碼
在CFRunLoopRef
中,也提供了兩個方法獲取CFRunLoopRef函數
CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);複製代碼
RunLoop和線程時一一對象的關係,線程在建立的時候並不會建立RunLoop對象,只有主動獲取線程中的RunLoop對象中,纔會去檢測線程中是否擁有RunLoop對象,有則返回該對象;若是沒有則建立RunLoop並返回對象oop
注意
:應該避免使用GCD Global隊列來建立RunLoop的常駐線程,由於在建立時可能出現全局隊列的線程池滿了的狀況,所以GCD沒法派發任務,頗有可能形成奔潰。
必須給RunLoop添加一個輸入源或者一個定時器才能在輔助線程上開啓RunLoop,若是一個RunLoop沒有任何源來監控,就會馬上退出。
//無條件運行RunLoop將線程放在一個永久的循環中,沒法在定製模式下運行RunLoop
- (void)run;
//在limitDate未到達以前,RunLoop會一直保持着運行
- (void)runUntilDate:(NSDate *)limitDate;
//RunLoop會被執行一次,時間達到或者時間處理完畢後,自動退出
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;複製代碼
上述三個方法中,其實使用上面的兩個方式啓動保持一直運行,也就是不斷的重複調用最後一個方法。
注意
:CFRunLoopStop() 方法只會結束當前的 runMode:beforeDate: 調用,而不會結束後續的調用。
雖然有四種方式能夠退出RunLoop循環,可是官方建議給RunLoop配置一個超時時間或者中止RunLoop來退出RunLoop。不建議使用移除runLoop的輸入源和定時器來使RunLoop退出,由於有些系統程序給run loop增長輸入源處理必要的事件。此時代碼可能沒有注意到這些輸入源的存在,於是不能徹底移除掉這些輸入源,導致RunLoop沒法退出。
#import "ExitViewController.h"
#import "CustomThread.h"
@interface ExitViewController (){
NSRunLoop *_runLoop;
NSMachPort *_machPort;
__weak CustomThread *_thread;
BOOL _isStopRunLoop;
}
@end
@implementation ExitViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor grayColor];
CustomThread *thread = [[CustomThread alloc] initWithTarget:self selector:@selector(createRunLoop) object:nil];
[thread setName:@"com.donyau.thread"];
[thread start];
_thread = thread;
}
- (void)createRunLoop{
NSLog(@"當前線程%@",[NSThread currentThread]);
_runLoop = [NSRunLoop currentRunLoop];
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer) {
CFRunLoopRef cfLoop = [_runLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
[self runLoopStop];
NSLog(@"執行");
}
void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
NSString *threadName = [NSThread currentThread].name;
if (activity & kCFRunLoopEntry) {
NSLog(@"%@進入runloop",threadName);
}else if (activity & kCFRunLoopBeforeTimers){
NSLog(@"%@即將處理Timers",threadName);
}else if (activity & kCFRunLoopBeforeSources){
NSLog(@"%@即將處理Sources",threadName);
}else if (activity & kCFRunLoopBeforeWaiting){
NSLog(@"%@即將處理進入睡眠",threadName);
}else if (activity & kCFRunLoopAfterWaiting){
NSLog(@"%@從睡眠中喚醒",threadName);
}else if (activity & kCFRunLoopExit){
NSLog(@"%@退出runloop",threadName);
}
}
#pragma mark - CFRunLoopStop
- (void)runLoopStop{
_machPort = (NSMachPort *)[NSMachPort port];
[_runLoop addPort:_machPort forMode:NSRunLoopCommonModes];
[self performSelector:@selector(logRunLoopStop:) withObject:@"1" afterDelay:4];
[self performSelector:@selector(logRunLoopStop:) withObject:@"2" afterDelay:8];
while (!_isStopRunLoop && [_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
}
}
- (void)logRunLoopStop:(NSString *)mesg{
NSLog(@"%@--%@",[NSThread currentThread],mesg);
_isStopRunLoop = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
}
@end複製代碼
DYCustomSourceThread.h
#import <Foundation/Foundation.h>
#import "DYRunLoopSource.h"
@interface DYCustomSourceThread : NSThread
@property (nonatomic, strong) DYRunLoopSource *runLoopSource;
@end複製代碼
DYCustomSourceThread.m
#import "DYCustomSourceThread.h"
#import "DYRunLoopSource.h"
@interface DYCustomSourceThread (){
}
@end
@implementation DYCustomSourceThread
void currentRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
NSString *threadName = [NSThread currentThread].name;
if (activity & kCFRunLoopEntry) {
NSLog(@"%@進入runloop",threadName);
}else if (activity & kCFRunLoopBeforeTimers){
NSLog(@"%@即將處理Timers",threadName);
}else if (activity & kCFRunLoopBeforeSources){
NSLog(@"%@即將處理Sources",threadName);
}else if (activity & kCFRunLoopBeforeWaiting){
NSLog(@"%@即將處理進入睡眠",threadName);
}else if (activity & kCFRunLoopAfterWaiting){
NSLog(@"%@從睡眠中喚醒",threadName);
}else if (activity & kCFRunLoopExit){
NSLog(@"%@退出runloop",threadName);
}
}
-(void)main{
@autoreleasepool {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
_runLoopSource = [[DYRunLoopSource alloc] init];
[_runLoopSource addToCurrentRunLoop];
CFRunLoopObserverContext context = {0, (__bridge void*)(self),NULL,NULL,NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ¤tRunLoopObserver, &context);
if (observer) {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopDefaultMode);
}
[runLoop run];
}
}
@end複製代碼
DYRunLoopSource.h
#import <Foundation/Foundation.h>
@interface DYRunLoopSource : NSObject
//將輸入源添加到RunLoop
- (void)addToCurrentRunLoop;
//移除輸入源
- (void)invalidate;
//喚醒RunLoop
- (void)wakeUpRunLoop;
@end複製代碼
DYRunLoopSource.m
#import "DYRunLoopSource.h"
@interface DYRunLoopSource (){
CFRunLoopSourceRef _runLoopSource;
CFRunLoopRef _runLoop;
}
@end
@implementation DYRunLoopSource
#pragma mark 輸入源添加進RunLoop時調用
void runLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"RunLoop添加輸入源");
}
#pragma mark 將輸入源從RunLoop移除時調用
void runLoopSourceCancelRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode){
NSLog(@"移除輸入源");
}
#pragma mark 輸入源須要處理信息時調用
void runLoopSourcePerformRoutine (void *info){
NSLog(@"輸入源正在處理任務");
}
-(instancetype)init{
if (self = [super init]) {
CFRunLoopSourceContext context = {0,(__bridge void *)(self),NULL,NULL,NULL,NULL,NULL,&runLoopSourceScheduleRoutine,&runLoopSourceCancelRoutine,&runLoopSourcePerformRoutine};
_runLoopSource = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
}
return self;
}
-(void)addToCurrentRunLoop{
_runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(_runLoop, _runLoopSource, kCFRunLoopDefaultMode);
}
- (void)invalidate{
CFRunLoopRemoveSource(_runLoop, _runLoopSource, kCFRunLoopDefaultMode);
[self wakeUpRunLoop];
}
-(void)wakeUpRunLoop{
if (CFRunLoopIsWaiting(_runLoop) && CFRunLoopSourceIsValid(_runLoopSource)) {
CFRunLoopSourceSignal(_runLoopSource);
CFRunLoopWakeUp(_runLoop);
}
}
@end複製代碼
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"執行timer事件");
[timer invalidate];
}];
[_runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
[_runLoop run];複製代碼
NSMachPort *machPort = (NSMachPort *)[NSMachPort port];
[_runLoop addPort:machPort forMode:NSRunLoopCommonModes];複製代碼
NSRunLoop的退出方式
避免使用 GCD Global隊列建立Runloop常駐線程
深刻研究 Runloop 與線程保活
深刻理解RunLoop