什么是RunLoop?
所以什么是RunLoop
【跑圈】可太贴切了
-
之所以iOS App能持续响应,保持程序运行状态,在于其有一个事件循环(Event Loop)
-
事件循环机制,即线程能随时响应并处理事件的机制,这种机制要求线程不能退出,而且需要高效的完成事件调度与处理。
-
事件循环这种机制就叫RunLoop
-
==RunLoop实际上是一个对象==,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如触摸事件、UI刷新时间、定时器时间、Selector事件等等),从而保持程序的持续运行,而且在没有事件处理的时候,会进入睡眠状态,从而节省CPU资源,提高程序性能。
默认情况下主线程的RunLoop原理
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
复制代码
其中的UIApplicationMain
函数内部帮我们开启了主线程的RunLoop。
UIApplicationMain
内部拥有一个无限循环的代码。
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
复制代码
程序会一直在do-while循环中执行
看一下苹果官方给出的RunLoop模型图
RunLoop
就是线程中的一个循环,RunLoop在循环中不断检测,通过Input sources(输入源)
和Timer sources(定时源)
两种来源等待接受消息,然后对接收到的事件通知线程进行处理,并在没有事件的时候进行休息。
RunLoop对象
RunLoop对象的获取
- 介绍一下RunLoop对象
Fundation框架(基于CFRunLoopRef的封装) NSRunLoop对象
-
NSRunLoop
是基于CFRunLoopRef
的封装,提供了面向对象的API,但是这些API不是线程安全的
[NSRunLoop currentRunLoop];//获得当前RunLoop对象
[NSRunLoop mainRunLoop];//获得主线程的RunLoop对象
复制代码
CoreFoundation CFRunLoopRef对象
-
CFRunLoopRef
是在CoreFoundation
框架内的,其提供了纯C语言函数的API,所有这些API都是线程安全
CFRunLoopGetCurrent();//获得当前线程的RunLoop对象
CFRunLoopGetMain();//获得主线程的RunLoop对象
复制代码
那么你对应的两种方式其实就是
//Foundation
NSRunLoop *runLoop1 = [NSRunLoop currentRunLoop];
NSRunLoop *mainRunLoop1 = [NSRunLoop mainRunLoop];
//Core Foundation
CFRunLoopRef runLoop2 = CFRunLoopGetCurrent();
CFRunLoopRef mainRunLoop2 = CFRunLoopGetMain();
复制代码
看一下这两个函数的具体实现
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
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;
}
复制代码
其都调用了_CFRunLoopGet0
这个函数,等会再看吧
CFRunLoopRef源码部分(引入线程相关)
看一下CFRunLoopRef
的源码
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; 【通过该函数CFRunLoopWakeUp内核向该端口发送消息可以唤醒runloop】
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; 【RunLoop对应的线程】
uint32_t _winthread;
CFMutableSetRef _commonModes; 【存储的是字符串,记录所有标记为common的mode】
CFMutableSetRef _commonModeItems;【存储所有commonMode的item(source、timer、observer)】
CFRunLoopModeRef _currentMode;【当前运行的mode】
CFMutableSetRef _modes;【存储的是CFRunLoopModeRef】
struct _block_item *_blocks_head;【do blocks时用到】
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
复制代码
- 除了记录一些属性外,重点是三个成员变量
pthread_t _pthread;【RunLoop对应的线程】
CFRunLoopModeRef _currentMode;【当前运行的mode】
CFMutableSetRef _modes;【存储的是CFRunLoopModeRef】
复制代码
引入下面的问题了
RunLoop和线程
先看一下_CFRunLoopGet0
这个函数是怎么实现的,和RunLoop
和线程有什么关系
//全局的Dictionary,key是pthread_t,value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问__CFRunLoops的锁
static CFSpinLock_t loopsLock = CFSpinLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//t==0是始终有效的“主线程”的同义词
//获取pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
//pthread为空时,获取主线程
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
//第一次进入时,创建一个临时字典dict
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//根据传入的主线程获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//保存主线程,将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
//释放dict
CFRelease(dict);
}
//释放mainRunLoop
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建
//从全局字典里获取对应的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//如果取不到,就创建一个新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
//创建好之后,以线程为key,runLoop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runLoop
if (!loop) {
//把newLoop存入字典__CFRunLoops,key是线程t
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)) {
//注册一个回调,当线程销毁时,销毁对应的RunLoop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
复制代码
通过这段源码我们可以看到
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取线程的RunLoop时创建,RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
RunLoop的相关类
与RunLoop相关的类有5个
CFRunLoopRef
代表了RunLoop的对象CFRunLoopModeRef
RunLoop的运行模式CFRunLoopSourceRef
就是RunLoop模型图中提到的输入源(事件源)CFRunLoopTimerRef
定时源CFRunLoopObserverRef
观察者,监听RunLoop状态的改变
- 一个
RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source/Timer/Observer
2. 每次调用RunLoop的主函数时,只能指定其中的一个Mode,这个Mode被称作CurrentMode
3. 如果需要切换Mode
,只能退出Loop
,再重新指定一个Mode
进入。这样做主要是为了分隔开不同组的Source/Timer/Observer
,让其互不影响
4. 如果一个mode中一个Sourcr/Timer/Observer
都没有,则RunLoop会直接退出,不进入循环
RunLoop中相关类的实现
一个RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source/Timer/Observer
这句话其实就是5个相关类的关系
CFRunLoopModeRef
还是那句话:CFRunLoopModeRef代表RunLoop的运行模式
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名称,运行模式是通过名称来识别的
Boolean _stopped; //mode是否被终止
char _padding[3];
//整个结构体最核心的部分
------------------------------------------
CFMutableSetRef _sources0;//Sources0
CFMutableSetRef _sources1;//Sources1
CFMutableArrayRef _observers;//观察者
CFMutableArrayRef _timers;//定时器
------------------------------------------
CFMutableDictionaryRef _portToV1SourceMap;//字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
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 */
};
复制代码
- 一个
CFRunLoopModeRef
对象有一个name,若干source0
,source1
,timer
,observer
和port
,可以看出来事件都是由mode
在管理,而RunLoop
管理着Mode
五种运行模式
系统默认注册的五个Mode
kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode
:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响UIInitializationRunLoopMode
: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultModeGSEventReceiveRunLoopMode:
接受系统事件的内部 Mode,通常用不到kCFRunLoopCommonModes
: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
其中kCFRunLoopDefaultMode
、UITrackingRunLoopMode
、kCFRunLoopCommonModes
是我们开发中需要用到的模式
CommonModes
在RunLoop对象中,前面有一个有一个叫CommonModes
的概念
//简化版本
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode;//当前运行的mode
CFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象,不同mode类型,它的mode名字不同
};
复制代码
- 一个Mode可以将自己标记为Common属性,通过将其ModeName添加到RunLoop的commonModes中。
- 每当RunLoop的内容发生变化时,RunLoop都会将
_commonModeItems
里的Source/Observer/Timer
同步到具有Common标记的所有Mode里。其底层原理
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
__CFRunLoopLock(rl);
if (!CFSetContainsValue(rl->_commonModes, modeName)) {
//获取所有的_commonModeItems
CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
//获取所有的_commonModes
CFSetAddValue(rl->_commonModes, modeName);
if (NULL != set) {
CFTypeRef context[2] = {rl, modeName};
//将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode
CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
CFRelease(set);
}
} else {
}
__CFRunLoopUnlock(rl);
}
复制代码
CFRunLoop对外暴露的管理Mode
接口只有下面两个
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
复制代码
什么是Mode Item?
Mode到底包含哪些类型的元素?
RunLoop
需要处理的消息,包括time以及source消息,他们都属于Mode item
RunLoop
也可以被监听,被监听的对象是observer
对象,也属于Mode item
- 所有的
mode item
都可以被添加到Mode中,Mode中可以包含多个mode item
,一个item
也可以被加入多个mode。但一个item
被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop
会退出,不进入循环
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
只能增加不能删除
苹果公开提供了两个Mode
kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
)和UITrackingRunLoopMode
,你可以用这两个Mode Name
来操作其对应的Mode- 同时苹果还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。
CFRunLoopSourceRef
事件产生的地方
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; //执行顺序
CFMutableBagRef _runLoops;//包含多个RunLoop
//版本
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
复制代码
两个版本 Source0
和Source1
Source0
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source)
,将这个Source
标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop
,让其处理这个事件Source1
包含了一个mach_port
和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop
的线程。
随便写个button的点击事件 通过thread backtrace查看调用栈
我们可以看到点击事件是怎样来的
- 首先程序启动,调用18行的main函数,main函数调用17行UIApplicationMain函数,然后一直往上调用函数,一直到第0行的点击事件
- 同时我们可以看到上面调用了
Source0
,也就是说我们点击事件属于Source0函数的,点击事件就是在Source0中进行处理的。
- 而至于Source1,则是用来接受、分发系统事件,然后再分发到Source0中处理
CFRunLoopTimerRef
基于时间的触发器
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;//包含timer的mode集合
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; //timer的回调
CFRunLoopTimerContext _context; //上下文对象
};
复制代码
CFRunLoopTimerRef
是基于时间的触发器,它和NSTimer
可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
会被唤醒以执行那个回调
对于NSTimer
scheduledTimerWithTimeInterval
和RunLoop的关系
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
复制代码
会自动加入NSDefaultRunLoopMode
两者一样
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
复制代码
定时器滑动时不准确
我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况
举个例子
新建个tableView
将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
正常情况下:一秒钟打印一次
当我们对tableView进行拖拽时
计时器停止了
这个原因就是
- 当我们不做任何操作的时候,RunLoop处于
NSDefaultRunLoopMode
下 - 当我们进行拖拽时,RunLoop就结束
NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式下没有添加NSTimer
,所以我们的NSTimer
就不工作了 - 当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回
NSDefaultRunLoopMode
模式,所以NSTimer
就又开始正常工作了
那么对应这个问题我们应该怎么解决呢?
我们不能在这两种模式下让NSTimer都正常工作吗?
使用CommonModes就可以解决了(这也就解决了之前的问题 Common到底能干哈)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
复制代码
此时我们进行拖拽就不会有任何问题
CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;//监听的RunLoop
CFIndex _rlCount;//添加该Observer的RunLoop对象个数
CFOptionFlags _activities; /* immutable */
CFIndex _order;//同时间最多只能监听一个
CFRunLoopObserverCallBack _callout;//监听的回调
CFRunLoopObserverContext _context;//上下文用于内存管理
};
//观测的时间点有一下几个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
复制代码
RunLoop的内部逻辑
根据文档中的描述 RunLoop的内部逻辑如下
精简后的 __CFRunLoopRun函数,保留了主要代码
看一下具体实现
【用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就是这样的一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()
时,线程就会一直停留在这个循环里,知道超时或者被手动调用,该函数才会返回
RunLoop回调
- 当App启动时,系统会默认注册五个Mode【就是上面那五个】
- 当RunLoop进行回调时,一般都是通过一个很长的函数调用出去(call out),当你在你的代码中断点调试时,通常能在调用栈上看到这些函数。这就是RunLoop的流程:
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
复制代码
实例测试
NSTimer的使用
上面有讲
ImageView延迟显示
当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能出现卡顿的情况。
如何解决这个问题?
我们应该推迟图片的实现,也就是ImageView推迟显示图片。当我们滑动时不要加载图片, 拖动结束在显示
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName.png"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
复制代码
用户点击屏幕,在主线程中,三秒之后显示图片,但是当用户点击屏幕之后,如果此时用户又开始滚动textview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。
这是因为限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滚动textview的时候,程序运行在tracking模式下面,所以方法setImage不会执行。
常驻线程
开发应用程序的过程中,如果后台操作十分频繁,比如后台播放音乐、下载文件等等,我们希望这条线程永远常驻内存
我们可以添加一条用于常驻内存的强引用子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop
@property (nonatomic, strong) NSThread *thread;
复制代码
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
[self.thread start];
}
- (void)run1 {
NSLog(@"----run1-----");
/*如果不加这句,会发现runloop创建出来就挂了,因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡。
下面的方法给runloop添加一个NSport,就是添加一个事件源,也可以添加一个定时器,或者observer,让runloop不会挂掉*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 方法1 ,2,3实现的效果相同,让runloop无限期运行下去
// 方法2
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// 方法3
// [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
我们同时在我们自己新建立的这个线程中写一下touchesBegan这个方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void) run2 {
NSLog(@"----run2------");
}
复制代码
我们必须保证线程不消亡,才可以在后台接受时间处理,所以如果没有实现添加NSPort或者NSTimer,会发现执行完run方法,线程就会消亡,后续再执行touchbegan方法无效。
实现了上面三个方法之一,就可以发现执行完了run方法,这个时候再点击屏幕,可以不断执行test方法,因为线程self.thread一直常驻后台,等待事件加入其中,然后执行。