RunLoop解析(视频+原版文字)

参考(抄袭)资料

  1. 深入理解RunLoop,by @Ibireme
  2. 孙源的线下分享视频低清在线,高清无码视频,Key Note 文件,by @Sunnyxx  
  3. RunLoop 的苹果官方文档



建议搭配以上资料辅助阅读


RunLoop 是什么鬼

首先,在一般情况下,代码的执行是线性的,执行完成之后就会退出返回:

int main(int argc, char *argv[]) {
	NSLog(@"hello world");
	return 0;
}


通常我们创建线程来处理自己的任务,也是这样的线性执行流程,当我们任务完成之后,便退出然后销毁线程。


但是对于一个 APP 来说,这种线性的执行流程,就不适用了。总不能让 APP 一打开,然后显示一下第一个页面,接着就马上退出了吧。得想一种办法,让 APP 的主线程能够一直驻留。在用户触发事件的时候,对其做出响应;在 APP 空闲的时候进入休眠,停止占用 CPU。这种模型通常被称为 Event Loop,事件循环。Run Loop 实现了这种事件处理机制。事件驱动型代码结构一般形式如下:

int main(int argc, char * argv[]) {
	while(AppIsRunning) {
		id whoWakesMe = SleepForWakingUp();
		id event = GetEvent(whoWakesMe);
		HandleEvent(event);
	}

	return 0;
}

Event Loop 结构中的重点有两部分:

  • 外部的 while 循环结构,它保证了线程在处理完事件之后不会退出;
  • SleepForWakingUp 函数,让线程在没有事件需要处理的时候陷入休眠,让出 CPU。当没有事件需要处理时,代码的实行会停在这个函数的调用处,线程在这里进入休眠状态;当事件到来时,线程被激活,whoWakesMe 获得返回值,代码从原来的休眠处重新跑起来,执行余下的操作。

再举一个简单的例子,名为“程序猿的 main thread”:

// by @Sunnyxx
while(活着) {
	有事干了 = 我睡觉了没事别叫我();

	if (该搬砖了) {
		搬砖();
	} else if (该吃饭了) {
		吃饭();
	} else if (该陪妹子了) {
		@throw(没有妹子);
	}
}


在这里,我不负责任地用自己的话总结一下:run loop 是一种消息处理机制,它让线程能一直驻留而不退出,并且在闲时休眠,在事件到达时处理事件

RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),然后这个入口函数返回。(by ibireme)


因为有了 run loop 的存在,使得:

  • 程序能一直运行并接受用户输入
  • 决定程序在何时该处理哪些事件
  • 调用解耦:例如主调方产生事件之后放入消息队列,让被调方自己取来处理,而不必等待被调方返回
  • 节省 CPU 时间:没事件处理时休眠


CFRunLoopRef 的源代码是开源,可以在这个链接下载到整个 CoreFoundation 的源码。为了方便跟踪和查看,你可以新建一个 Xcode 工程,把这堆源码拖进去看。

Run Loops in Cocoa


CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的;
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。


GCD 跟 RunLoop 之间存在一些协作关系;mach kernel 让线程陷入休眠;block 为 run loop 提供业务代码;线程则更是 run loop 不可缺少的环节。

Example1

此外还有一些平时使用得多的类跟库,也是依赖于 run loop 的:

  1. NSTimer: 每个 timer 都必须得添加到 run loop 中才能跑起来;
  2. UIEvent: 时间的产生,分发,到代码执行都是通过 run loop 在跑的;
  3. Autorelease: 本次 run loop 结束时会将本次 loop 内产生的所有 Autorelease 对象释放,事件大约在本次 run loop 休眠之后,下次 run loop 休眠之前的某个时间点。下面会提到;
  4. Selector: 要想在线程中执行 selector,线程中必须有一个正在运行的 run loop;
  5. NSDelayedPerforming: performSelector:AfterDelay: 之类的函数,实际上其内部会创建一个 timer 并添加到当前线程的 run loop 中。所以如果当前线程没有 run loop,则这个方法会失效。
  6. NSThreadPerformAddition: 跟 4. 同,另外实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。(by Ibireme)
  7. 界面更新相关: 改变了 UI 的 frame 或者是 UIView/CALayer 的层次等等更新了界面之后,会在 run loop 的 observer 中执行实际的绘制和调整,下面会提到;
  8. dispatch_get_main_queue: block 会在主线程的 run loop 中得到执行,下详;
  9. NSURLConnection: delegate 和网络回来的数据都是在 run loop 中跑的,下详;
Example2

看看一个 sample 样例的调用堆栈:

start 是 dyld 干的,将程序调起来,然后是 main 函数,调用 UIApplicationMain 并返回。接着 Graphics Services 是处理硬件输入的,比如点击,所有的 UI 事件都是由它发出来的。接下来是 run loop ,最后是 UI 事件。


