参考(抄袭)资料

  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.0target:selfselector:@selector(timerTick:)userInfo:nilrepeats:YES];

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

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0target:selfselector:@selector(timerTick:)userInfo:nilrepeats: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找到对应modeCFRunLoopModeRef 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:downloadedImageafterDelay:0inModes:@[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;<span style="font-family: Arial, Helvetica, sans-serif;">}</span>

RunLoop解析(视频+原版文字)相关推荐

  1. Video Analysis 相关领域解读之Video Captioning(视频to文字描述)

    之前两次分别介绍了video analysis中的action recognition 以及 temporal action detection 这两个领域.这两个领域算是对视频mid-level的理 ...

  2. 如何才能从英语视频转换文字呢?

    现在越来越多的小伙伴们通过看英语视频来学习外语,但是不是所有的视频都是有文字的,出现这样的情况挺麻烦的,毕竟会影响到学习,那么小伙伴如何才能从英语视频转换文字呢? 第一步:打开转易侠语音转文字,单击& ...

  3. 视频转文字怎么操作?这三种转换方法你该学会

    如今短视频让各种知识传播变得生动形象,但是视频学习对于后期的整理复习不是很便捷,现在教大家一种好用的视频知识整理方法,那就是视频转文字,可以将视频内容轻松转换为文字形式.那么就有人问了,怎样转换才更简 ...

  4. 怎么视频转文字?分享3个视频转文字方法

    前几天朋友和我说他准备考一个职业证书,于是上网找了一些相关的视频去看.但是这些学习视频相对枯燥,再加上没有字幕,导致他看了没一会就开始犯困,没记下多少笔记,因此来求助我有没有什么好的解决办法.于是我马 ...

  5. 这篇文章来告诉你几个实用的视频转文字的方法

    相信大家在闲暇之余,都会通过一些网课来提高自己的知识本领吧!有的时候在上网课的过程中,会感觉自己做笔记的速度赶不上老师的进度,重复观看又比较麻烦,这时我们就可以借助一些视频转换软件来将视频转换成文字, ...

  6. 免费视频转文字-音频转文字软件:网易见外工作台, Speechnotes, autosub, Speech to Text, 百度语音识别

    文章目录 网易见外工作台(推荐) Chrome插件 Speechnotes autosub 百度语音识别API IBM的Speech to Text(不推荐) 此文首发于我的Jekyll博客:zhan ...

  7. 1分钟教会你如何视频转文字,简单又实用

    平常我们在刷短视频或是追剧时,偶尔会看到一些戳中心巴的台词文案,就想将其摘抄下来,但在这过程中就需要不断的中途暂停或重复播放,再进行手动输出,十分麻烦. 其实有一个较为不错的解决方法,只要借助工具,将 ...

  8. mac 视频转文字工具

    mac 视频转文字工具 参考博文链接:https://www.zhihu.com/question/24717723 brew install ffmpeg 在python2.7的环境下:pip in ...

  9. 有没有什么软件可以把视频转文字?看看这些转换软件

    现在的科学技术发展得很快,我们日常的学习也不再局限于课堂或书本里面,有时候我们可以在网上观看视频的讲解.那么大家在上网课的时候,不知道会不会遇到一些讲课速度很快的老师呢,我就遇到过,经常在做笔记的时候 ...

最新文章

  1. restful可以转发么_什么是RESTFUL?REST的请求方法有哪些,有什么区别?
  2. android新闻app_如何利用 Python 爬虫实现给微信群发新闻早报?
  3. Spring Boot 最流行的 16 条最佳实践!
  4. php基本语法 格式,PHP 基本语法格式
  5. linux6.5能安装的firefox,Centos6.5安装firefox
  6. qt的一些参数配置 win和linux
  7. 如何为Swift进行宏定义
  8. excel处理几十万行数据_Python处理Excel数据
  9. 关闭笔记本电脑计算机键盘,笔记本电脑键盘怎么关_笔记本电脑键盘关闭步骤-win7之家...
  10. 31省份RD经费内部支出、全时当量、专利数、技术市场成交额(1997-2019年)
  11. linux一台服务器上装两个mysql数据库
  12. easyui datagrid 可编辑单元格 显示 clear icon 和 放大镜图标
  13. z-buffer算法
  14. mysql aced是什么_memcached编译安装及缓存mysql测试
  15. windows 下用开源流媒体压力测试工具 rtmpstress 测试RTMP媒体服务器负载性能
  16. 【Android界面实现】Starting an Activity(Activity生命周期金字塔模型)
  17. Xshell的Sessions存放目录
  18. linux经验总结(持续更新)
  19. 卷积神经网络(CNN)相关知识以及数学推导
  20. 【阿里聚安全·安全周刊】战斗民族黑客入侵德国政府|“猫脸识别”门禁

热门文章

  1. HTML自学笔记-1(进入篇)
  2. Android 实战项目汇总
  3. navigation Bar、toolBar、tabbar 区别
  4. 《白帽子讲Web安全》memo0
  5. solr mysql dih_Solr结构化数据导入DIH
  6. 堆和栈的区别(内存和数据结构)
  7. 华为harmonyos2.0哪里下载,华为HarmonyOS最新官方版-华为HarmonyOS2.0最新下载地址-游侠软件下载...
  8. HG30-3a型数字式多功能校准仪
  9. PHP学习----换行符
  10. 盘点linux现状未来发展,盘点Linux现状及未来发展