使用钩子实现了对字典和数组的赋值的校验,顺便随手撸了一个简单的jsonToModel
,iOS
除了runtime
还有一个东西的叫做runloop
,各位看官老爷一定都有了解,那么今天这篇文章初识一下runloop
。
什么是runloop
简单来讲runloop
就是一个循环,我们写的程序,一般没有循环的话,执行完就结束了,那么我们手机上的APP是如何一直运行不停止的呢?APP就是用到了runloop
,保证程序一直运行不退出,在需要处理事件的时候处理事件,不处理事件的时候进行休眠,跳出循环程序就结束。用伪代码实现一个runloop
其实是这样子的
1 | int ret = 0; |
获取runloop
iOS中有两套可以获取runloop代码,一个是Foundation
、一个是Core Foundation
。Foundation
其实是对Core Foundation
的一个封装,
1 | NSRunLoop * runloop1 = [NSRunLoop currentRunLoop]; |
runloop1
和mainloop1
地址一致,说明当前的runloop
是mainrunloop
,runloop1
作为对象输出的结果其实也是runloop2
的地址,证明Foundation runloop
是对Core Foundation
的一个封装。
RunLoop
底层我们猜测应该是结构体,我们都了解到其实OC
就是封装了c/c++
,那么c厉害之处就是指针和结构体基本解决常用的所有东西。我们窥探一下runloop
的真是模样,通过CFRunLoopRef *runloop = CFRunLoopGetMain();
查看CFRunloop
是typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
,我们常用的CFRunLoopRef
是__CFRunLoop *
类型的,那么再在源码(可以下载最新的源码)中搜索一下 struct __CFRunLoop {
在runloop.c 637行
如下所示:
1 | struct __CFRunLoop { |
经过简化之后:
1 | struct __CFRunLoop { |
runloop
中包含一个线程_pthread
,一一对应的CFMutableSetRef _modes
可以有多个mode
CFRunLoopModeRef _currentMode
当前mode
只能有一个
那么mode里边有什么内容呢?我们猜测他应该和runloop
类似,在源码中搜索CFRuntimeBase _base
看到在runloop.c line 524
看到具体的内容:
1 | struct __CFRunLoopMode { |
经过简化之后是:
1 | struct __CFRunLoopMode { |
一个mode
可以有多个timer
、souces0
、souces1
、observers
、timers
那么使用图更直观的来表示:
一个runloop
包含多个mode
,但是同时只能运行一个mode
,这点和大家开车的驾驶模式类似,运动模式和环保模式同时只能开一个模式,不能又运动又环保,明显相悖。多个mode
被隔离开有点是处理事情更专一,不会因为多个同时处理事情造成卡顿或者资源竞争导致的一系列问题。
souces0
- 触摸事件
- performSelector:onThread:
测试下点击事件处理源
1 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ |
#1
看到现在是在队列queue = ‘com.apple.main-thread’中,#10
Runloop
启动,#9
进入到__CFRunLoopDoSources0
,最终__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
调用了__handleEventQueueInternal
->[UIApplication sendEvent:]
->[UIWindow sendEvent:]
->[UIWindow _sendTouchesForEvent:]
->[UIResponder touchesBegan:withEvent:]
->-[ViewController touchesBegan:withEvent:](self=0x00007fc69ec087e0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00006000012a01b0) at ViewController.mm:22:2
,可以看到另外一个知识点,手势的传递是从上往下的,顺序是UIApplication -> UIWindow -> UIResponder -> ViewController
。
Source1
- 基于Port的线程间通信
- 系统事件捕捉
Timers
- NSTimer
- performSelector:withObject:afterDelay:
1 | timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); |
最终进入函数__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
调用了libdispatch的_dispatch_main_queue_callback_4CF
函数,具体实现有兴趣的大佬可以看下源码的实现。
Observers
- 用于监听RunLoop的状态
- UI刷新(BeforeWaiting)
- Autorelease pool(BeforeWaiting)
Mode
类型都多个,系统暴露在外的就两个,
1 | CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode; |
那么这两个Mode都是在什么情况下运行的呢?
kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
:App
的默认Mode
,通常主线程是在这个Mode
下运行UITrackingRunLoopMode
:界面跟踪Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其他Mode
影响
进入到某个Mode
,处理事情也应该有先后顺序和休息的时间,那么现在需要一个状态来表示此时此刻的status
,系统已经准备了CFRunLoopActivity
来表示当前的状态
1 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
1UL
表示无符号长整形数字1
,再次看到这个(1UL << 1)
我么猜测用到了位域或者联合体,达到省空间的目的。kCFRunLoopAllActivities = 0x0FFFFFFFU
转换成二进制就是28个1
,再进行mask
的时候,所有的值都能取出来。
现在我们了解到:
CFRunloopRef
代表RunLoop
的运行模式- 一个
Runloop
包含若干个Mode
,每个Mode
包含若干个Source0/Source1/Timer/Obser
Runloop
启动只能选择一个Mode
作为currentMode
- 如果需要切换
Mode
,只能退出当前Loop
,再重新选择一个Mode
进入 - 不同组的
Source0/Source1/Timer/Observer
能分隔开来,互不影响 - 如果
Mode
没有任何Source0/Source1/Timer/Observer
,Runloop
立马退出。
runloop切换Mode
1 | CFRunLoopObserverRef obs= CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { |
当runloop
切换mode
的时候,会退出当前kCFRunLoopDefaultMode
,加入到其他的UITrackingRunLoopMode
,当前UITrackingRunLoopMode
完成之后再退出之后再加入到kCFRunLoopDefaultMode
。
我们再探究下runloop
的循环的状态到底是怎样来变更的。
1 | // //获取loop |
runloop
唤醒之后不是立马处理事件的,而是看看timer
有没有事情,然后是sources
,发现有触摸事件就处理了,然后又循环查看timer
和sources
一般循环2次进入休眠状态,处理source
之后是循环三次。
RunLoop在不获取的时候不存在,获取才生成
RunLoop
是在主动获取的时候才会生成一个,主线程是系统自己调用生成的,子线程开发者调用,我们看下CFRunLoopGetCurrent
1 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
看到到这里相信大家已经对runloop
有了基本的认识,那么我们再探究一下底层runloop
是怎么运转的。
首先看官方给的图:
那我又整理了一个表格来更直观的了解状态运转
步骤 | 任务 |
---|---|
1 | 通知Observers:进入Loop |
2 | 通知Observers:即将处理Timers |
3 | 通知Observers:即将处理Sources |
4 | 处理blocks |
5 | 处理Source0(可能再处理Blocks) |
6 | 如果存在Source1,跳转第8步 |
7 | 通知Observers:开始休眠 |
8 | 通知Observers:结束休眠1.处理Timer2.处理GCD Asyn To Main Queue 3.处理Source1 |
9 | 处理Blocks |
10 | 根据前面的执行结果,决定如何操作1.返回第2步,2退出loop |
11 | 通知Observers:退出Loop |
查看runloop源码中runloop.c
2333行
1 | //入口函数 |
经过及进一步精简
1 | //入口函数 |
精简到这里基本都能看懂了,还写了很多注释,基本和上面整理的表格一致。
这里的线程休眠__CFRunLoopServiceMachPort
是调用内核函数mach_msg()进行休眠,和我们平时while(1)
大不同,while(1)
叫死循环,其实系统每时每刻都在判断是否符合条件,耗费很高的CPU,内核则不同,Mach内核提供面向消息,基于基础的进程间通信。
保活机制
一个程序运行完毕结束了就死掉了,timer
和变量也一样,运行完毕就结束了,那么我们怎么可以保证timer
一直活跃和线程不结束呢?
timer保活和多mode运行
timer
可以添加到self
的属性保证一直活着,只要self
不死,timer
就不死。timer
默认是添加到NSDefaultRunLoopMode
模式中,因为RunLoop
同时运行只能有一个模式,那么在滑动scroller
的时候怎Timer
会卡顿停止直到再次切换回来,那么如何保证同时两个模式都可以运行呢?Foundation
提供了一个API(void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode
添加上,mode
值为NSRunLoopCommonModes
可以保证同时兼顾2种模式。
测试代码:
1 | static int i = 0; |
当滑动的时候timer
的时候,timer
还是如此丝滑,没有一点停顿。
没有卡顿之后我们VC -> dealloc
中timer
还是在执行,那么需要在dealloc
中去下和删除观察者
1 | -(void)dealloc{ |
退出vc
之后dealloc
照常执行,日志只有-[ViewController dealloc]
,而且数字没有继续输出,说明删除观察者和取消source
都成功了。
那么NSRunLoopCommonModes
是另外一种模式吗?
通过源码查看得知,在runloop.c line:1632 line:2608
1 | if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { |
还有很多地方均可以看出,当是currentMode
需要和_mode
相等才去执行,当是kCFRunLoopCommonModes
的时候,只需要包含curMode
即可执行。可见kCFRunLoopCommonModes
其实是一个集合,不是某个特定的mode
。
线程保活
线程为什么需要保活?性能其实很大的瓶颈是在于空间的申请和释放,当我们执行一个任务的时候创建了一个线程,任务结束就释放掉该线程,如果任务频率比较高,那么一个一直活跃的线程来执行我们的任务就省去申请和释放空间的时间和性能。上边已经讲过了runloop
需要有任务才能不退出,总不可能直接让他执行while(1)
吧,这种方法明显不对的,由源码得知,当有监测端口的时候,也不会退出,也不会影响应能。所以在线程初始化的时候使用
1 | [[NSRunLoop currentRunLoop] addPort:[NSPort port] |
来保活。
在主线程使用是没有意义的,系统已经在APP启动的时候进行了调用,则已经加入到全局的字典中了。
验证线程保活
1 | @property (nonatomic,strong) FYThread *thread; |
[[NSRunLoop currentRunLoop] addPort:[NSPort port]forMode:NSDefaultRunLoopMode]
添加端口注释掉,直接执行了--end--
,线程虽然strong
强引用,但是runloop
已经退出了,所以函数alive
没有执行,不注释的话,alive
还会执行,end
一直不会执行,因为进入了runloop
,而且没有退出,代码就不会向下执行。
那我们测试下该线程声明周期多长?
1 | - (void)viewDidLoad { |
拥有该线程的是VC
,点击pop
的时候,但是VC
和thread
没释放掉,好像thread
和VC
建立的循环引用,当self.thread=[[FYThread alloc]initWithTarget:self selector:@selector(test) object:nil];
注释了,则VC
可以进行正常释放。
通过测试了解到
这个线程达到了永生,就是你杀不死他,简直了死待。查找了不少资料才发现官方文档才是最稳的。有对这句[[NSRunLoop currentRunLoop] run]
的解释
If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
就是系统写了以一个死循环但是没有阻止他的参数,相当于一直在循环调用runMode:beforeDate:
,那么该怎么办呢?
官方文档给出了解决方案
1 |
|
将代码改成下面的成功将死待杀死了。
1 | - (void)test { |
点击popVC:
首先将self.shouldKeepRunning = NO
,然后子线程执行CFRunLoopStop(CFRunLoopGetCurrent())
,然后在主线程执行pop
函数,最终返回上级页面而且成功杀死VC
和死待。
当然这个死待其实也是有用处的,当使用单例模式作为下载器的时候使用死待也没问题。这样子处理比较复杂,我们可以放在VC
的dealloc
看看是否能成功。
关键函数稍微更改:
1 | //停止子线程线程 |
当点击返回按钮VC
和线程都没死,原来他们形成了强引用无法释放,就是VC
始终无法执行dealloc
。将函数改成block
实现
1 | __weak typeof(self) __weakSelf = self; |
测试下崩溃了,崩溃到了:
1 | while (__weakSelf.shouldKeepRunning ){ |
怎么想感觉不对劲啊,怎么会不行呢?VC
销毁的时候调用子线程stop
,最后打断点发现到了崩溃的地方self
已经不存在了,说明是异步执行的,往前查找使用异步的函数最后出现在了[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
,表示不用等待stopThread
函数执行时间,直接向前继续执行,所以VC
释放掉了,while (__weakSelf.shouldKeepRunning )
是true
,还真进去了,访问了exe_bad_access
,所以改成while (__weakSelf&&__weakSelf.shouldKeepRunning )
再跑一下
1 | //log |
如牛奶般丝滑,解决了释放问题,也解决了复杂操作。本文章所有代码均在底部链接可以下载。
使用这个思路自己封装了一个简单的功能,大家可以自己封装一下然后对比一下我的思路,说不定有惊喜!
资料参考
- runloop源码
- 小码哥视频
- 任务调度
- libdispatch
资料下载
- 学习资料下载git
- demo code git
- runtime可运行的源码git
- thread保活c语言版本
- thread 保活
最怕一生碌碌无为,还安慰自己平凡可贵。
广告时间