主线程中几乎所有函数都是从以下六个之一的函数调起的:


__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__


长得比较丑陋,当然,这么长串的名字是为了在调用栈里面自解释。上面的函数都是 “Call Out”,通俗来讲就是调出,往上层调用。

RunLoop 机制


CFRunLoopSource

source 是 run loop 的数据源抽象类(id ),run loop 中存在两个 version 的 source:

  1. source0,处理 APP 内部事件,APP 自己负责管理(触发),例如 touch 事件,UIEvent,CFSocket。source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  2. source1,由 run loop 和内核管理,Mach port 驱动,例如 CFMachPort,CFMessagePort。关于 port:给某个进程发消息可以发到某个 port 上,如果进程监听这个 port,就可以收到这个消息。注意,是进程。一个 app 就是一个进程。

source version 0 的内部结构


source 结构内部有一个联合体,version0 中的结构中,成员主要都是各种函数指针,这些都是 run loop 需要调用的方法。如果自己实现一个 source 的话需要一个一个填进去。重要的方法是最后一个 perform 方法,里面具体进行业务处理(此处从刚才Button的堆栈中也能体现出来)。

CFRunLoopObserver

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};


kCFRunLoopEntry 开始进入 run loop 了;使用 Observer 肯定会需要用到上面这个枚举,run loop 利用他们来告知 observer 目前自身的状态:使用 Observer 肯定会需要用到上面这个枚举,run loop 利用他们来告知 observer 目前自身的状态:

  1. kCFRunLoopEntry 开始进入 run loop 了;
  2. kCFRunLoopBeforeTimers 要执行 timer 了;
  3. kCFRunLoopBeforeSources 要执行 source 了;
  4. kCFRunLoopBeforeWaiting 将要睡眠了;
  5. kCFRunLoopAfterWaiting run loop 被唤醒了;
  6. kCFRunLoopExit run loop 退出了;

框架中的很多机制都是由 observer 来触发的,例如 CAAnimation(BeforeWaiting或者AfterWaiting时、汇集整个loop的Animation一起执行)。可以看下面关于界面更新的内容。


上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。


另外再看看 Observer 跟 Autorelease Pool (自动释放池)之间的关系:


RunLoop 与 线程之间的关系:

CFRunLoop 与 Thread 之间是一一对应的,但不是说一个线程只能起一个 run loop,可以多个嵌套。RunLoop 不能直接创建,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:


/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}


线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)、子线程中默认是没有RunLoop的。

RunLoop Mode

run loop 必须在某种模式下来跑,系统预定义了几种模式。它们并不是一个 filter 的作用。mode 其实是一个 “树枝节点” ,Source、Observer以及Timer的几个节点实际上是在 mode 里面的。mode 对他们的存取方式如下:


run loop 在同一个时间段只能在一种特定的 mode 下 run,如果需要更换 mode 的话,需要先停止(应该是退出?)当前 loop,然后重新启动新 loop。mode 是 iOS App 滑动顺畅的关键。有以下几种 mode:

  1. NSDefaultRunLoopMode:默认的状态,也是空闲的状态——对 APP 进行除滑动外其余操作或者无操作时,main run loop 就会处于这个 mode;
  2. UITrackingRunLoopMode:滑动 ScrollView 时会切换到这个 mode;
  3. UIInitializationRunLoopMode:私有,在 APP 启动时会处于这个 mode,启动后 切到 Default进行待机;
  4. NSRunLoopCommonModes:默认情况下包含 1 与 2 两种 mode。也可以自己定义Mode添加到其中(基本不会出现);


经典问题,UITrackingRunLoopMode 与 Timer:


[NSTimer scheduledTimerWithTimeInterval:1.0
                                 target:self
                               selector:@selector(timerTick:)
                               userInfo:nil
                                repeats:YES];


在主线程中调用上面的方法时, timer 默认被添加到 NSDefaultRunLoopMode 中,如果 scrollView 发生滑动,main run loop 会切换到 UITrackingRunLoopMode 下,于是 timer 便不会工作。如果要解决这个问题,可以将 timer 添加到 NSRunLoopCommonModes 中:

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
                                         target:self
                                       selector:@selector(timerTick:)
                                       userInfo:nil
                                        repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];



另在再来看看 RunLoopMode 切换时的调用堆栈:


开始滑动时,run loop 停止,然后利用 pushRunLoopMode 将 run loop 切换到 tracking mode 下;滑动停止,利用 popRunLoopMode 将 run loop 恢复回原来的模式(RunLoop始终是一个)。


CFRunLoop对外暴露的管理 Mode 接口只有下面2个:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);


当你调用 CFRunLoopRunInMode() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。


