Runloop
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil,
NSStringFromClass([AppDelegate class]));
}
}
入口函数main 中UIApplicationMain 主线程默认开启了
runloop,主线程一直不会结束,也不会有返回值。导致main
函数一直运行中,程序也就不会自动退出了。
线程 和 主runloop 是一一对应的,程序依赖进程,进程依赖
线程,而线程都需要一个对应的runloop来维持,否则会被释
放。
一条线程持续运行需要一个runloop来维持
RunLoop对象创建后,会被保存在一个全局的Dictionary里,
线程作为key,Runloop对象作为value
static CFMutableDictionaryRef __CFRunloops = NULL;
访问runloop
iOS中有两套api 访问和和使用 runloop:
1:Foundation 框架的 NSRunloop
2:Core Foundation 框架的 CFRunloopRef
NSRunloop 是 基于 CFRunloopRef 上的一层OC封装,本质都是一样的,用法
不一样
创建 runloop
不允许开发者手动创建,只能使用现有的API 获取时,交由系统创建
NSRunloop *runloop1 = [NSRunloop mainRunloop];
NSRunloop *runloop2 = [NSRunloop currentRunloop];
CFRunloopRef *runloop1 = CFRunloopGetMain();
CFRunloopRef *runloop2 = CFRunloopGetCurrent();
本质上都是调用 _CFRunloopGet0 方法创建
_CFRunloopGet0方法步骤:
runloop 字典是否存在,不存在就创建
以线程为key,获取runloop,获取到就返回
获取不到就会创建一条runloop 对应并保存起来
说明子线程在获取的时候会自动创建runloop
runloop 不允许开发者手动创建,只能是在获取的时候自动创建
runloop 小结:
线程与runloop是一一对应 的,使用全局字典保存这个关系,线程作key,
runloop 作value。
主线程系统自动创建runloop(在 UIApplicationMain ()方法中)以维持app
运行,
但是,
子线程创建时没有,需要主动获取,系统才会创建,否则任务结束前线程就已经
被销毁
runloop创建发生在第一次获取时,销毁发生在线程结束时
子线程获取runloop后,虽然创建了runloop ,但是如果不给事件(比如timer,
source 等)同样不行,我们需要手动将timer放入runloop的某个mode里才
行。
RunLoop
结构
****** __CFRunLoop *******
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop *
CFRunLoopRef;
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort;// used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
uint32_t _winthread;
// ️ ️ ️ ️ ️ ️ ️ 核心组成 ️ ️ ️ ️ ️ ️
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
// ️ ️ ️ ️ ️ ️ ️ 核心组成 ️ ️ ️ ️ ️ ️
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
疑问:
从上面的代码结构中发现mode,但是没有发现sourxe,timer等等?
modes
mode 就像manager一样管理着runloop的事件状态及细节:
************** __CFRunLoopMode ***********
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before
locking this */
Boolean _stopped;
char _padding[3];
// ️ ️ ️ ️ ️ ️ ️核心组成 ️ ️ ️ ️ ️ ️
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
// ️ ️ ️ ️ ️ ️ ️核心组成 ️ ️ ️ ️ ️ ️
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS // ️ 处理GCD事件
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 */
};
modes
看图
一个RunLoop对象包含多个modes
modes 里 包含很多mode 【RunLoopMode】
每个RunLoopMode内部核心内容是4个数组容器:
Set(source0),Set(source1),
MArray(observer)和MArray(timer)
UIinitializationRunloopMode
app启动时第一个mode,
启动初始化后不再使用,
切换到 defaultMode
kCFRunLoopDefaultMode
默认Mode,通常主线程
是在这个Mode下运行的
包含:
Set(source0)
Set(source1)
MArray(observer)
MArray(timer)
UITrakingRunLoopMode
界面追踪Mode,Scrollview
的触摸滑动事件
包含:
Set(source0)
Set(source1)
MArray(observer)
MArray(timer)
NSRunLoopCommonModes
包含左边两种mode的容器,
实际运行是来回切换的
包含:DefaultMode
Set(source0)
Set(source1)
MArray(observer)
MArray(timer)
包含:TrakingMode
Set(source0)
Set(source1)
MArray(observer)
MArray(timer)
RunLoop对象内部有一个_currentMode
当前正在运行的mode,可能是default,可能是traking
小结
每个runloop包含多个mode,每个mode都有四个列表用于存储source,
timer,obsever
runloop运行时只在某一个mode上,设置为currentMode
mode都切换都会退出当前loop,重新进入mode。并执行新mode的事件,而先
前的mode事件被停止
如果当前mode中没有任何source,timer,observer 就会立马退出当前
runloop
RunLoopMode
经过上面的理解,我们再看具体mode
CFRunLoopSourceRef
分为source0和source1
source0
触摸事件处理、[performSelector: onThread: ]
App自己管理的UIEvent,CFSocket等等
1 触摸事件 :hitTest:withEvent
硬件Event 事件转 source1 再转 source0 处理,
2 performSelectors的事件
假如你在主线程performSelectors一个任务到子线程
小结
source0 是基于非Port 的事件
source1
基于Port的线程间通信、系统事件捕捉
AF常驻线程就是添加port信号
1. 进程间通信: 进程直接通过端口port来通信,这是系统来调度的,不是用户
添加代码
2. 硬件或者其它进程事件转化来的 source1 都将分发给 source0 进行处理
因此也可以简单理解为:
source1 是基于端口Port的事件
source0 是基于非Port 的事件
小结
source1 包含mach_port 和一个回调(函数指针)。可以监听系统端口,通过
内核和其它进程及其它线程通信,还能接收,分发 系统事件,并主动唤醒
runloop ,这些都是系统帮我们处理的。
触摸屏幕
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会被IOKit先包装成Event,通过
mach_Port传给正在活跃的APP , Event先告诉
source1(mach_port),source1唤醒RunLoop, 然后将事件Event分发给
source0,然后由source0来处理
CFRunLoopTimerRef
timer
定时器事件、[performSelector: withObject: afterDelay:]
注意: timer 可由开发者手动添加到loop中,也是我们常见的场景
小结
runloopTimer 基于时间触发,包含时间长度 和一个回调(函数指针)。
注册timer: 当timer被加入runloop时,runloop会注册对应的时间点,当时间
点到了时,runloop会唤醒然后执行timer的那个回调(也就是block)。
这也即是timer完全依赖runloop,一旦runloop被堵塞,那么timer也就受到影
响。
CFRunLoopTimer 和 NSTimer 是toll-free bridged 对象桥接 ,也就是可以相
互转换,同一个东西,一个是C 语法使用,一个是OC 语法使用,底层都是C
timer滑动时被停止?
原因:
由于_currentMode 只能在某一个mode下,而timer默认是在defaultMode下,
所以滑动时实际使用的是 TrackingMode 里的 timer ,不一样
解决:
将timer加入到commonMode时,会被同时加入到两种mode里的timer 数组
里,引用,对象是同一个,所以不管那种状态都会有效
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
CFRunLoopObserverRef
监听者
状态变更
Runloop状态变更的时,会通知监听者进行函数回调,UI界面的刷新就是在监听
到Runloop状态为BeforeWaiting时进行的
/* Run Loop Observer Activities */
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不同状态发生变化时都能发出通知,根据这个
通知我们可以处理相应的事情。
可以 通过 obser = CFRunloopObsevrCreateWithHandler(xx) 的block 来
获取当前线程的observer 和 Activities ,然后 根据Activities 不同状态处理不
同事件
Runloop
启动和关闭
启动
run
无条件启动
简单,无条件,但是也无法控制runloop。
不能选择运行模式
runUntilDate
设置时间限制下启动
相比run,可以指定在某个时间后结束
runMode:before:Date:
在特定模式下启动
更优的选择,可以指定 mode和过期时间
//创建一个timer
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES
block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer事件2");
}];
//将timer添加到RunLoop的指定模式里面
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
退出
方式
设置超时模式时
手动停止
问题
删除输入源,定时器可能导致runloop退出,但是不可靠,系统可能会添加一些
输入源导致无法退出
设置标记控制
使用date可以控制,但是控制的精度不够,要求在某个逻辑或者某个时间点结束
场景下做不到。
CFRunloopStop() 结束当前add方法:
if(self.needstop) {
//创建一个timer
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES
block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer事件2");
}];
//将timer添加到RunLoop的指定模式里面
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
}
GCD与RunLoop
GCD和RunLoop是两个独立的机制,大部分情况下是彼此不相关的。但是上面我
们看到RunLoop里面有一个核心操作叫
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__,翻译过来大
概是 RunLoop正在服务(GCD的)主线程队列,说明GCD讲一些事情交给了
RunLoop处理。实际上,当我们从子线程异步调回到主线程执行任务时,GCD会
将这个主线程任务丢给RunLoop,最后通过
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__函数传送给
GCD内部去处理,下面的代码就是这种情况
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"子线程事件");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"回到主线程");
});
});
线程的休眠
while(1){;} 循环 会一直占用CPU,并不是真正的暂停
__CFRunLoopServiceMachPort函数
是一种真正意义上的休眠,它使得当前线程真正停下来,并且不再需要占用CPU
资源去执行汇编指令了。其内部其实调用了mach_msg()函数,这是系统内核提
供给我们的一个API,它使的我们作为应用层面的开发人员,可以调用内核层面的
函数,线程休眠就是一种内核层面的操作。