RunLoop
文章目录
- RunLoop
- RunLoop简介
- RunLoop基本使用
- Runloop伪代码
- Runloop模型图
- Runloop对象
- Runloop对象的获取
- _CFRunLoopGet0方法
- RunLoop的相关类
- RunLoop相关类的实现
- CFRunLoopRef
- CFRunLoopModeRef
- 五种运行模式
- CommonModes
- 什么是Mode Item?Mode到底包含哪些类型的元素?
- CFRunLoopSourceRef
- Source0
- Source1
- CFRunLoopTimerRef
- 对于NSTimer:
- NSTimer在滑动时停止工作的问题
- CFRunLoopObserverRef
- RunLoop的六种状态
- RunLoop实现逻辑
- __CFRunLoopRun源码实现
- RunLoop回调(流程)
- RunLoop启动方法
- RunLoop关闭方法
- ImageView延迟显示
RunLoop简介
简单的说 RunLoop是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
在App运行的过程中,主线程的Runloop保证了主线程不被销毁从而保证应用的存活,从而能实时接收到用户的响应事件,能够触发定时事件。如果没有Runloop的话,程序执行完代码就会立马return。
RunLoop基本使用
- 保持程序的持续运行
- 处理app中各种事件
- 节省CPU资源,提高程序性能:该做事时做事,该休眠时休眠。并且休眠时不占用CPU
Runloop伪代码
int main(int argc, char *argv[]) {
@atuoreleasepool {
int retVal = 0;
do {
// 睡眠中等待消息
int message = sleep_and_wait();
// 处理消息
retVal = process_message(message);
} while (0 == retVal);
return 0;
}
}
Runloop会一直在do-while循环中执行,这也就是我们写的程序不会在执行完一次代码之后就退出的原因了。
Runloop模型图
官方给的图:
Runloop对象
runloop实际上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后,就会处于这个函数内部的循环中,直到循环结束,函数返回。
Runloop对象的获取
Runloop对象主要有两种获取方式:
// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
NSRunloop
类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef
的封装,提供了面向对象的API,但是这些API不是线程安全的。
// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象
CFRunLoopRef
类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全。
CoreFoundation框架中这两个函数的具体实现:
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
这个方法
_CFRunLoopGet0方法
//全局的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);
//如果这个__CFRunLoops字典不存在,即程序刚开始运行
if (!__CFRunLoops) {
__CFSpinUnlock(&loopsLock);
//第一次进入时,创建一个临时字典dict
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
//根据传入的主线程,获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//保存主线程的Runloop,将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
//释放dict,因为我们已经将dict的内存保存了,该临时变量也就没用了,要及时的释放掉
CFRelease(dict);
}
//释放mainRunLoop,刚才用于获取主线程的Runloop,已经保存了,就可以释放了
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建
//从全局字典里获取对应的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
//如果找不到对应的Runloop
if (!loop) {
//创建一个该线程的Runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
//再次在__CFRunLoops中查找该线程的Runloop
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
//如果在字典中还是找不到该线程的Runloop
if (!loop) {
//把刚创建的该线程的newLoop存入字典__CFRunLoops,key是线程t
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
//并且让loop指向刚才创建的Runloop
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
//loop已经指向这个newLoop了,他也就可以释放了
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);
}
}
//返回该线程对应的Runloop
return loop;
}
流程大概是这个样子:
从这部分源码我们可以得知:
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存一个全局的Dictionary里,线程作为key,RunLoop作为value
- 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取线程的RunLoop时创建,RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
- RunLoop会在线程结束时销毁
RunLoop的相关类
与RunLoop相关的类有5个
CFRunLoopRef
代表了RunLoop的对象CFRunLoopModeRef
RunLoop的运行模式CFRunLoopSourceRef
就是RunLoop模型图中提到的输入源(事件源)CFRunLoopTimerRef
定时源CFRunLoopObserverRef
观察者,监听RunLoop状态的改变
1.一个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相关类的实现
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;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
Runloop中除了记录了一些属性外,重点还是一下几个:
pthread_t _pthread; // runloop对应的线程
CFMutableSetRef _commonModes; // 存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems; // 存储所有common标记的mode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; // 当前运行的mode
CFMutableSetRef _modes; // 装着一堆CFRunLoopModeRef类型,runloop中的所有模式
RunLoop中主要的变量就是_pthread
、_currentMode
、_modes
,_currentMode
主要就是为了在_modes中找当前对应的mode的item,然后发送消息。而_commonModes
和_commonModeItems
完全就是为了common标记mode准备的,如果我们选择的mode是commonMode,那么就不用在_modes
中找每个mode对应的item了,因为被标记的mode的item都在_commonModeItems
中,直接给他里边的所有item发消息就完了。
CFRunLoopModeRef
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实际上是Source,Timer 和 Observer 的集合,不同的Mode把不同组的Source、timer、Observer隔绝开来。runloop在某一时刻只能运行在一个mode下,处理这一个mode中的source、timer、observer。
五种运行模式
系统默认注册的五个mode:
kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode
:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响UIInitializationRunLoopMode
:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultModeGSEventReceiveRunLoopMode
:接受系统事件的内部 Mode,通常用不到kCFRunLoopCommonModes
:并不是一种模式
只是一个标记,当mode标记为common时,将mode添加到runloop中的_commonModes
中。runloop中的_commonModes
实际上是一个Mode的集合,可使用CFRunLoopAddCommonMode()
将Mode放到_commonModes
中。每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems
里的同步到具有Common标记的所有的Mode里
CommonModes
在RunLoop对象中,前面有一个有一个叫CommonModes的概念,它记录了所有标记为common的mode:
//简化版本
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只能增加不能删除
CFRunLoopSourceRef
根据官方描述,CFRunLoopSourceRef
是input sources
的抽象。
CFRunLoopSource
分为Source0
和Source1
两个版本。
它的结构如下:
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态才会被处理
pthread_mutex_t _lock;
CFIndex _order; //执行顺序
CFMutableBagRef _runLoops;//包含多个RunLoop
//版本
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
可一通过共用体union看出,它有两个版本,Source0
和Source1
:
Source0
Source0
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source)
,将这个Source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)
来唤醒RunLoop,让其处理这个事件。
Source0
是App内部事件,由App自己管理的UIEvent、CFSocket都是source0
。当一个source0
事件准备执行时,必须要先把它标为signal状态,以下是source0
结构体:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
Source0 :非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。一般是APP内部的事件, 比如hitTest:withEvent的处理, performSelectors的事件。
Source1
Source1
包含了mach_port
和一个回调(函数指针),Source1
可以监听系统端口,通过内核和其他线程通信,接收、分发系统事件,他能主动唤醒RunLoop(由操作系统内核进行管理)
Source1在处理的时候会分发一些操作给Source0去处理。
source1结构体:
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
#if TARGET_OS_OSX || TARGET_OS_IPHONE
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
void * (*getPort)(void *info);
void (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;
Source1 :基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好, 比如屏幕点击, 网络数据的传输都会触发sourse1。
例:
一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event,Event先告诉source1(mach_port),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。
CFRunLoopTimerRef
CFRunLoopTimer
是基于时间的触发器,其包含一个时间长度、一个回调(函数指针)。当其加入runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行那个回调。
并且CFRunLoopTimer
和NSTimer
是toll-free bridged
(对象桥接),可以相互转换。其结构如下:
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; //上下文对象
};
对于NSTimer:
scheduledTimerWithTimeInterval
和RunLoop
的关系:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
系统会将NSTimer自动加入NSDefaultRunLoopMode
模式中,所以它就等同于下面代码:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSTimer在滑动时停止工作的问题
在之前写仿写知乎日报的时候,首页一个tableView里嵌套了一个scrollView,scrollView是一个自动轮播图,当我们拖动tableView的时候,scrollView也不动了,这个问题当时学长帮我解决的,一直不知道原因:
self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(timeOut:) userInfo:nil repeats:YES];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addTimer:self.timer forMode:NSRunLoopCommonModes];
我们来写个例子:
self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
self.scrollView.scrollEnabled = YES;
self.scrollView.pagingEnabled = YES;
self.scrollView.bounces = YES;
self.scrollView.alwaysBounceHorizontal = NO;
self.scrollView.alwaysBounceVertical = NO;
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.contentSize = CGSizeMake(900, 200);
self.scrollView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.scrollView];
static int count = 0;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
输出结果:
在5和6秒之间我们拖动,timer就停止了。
造成这种问题的原因就是:
- 当我们不做任何操作的时候,RunLoop处于
NSDefaultRunLoopMode
下 - 当我们进行拖拽时,RunLoop就结束
NSDefaultRunLoopMode
,切换到了UITrackingRunLoopMode
模式下,这个模式下没有添加该NSTimer以及其事件,所以我们的NSTimer就不工作了 - 当我们松开鼠标时候,RunLoop就结束
UITrackingRunLoopMode
模式,又切换回NSDefaultRunLoopMode
模式,所以NSTimer就又开始正常工作了
想要解决这个问题也很简单,我们直接让NSTimer在两种mode下都能工作就完了,这就用到我们之前不太清楚其用法的NSRunLoopCommonModes
了:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
当然你也可以把NSTimer分别加入到NSDefaultRunLoopMode
和UITrackingRunLoopMode
,这两种写法是相同的,因为系统的mode是默认在_commonModes中的:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
修改之后就会一直正常运行了。
CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者可以观察Runloop的各种状态,每个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;//上下文用于内存管理
};
RunLoop的六种状态
//观测的时间点有一下几个
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
};
这六种状态都可以被observer观察到,我们也可以利用这一方法写一些特殊事件,创建监听,监听RunLoop的状态变化:
// 创建observer
CFRunLoopObserverRef ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});
// 添加observer到runloop中
CFRunLoopAddObserver(CFRunLoopGetMain(), ob, kCFRunLoopCommonModes);
CFRelease(ob);
输出结果:
RunLoop是在不停的监听状态并做出反应
RunLoop实现逻辑
RunLoop的内部逻辑如下:
__CFRunLoopRun源码实现
精简后的__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);
//调用函数__CFRunLoopRun 进入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)回调,处理Source0
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;
}
//6.通知Observers:RunLoop的线程即将进入休眠
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);
//设置do-while之后的返回值
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启动时,系统会默认注册五个上面说过的5个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);
}
RunLoop启动方法
- 1.
run
,无条件 -
- 无条件地进入运行循环是最简单的选项,但也是最不理想的选择。无条件地运行runloop将线程放入永久循环,这使您无法控制运行循环本身。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。
- 2.
runUntilDate
, 设置时间限制 -
- 设置了超时时间,超过这个时间runloop结束,优于第一种
- 3.
runMode:beforeDate:
,在特定模式下 -
- 相对比较好的方式,可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的
-
- 实际上run方法的实现就是无限调用
runMode:beforeDate:
方法
- 实际上run方法的实现就是无限调用
-
runUntilDate:
也会重复调用runMode:beforeDate:
方法,区别在于它超时就不会再调用
RunLoop关闭方法
- 1.将运行循环配置为使用超时值运行。
- 2.手动停止。
这里需要注意,虽然删除runloop的输入源和定时器可能会导致运行循环的退出,但这并不是个可靠的方法,系统可能会添加输入源到runloop中,但在我们的代码中可能并不知道这些输入源,因此无法删除它们,导致无法退出runloop。
我们可以通过上述2、3方法来启动runloop,设置超时时间。但是如果需要对这个线程和它的RunLoop有最精确的控制,而并不是依赖超时机制,这时我们可以通过 CFRunLoopStop()
方法来手动结束一个 RunLoop。但是 CFRunLoopStop()
方法只会结束当前正在执行的这次runMode:beforeDate:
调用,而不会结束后续runloop的调用。
ImageView延迟显示
当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能出现卡顿的情况。
我们应该推迟图片的实现,也就是ImageView推迟显示图片。当我们滑动时不要加载图片, 拖动结束在显示:
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
用户点击屏幕,在主线程中,三秒之后显示图片,但是当用户点击屏幕之后,如果此时用户又开始滚动tableview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。
这是因为限定了方法setImage只能在NSDefaultRunLoopMode
模式下使用。而滚动tableview的时候,程序运行在tracking模式下面,所以方法setImage不会执行。