【iOS】--Runloop初学
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添加到上面这个全局变量的字典中,方便后面获取runloop时,可以直接在字典中查找。
大概流程如下图所示:
从这部分源码我们可以得知:
- 每条线程都有唯一的一个与之对应的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:
NSRunLoopCommonModes
// 一个伪模式,包含一个或多个其他运行循环模式。
NSString * const NSRunLoopCommonModes = @"kCFRunLoopCommonModes";
NSDefaultRunLoopMode
// 用于处理除连接对象之外的输入源的模式。
NSString * const NSDefaultRunLoopMode = @"kCFRunLoopDefaultMode";
NSEventTrackingRunLoopMode
// 在模态跟踪事件(例如鼠标拖动)时设置的模式。
NSString * const NSEventTrackingRunLoopMode = @"kCFRunLoopEventTrackingMode";
NSModalPanelRunLoopMode
// 等待模态面板(例如保存或打开面板)的输入时设置的模式。
NSString * const NSModalPanelRunLoopMode = @"kCFRunLoopModalPanelMode";
UITrackingRunLoopMode
// 在进行控件跟踪时设置的模式。
NSString * const UITrackingRunLoopMode = @"kCFRunLoopUITrackingMode";
还有UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode。
- 常用的3个Mode:
- NSDefaultRunLoopMode, 默认的模式, 有事件响应的时候, 会阻塞旧事件。
- NSRunLoopCommonModes, 普通模式, 不会影响任何事件。
- UITrackingRunLoopMode, 只能是有事件的时候才会响应的模式。
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, ...);
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName):
该函数用于向指定的 RunLoop 添加一个常用运行模式(Common Mode)。运行模式是用于标识一组事件源和定时器的集合,以确定哪些事件应该被 RunLoop 处理。通过添加常用运行模式,可以将事件源和定时器同时添加到多个运行模式中,以确保在这些运行模式下都能被正确处理。
CFRunLoopRunInMode(CFStringRef modeName, …):
该函数用于运行指定的运行模式下的 RunLoop。它会使当前线程进入一个循环,不断处理该运行模式下的事件,直到满足退出条件。
这两个函数结合使用可以实现对 RunLoop 的管理和调度。CFRunLoopAddCommonMode 用于添加常用的运行模式,将事件源和定时器添加到多个运行模式中,以确保它们都能被正确处理。而 CFRunLoopRunInMode 则用于运行指定的运行模式下的 RunLoop,使当前线程进入循环,并处理该运行模式下的事件。
什么是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。
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;
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;
两者关系与区别:
Source0:
- Source0是非基于Port的事件源,用于处理应用程序内部生成的事件。
- 它通常用于响应和处理一些特定的应用程序级事件,如触摸事件、用户交互事件等。
- Source0事件通过调用相应的事件处理方法来执行特定的操作,比如调用视图的touchesBegan:withEvent:方法来处理触摸事件。
- Source0事件是由应用程序自身生成和处理的,不涉及进程间通信。
Source1:
- Source1是基于Port的事件源,用于处理与其他进程或内核之间的交互。
- 它用于处理一些系统级事件,如网络数据到达、定时器事件、文件描述符的可读或可写状态等。
- Source1事件通常由系统或其他进程发送给当前应用程序,需要通过RunLoop进行接收和处理。
- Source1事件的处理涉及与其他进程或内核的通信,比如进行网络数据的读写、处理文件描述符的状态变化等。
例:
一个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在滑动时停止工作的问题
在写定时无限轮播图时,需要添加定时器,但是在手动滑动或者按住屏幕时,定时器就会被打断,从而不会继续轮播。
我们来写个例子:
self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(50, 80, 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);
}];
输出结果:
可以看出在第四秒和第五秒之间我们拖动,timer就停止了。
造成这种问题的原因就是:
- 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下
- 当我们进行拖拽时,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加该NSTimer以及其事件,所以我们的NSTimer就不工作了
- 当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了
要想解决这个问题,我们直接让NSTimer在两种mode下都能工作就完了,
[[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与现线程的关系
- 一条线程对应一个RunLoop对象, 每条线程都有唯一一个与之对应的RunLoop对象。
- RunLoop并不保证线程安全. 我们只能在当前线程内部操作当前线程的RunLoop对象, 而不能在当前线程内部去操作其他线程的RunLoop对象方法。
- RunLoop对象在第一次获取RunLoop时创建, 销毁则是在线程结束的时候。
- 主线程的RunLoop对象系统自动帮助我们创建好了(UIApplicationMain函数), 子线程的RunLoop对象需要我们主动创建和维护。
RunLoop启动方法
- 1.run,无条件
- 无条件地进入运行循环是最简单的选项,但也是最不理想的选择。无条件地运行runloop将线程放入永久循环,这使您无法控制运行循环本身。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。
- 2.runUntilDate, 设置时间限制
- 设置了超时时间,超过这个时间runloop结束,优于第一种
- 3.runMode:beforeDate:,在特定模式下
- 相对比较好的方式,可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的
- 实际上run方法的实现就是无限调用runMode:beforeDate:方法
- 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不会执行。