【原英】https://developpaper.com/ios-...
在知道Runloop之前
一般来说,一个线程只能执行一个任务,执行完就会退出。当我们需要让线程可以随时处理事件而不退出线程,我们就要使用Runloop
。
它内部是一个do while循环,不断地处理各种任务,保证线程能够的连续运行。
Runloop的目的
保证线程中有任务时可以执行线程工作。当线程中没有任务时,让线程休眠,提高程序性,节省资源。
Runloop的作用
简述 | 详述 |
---|---|
保持程序连续运行 | 应用程序启动后,将启动主线程。当主线程启动时,会启动主线程对应该的runloop 。Runloop 可以保证线程不会被破坏。如果主线程没有被销毁,程序将继续运行。 |
处理应用程序中的各种事件 | 事件响应、手势识别、界面刷新、自动释放池、NSTimer等事件处理。 |
节省CPU资源,提高程序性 | 当线程中有任务时,确保线程能够工作。当线程没有任务时,让线程休眠,提高程序性能,节省资源。该做事的时候做事,该休息的时候休息。 |
名称 | 类型 | 所在框架 | 原子性 |
---|---|---|---|
NSRunloop | NSObject | Fundation | 线程不安全 |
CFRunloop | struct | CoreFundation | 线程安全 |
Runloop
,阅读源代码是一个不错的选择。老司机说有了源码,runloop
就不会那么神秘了。首先,我们通常说runloop
有两种。一个是NSRunloop
,另一个是CFRunloop
。那么让我先通过源码了解一下CFRunloop
。首先,我们看一下基本的数据结构1.首先我们来看看
CFRunloop
的定义。这里我注释了所有需要注意的参数struct __CFRunLoop {
CFRuntimeBase_base;
pthread_mutex_t_lock;
/* locked for accessing mode list */
__CFPort_wakeUpPort;
// used for 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;
//doblocks的时候用到
struct _block_item*_blocks_tail;
CFTypeRef_counterpart;
};
可以看出
CFRunloop
是一个有很多属性的结构体。看一下这个结构中我们需要注意的参数。每个
runloop
都有自己的mode
,并且有不止一种mode
,mode
存放着runloop
要处理的事件源。事件源有3种,source
、timer
和observer
。Runloop
有很多模式,但是在某个时间只能有一种特定的模式,即_currentmode
。下面第二项描述的是runloop
的模式,它与runloop
结构体(mode)相关参数中的几个模式有关:_currentmode
,表示runloop
的当前模式;_Modes
表示运行循环中的所有模式;另外,
runloop
中的一种模式是 NSRunloop
常用模式。这种模式没有意义。它只是标记模式的集合;_Commonmodes
是指在NSRunloopCommonmodes
模式下保存的模式。我们还可以在这个集合中添加自定义模式;_Commonmodeitem
表示添加到NSRunloopCommonmodes
的源/定时器;2.上面提到的runloop中有很多模式。让我们了解一下模式cfrunloopmode。这是cfrunloop的源代码。
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 */
};
其实
CFRunloop
相关的几个定义都是结构体,CFRunloopMode
也是结构体。看代码了解CFRunloopMode
的几个相关参数,主要是上面标注的四个参数如上所述,
runloop
用于处理事件。它主要处理三种类型的事件:源、定时器和观察者。那么source
也可以分为source0
和source1
两种。CFRunloopMode
的定义中有四个集合,分别代表存储这四个事件源的集合,如上所述。runloop
中的模式主要有以下几种:1)
KCFRunloopDefaultMode
:app的默认模式。通常,主线程以这种模式运行。2)
UitrackingRunloopMode
:界面跟踪模式,用于Scrollview跟踪触摸滑动,保证界面滑动不受其他模式影响。3)
KCFRunloopCommonMode
:这是一个占位符模式。它用作标记 KCFRunloopDefaultMode
和 UITrackingRunnoopMode
。这不是真正的模式。4)
UiinitializationRunloopMode
:app刚启动时进入的第一个模式。启动后将不再使用。5)
CSeventreceiveRunloopMode
:接受系统事件的内部模式。通常不使用当
runloop
启动时,只能选择一种模式作为currentmode
。如果需要切换模式,只能退出当前模式,重新选择一个模式进入。这里基于上面对
CFRunloop
和CRFunloopMode
的理解,RunloopMode
保存在runloop中,实际执行的任务保存在RunloopMode
中。3.
RunloopMode
存储了runloop
要处理的事件源。事件源有3种:South
、timer
和observer
。1)
CFRunloopSourceRef
是 South
事件的来源。看看它的定义。struct __CFRunLoopSource {
CFRuntimeBase_base;
uint32_t_bits;
// 用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
pthread_mutex_t_lock;
CFIndex_order;
// immutable
CFMutableBagRef_runLoops;
union {
CFRunLoopSourceContext version0;
// immutable, except invalidation
CFRunLoopSourceContext1 version1;
// immutable, except invalidation
} _context;
};
CFRunloopSourceRef
是runloop要处理的事件源之一。Version0
和 Version1
是 source0
和 source1
根据不同事件的处理来区分的。2)
CFRunLoopTimerRef
。runloop
的相关定时器事件和定时器,定时执行一个任务,也在runloop
中处理。struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
//标记fire状态
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
//添加该timer的runloop
CFMutableSetRef _rlModes;
//存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval;
//理想时间间隔/* immutable */
CFTimeInterval _tolerance;
//时间偏差/* mutable */
uint64_t _fireTSR;
/* TSR units */
CFIndex _order;
/* immutable */
CFRunLoopTimerCallBack _callout;
/* immutable */
CFRunLoopTimerContext _context;
/* immutable, except invalidation */
};
3)
CFRunLoopObserverRef
。 CFRunloopObserverRef
是runloop的监听器,可以监听runloop的状态变化。struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities;
/* immutable */
CFIndex _order;
/* immutable */
CFRunLoopObserverCallBack _callout;
/* immutable */
CFRunLoopObserverContext _context;
/* immutable, except invalidation */
};
Runloop在运行时有以下状态:
文章图片
您可以将观察结果添加到运行循环。通过监控runloop的状态来判断是否卡顿。创建
observer
观察者,将创建的observer
加入到主线程runloop的普通模式中,并创建一个连续的子线程来监控主线程的runloop状态。一旦发现睡眠前的kCRRonloopBeforeSource
状态或者唤醒后的kCFRonloopFafterwaiting
状态,在设定的时间阈值内没有变化,就可以判断为卡住了,转储堆栈的信息,从而进一步分析哪个方法有执行时间长。4、了解了runloop的基本数据结构之后,我们来看看runloop是如何运行的。
首先,如何创建一个runloop?其实runloop不需要手动创建。任何 runloop 都与一个线程相关联。先有线程,后有runloop。Apple 提供了两个 API。让我们获取runloop、
CFRunloopgetmain()
和CFRunloopGetCurrent()
。这两个方法分别获取当前线程的mainrunloop
和runloop
。文章图片
从上面两个函数可以看出,
runloop
是通过_CFRunloopGet0()
,它以线程为参数,与通过key从NSDictionary
中获取值非常相似。接下来看看_CRFunloopGet0()
的实现。获取线程的运行循环。首先,以线程为key,从全局字典中查找。如果没有找到,就新建一个,以
thread
为key
,runloop
为value
保存到全局字典中(如果全局字典不存在,先初始化全局字典,并创建MainRunloop
保存它在全球字典中)。以下是源代码,我添加了注释。文章图片
以上就是获取当前runloop的原理,以及任务在runloop内部是如何执行的。这是一个图表。
文章图片
CFRunlooprun
和 CFRunloopRuninMode
都在内部调用 CFRunloopRunspecific
。CFRunloopRunspecific
内部调用__CFRunloopRun
,CFRunloopRunspecific
和__CFRunloopRun
一起就是runloop的完整实现。看看下面的伪代码解释,就是runloop的内部逻辑:文章图片
Runloop和线程的关系
- Runloop 存储在一个全局字典中。线程是关键,runloop 是价值。
- 第一次创建线程时,没有runloop对象。Runloop 将在第一次获取时创建。
- Runloop 在线程结束时被销毁。
- 主线程的runloop已经自动获取(创建),子线程默认不开启runloop。
- 每个线程都有一个唯一的runloop对象与之对应。
- 先有线程,后有runloop。
1.控制线程生命周期(线程保持活动,线程永远保持)。原理:如果模式中没有
soure0
/source1
/timer
/observer
,runloop会立即退出。因此,为了不让它退出,可以在runloop中添加一个soure1
。这是af2中使用的原理。下面AFNetworking的一个例子:
文章图片
- Tableview 延迟加载图片。将SETIMAGE置于
NSDefaultRunloopMode
中,即滑动时不会调用分配图片的方法,但滑动后切换到NSDefaultRunloopMode
才会调用。
3.解决滑动时
NSTimer
停止工作的问题(共模增加定时器)。NSTimer * timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
默认情况下,计时器处于
NSDefaultRunloop
模式。当我们滑动页面的时候,runloop
会切换到UITracking runloopmode
,所以我们的定时器会停止工作。就像商场里的倒计时一样,当我们滑动页面时,倒计时就会停止。为了解决这个问题,我们需要让定时器工作在UITracking runloopmode
,和NSRunloopCommonModes
,这个模式相当于NSDefaultRunloopMode
和UITrackingRunloopMode
的组合。因此,为定时器指定NSRunnoopcommonModes
模式,使定时器可以在NSDefaultRunloopMode
和UITrackingRunnloopMode
两种模式下运行。- 另外,可以通过监控runloop的状态来应用卡顿。
runloop
的进入sleep
前和唤醒后的两个loop状态定义的值分别是kCFRenloopbeforesources
和kCFRenloopafterwaiting
,即触发source0
回调和接收mach_port
消息有两种状态。创建observer
观察者,将创建的observer
加入到主线程runloop
的普通模式中,并创建一个连续的子线程来监控主线程的runloop
状态。一旦发现睡眠前的kCFRonloopBeforeSource
状态或者唤醒后的kCFRonloopFafterwaiting
状态,在设定的时间阈值内没有变化,就可以判断为卡住了,转储堆栈的信息,从而进一步分析哪个方法有执行时间长。【RunLoop原理】以上是开发中常用的runloop相关应用。