Mode 暴露的管理 mode item 的接口有下面几个:


CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

RunLoop Timer

NSTimer 是对 CFRunLoopTimer 的上层封装(在上层调用的是内核MKTimer)。包括 performSelector:afterDelay: 里面使用的也是 RunLoopTimer。CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

此外、RunLoop的Timer与GCD的Timer均为独立运行。

RunLoop 与 GCD 的关系


GCD 中 dispatch 到 main queue 的 block 是被分发到 main run loop 中执行的。这是由于 GCD 中的主线程跟 run loop 中的主线程是同一个。


假如使用 GCD 中的 dispatch_after,当时间到了之后,dispatch_after 才会将 block 放到 run loop 中去执行。

RunLoop 的挂起和唤醒


上面的 mach_msg 跟 mach_msg_trap 是指定某个 mach_port 然后发给内核的,trap 就是一个等待的消息,表示等待被唤醒,于是 run loop 便会暂停而被挂起。


挂起与唤醒过程:

  • 在 run loop 进入等待前,先要指定一个用于唤醒的 mach_port
  • 然后调用 mach_msg 监听唤醒端口。被唤醒前,系统内核将这个线程挂起,停留在 mach_msg_trap 状态
  • 由另一个线程(或者另一个进程中的某个线程)向内核发送这个端口的 msg,trap 状态被唤醒,run loop 继续还是处理任务
RunLoop 迭代执行顺序

RunLoop 迭代执行顺序伪代码

  • 第一行设置过期时间,这是通过 GCD 的 timer 来监测的;
  • 通知 observer 相关 run loop 状态;
  • 执行 block,执行添加到 run loop 中的 source0;
  • 向 GCD 查询是否有需要分派到主线程的任务;
  • 进入休眠,通知 observer 即将进入休眠了;
  • SleepAndWaitForWakingUpPorts() 让线程进入休眠,等待消息来唤醒,即上面提到的 mach_msg_trap 状态;
  • 当消息来了,于是 wakeUpPort 得到返回值,根据返回值来执行业务处理;


根据苹果的官方文档的描述,执行流程如下:

这里是 ibireme 的另一份伪代码:


/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// · 一个基于 port 的Source 的事件。
            /// · 一个 Timer 到时间了
            /// · RunLoop 自身的超时时间到了
            /// · 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到消息,处理消息。
            handle_msg:
 
            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}


RunLoop 实践
AFNetWorking


注意这行代码:


[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];


networkRequestThread 创建一个单例线程,线程跑起来之后先去跑 networkRequestThreadEntryPoint:,然后在这个函数中创建这个线程的 run loop。新建完的 runLoop 如果没有事件处理的话就会直接退出了,所以让它随便监听一个 port,让它等待,一直活着。所以这个线程就可以一直驻留。这是一个创建常驻服务线程的好方法。



从调用堆栈可以看到,线程执行入口函数创建了 run loop 之后,停在 mach_msg_trap 状态,线程进入休眠。

TableView 延时加载图片

网络图片下载完成之后去设置 cell 中的 imageView,会导致主线程“卡一下”。解决这个问题的最简单的方法,就是将设置图片的代码放到 NSDefaultRunLoopMode 中去运行:


UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
                           withObject:downloadedImage
                           afterDelay:0
                              inModes:@[NSDefaultRunLoopMode]];


于是在滑动时不会设置 imageView,直到滑动停止 mode 切换为 defaultMode 才会执行设置 image 的代码。

让 Crash 掉的 APP 回光返照


//取当前 run loop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();

//取 run loop 所有运行的 mode
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
while (1) {
    for (NSString *mode in allModes) {
    	//在每个 mode 中轮流运行至少 0.001 秒
        CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
    }
}


对于因为接收到 crash 的 signal 而挂掉的程序,可以在接收到 crash 的信号之后重新起一个 run loop 然后跑起来。但是这个并不能保证 app 能像原来一样能正常运行,只能是利用它来在奄奄一息的状态下弹出一些友好的错误信息。

Async Test Case

原来写 test case 时最大的问题就是,它不支持异步。当时的一种解决方法是”每0.0001秒验证”:


- (void)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeout];
    do {
        CFTimeInterval quantum = 0.0001;
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, quantum, false);
    } while([timeoutDate timeIntervalSinceNow] > 0.0 && !block());
}


这是原来的方案,后来更新了,换成了 run loop sleep 前验证:

- (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
{
    __block Boolean fulfilled = NO;
    void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
    ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        fulfilled = block();
        if (fulfilled) {
            CFRunLoopStop(CFRunLoopGetCurrent());
        }
    };
    
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    // Run!
    CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);
    
    CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    CFRelease(observer);
    
    return fulfilled;}

你可能感兴趣的:(优化/效率,Ios基础)