iOS底层-Runloop(一)
先说说我是咋理解runloop的吧?然后在专业的讲讲
本人理解:
- 从C语言的角度来看,整个app就是调用了一个main函数,只是这个main函数一直不返回值.
- main函数内部:
- 常识: 执行任务需要开启一个线程,因为任务必须在线程下执行
- 一般的main函数执行流程:开启一个线程->执行main函数->添加许多任务(即写了很多代码)->任务完成(代码执行完毕)->返回main函数值程序结束->线程关闭
- runloop啥作用?
- Runloop就是一个死循环,把这个死循环放在上面开启的线程中,那么线程就永远不会结束.因此main永远不返回.但是这个死循环比较牛逼,下面讲讲他的牛逼之处:
- 一般死循环特点:
- 程序一旦进入死循环,一直循环执行,永不停歇,并且执行速度非常快
- 只执行这段代码,其他任何代码都不会执行,也就是说,只专注在这跑圈,不管外界发生任何事
- Runloop特点:
- 程序一旦进入runloop,当外界没事的时候,他就慢慢跑圈,(至于多慢我就不知道了),甚至休眠,一旦有事,他就快速跑到该事件,去执行该事件,执行完毕后再接着休眠
RunLoop的传说
- 什么是runloop?
- 字面理解:运行循环/跑圈
- 在程序运行过程中循环做一些事情
- 基本作用
- 保持程序的持续运行
- 打开app它一直不死,就是因为runloop
- 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
- 能让我们的主线程,有事情就做事情,没事情就休息,比如点击事件:点击前主线程在休息,当点击事件处理完毕后,让主线程再次休息…
- 保持程序的持续运行
-
如果没有runloop
int main(int argc, char * argv[]) { NSLog(@"execute main function"); return 0; }
- 第3行后程序就结束了
-
如果有了runloop大致运行代码逻辑如下:
int main(int argc, const char * argv[]) { @autoreleasepool { int retval = 0; do{ //睡眠中等待消息 int message = sleep_and_wait(); //处理消息 retval = process_message(message); }while(0== retval); } return 0; }
- 由于main函数里面( UIApplicationMain())启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态
-
main函数中的RunLoop
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
- 代码的UIApplicationMain函数内部就启动了一个RunLoop
- 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
- 这个默认启动的RunLoop是跟主线程相关联的
既然runloop那么牛逼,那么我们就来认识他一下:
RunLoop
RunLoop对象
- 相关框架
- iOS中有2套API来访问和使用RunLoop:
- Foundation(OC):NSRunLoop
- Core Foundation(C):CFRunLoopRef
- 该框架有个特点:所有的类都已CF开头,以Ref(reference:引用)结尾
- NSRunLoop和CFRunLoopRef都代表着RunLoop对象
- NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
- CFRunLoopRef是开源的
- iOS中有2套API来访问和使用RunLoop:
- RunLoop与线程关系
- 上面说到,runloop通常是用来卡住线程的,那么他们必然有关系啦
- 每条线程都有唯一的一个与之对应的RunLoop对象
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop在第一次获取时创建,在线程结束时销毁
- 主线程的RunLoop已经自动创建好了,并且主动启动了,子线程的RunLoop需要主动创建,然后手动启动
- 上面说到,runloop通常是用来卡住线程的,那么他们必然有关系啦
- 获得RunLoop对象
-
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
-
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象 CFRunLoopGetMain(); // 获得主线程的RunLoop对象
-
-
举例使用
- (void)viewDidLoad { [super viewDidLoad]; //当前线程就是主线程,打印地址一样 NSLog(@"%p--%p",[NSRunLoop mainRunLoop],[NSRunLoop currentRunLoop]); //创建一个线程 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil]; [thread start]; } -(void)run{ /* [NSRunLoop currentRunLoop]:这就是在子线程中创建了一个runloop,不是通过alloc/init,而且该创建是懒加载的 */ NSLog(@"%p",[NSRunLoop currentRunLoop]); }
RunLoop的相关类
- Core Foundation中关于RunLoop的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
- 查看源码
-
CFRunLoopRef的本质
typedef struct __CFRunLoop * CFRunLoopRef; struct __CFRunLoop { pthread_t _pthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; //当前模式 CFMutableSetRef _modes; //集合内部装的是CFRunLoopModeRef类型 ... };
- 一个Runloop内部有一堆mode,但是仅仅有一个是当前mode
-
CFRunLoopModeRef的本质
typedef struct __CFRunLoopMode *CFRunLoopModeRef; struct __CFRunLoopMode { CFStringRef _name; CFMutableSetRef _sources0; //集合内部装的是CFRunLoopSourceRef类型 CFMutableSetRef _sources1; //集合内部装的是CFRunLoopSourceRef类型 CFMutableArrayRef _observers; //集合内部装的是CFRunLoopObserverRef类型 CFMutableArrayRef _timers; //集合内部装的是CFRunLoopTimerRef类型 ... };
-
这几个类的关系图如下:
- 分析:
- RunLoop就像一个汽车,要跑起来,那么就要装一些必备的东西
- RunLoop里面有很多Mode对象->每个Mode对象里面又有三种集合分别存放:Source/Observer/Timer对象->有了这些Mode以及子内容runloop才能一直跑圈/休息/有事件触发
- 注意:runloop中的Mode中必须有source或者timer才能一直运行,否则跑一圈就停了
- 分析:
-
CFRunLoopModeRef
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
- 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
[NSRunLoop currentRunLoop] runMode: beforeDate:
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
- 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
-
如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
-
系统默认注册了5个Mode:(前两种最重要)
- kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行
-
UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- 分析:当我们滚动图片时就会切换到这个模式,跟踪你的滚动,一旦停止滚动就会切换到Default模式
- 举例:当我们滚动的时候,其他事件没反应,比如定时器,当滚动的时候定时器没反应,因为它在Default模式中
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
CFRunLoopTimerRef
- CFRunLoopTimerRef是基于时间的触发器,基本上说的就是NSTimer
- 它受runloop的Mode影响
- GCD的定时器不受runLoop的Mode影响
- GCD定时器实现不一样
-
代码举例
- (void)viewDidLoad { [super viewDidLoad]; //向sb中拖入一个textView //情况一:没有滚动textView时,1秒打印一次,一旦滚动textView,定时器无用了,停止滚动,定时器有起作用了 // [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES]; //上面一段代码做了很多事情,等价代码如下: //timer添加到defaultMode里面去,mode再添加到runloop中去,runloop在启动时候再指定这个mode->拿出timer来用 NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES]; //也就说只能在Defaultmode下好使 // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; //情况二:如果想要一滚动timer就有用,一停止滚动timer就失效,那么模式就用UITrackingRunLoopMode // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; //情况三:滚动不影响定时器 //定时器只会跑在标记为CommonModes的模式下(或者用GCD定时器) [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; //通过打印runloop,我们发现kCFRunLoopDefaultMode/UITrackingRunLoopMode这两种模式都被标记为了commonmodes标签了,因此就可以做到了 NSLog(@"%@",[NSRunLoop currentRunLoop]); } - (void)test { NSLog(@"----"); }
CFRunLoopSourceRef
- 系统定的我们用不着
- CFRunLoopSourceRef是事件源(输入源)
- 我们的一些触发事件,都是由source触发的
- 以前的分法
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources(处理performselector调用)
- 根据函数调用栈分法
- Source0:非基于Port
- Source1:基于Port的,通过内核和其他线程通信,接收/分发系统事件
- 以前的分法
CFRunLoopObserverRef
- CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
-
可以监听的时间点有以下几个:
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0),//即将进入loop,左移动0->2的0次方= 1 kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer …2 kCFRunLoopBeforeSources = (1UL << 2),//即将处理source...4 kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠 …32 kCFRunLoopAfterWaiting = (1UL << 6),//刚刚从休眠中唤醒...64 kCFRunLoopExit = (1UL << 7),//即将推出loop 128 kCFRunLoopAllActivities = 0x0FFFFFFFU };
- 代码举例(自己监听runloop的状态)
- 可以监听触摸、滚动,runloop的状态改变
// 创建observer CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"====runloop的当前模式:%@",mode); CFRelease(mode); } break; case kCFRunLoopExit: { CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()); NSLog(@"====runloop的当前模式:%@",mode); CFRelease(mode); } break; default: break; } // NSLog(@"----监听到RunLoop状态发生改变---%zd", activity); }); //添加观察者到当前线程的RunLoop的当前Mode中,监听runloop的状态 CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 释放Observer CFRelease(observer); /* //需要自己管理内存 CF的内存管理(Core Foundation) 1.凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release * 比如CFRunLoopObserverCreate 2.release函数:CFRelease(对象); */
RunLoop的逻辑流程图
- runloop一旦启动,在某种模式下运行时,没有消息时等待状态,一旦有消息就立刻进行处理,那么处理的是什么东西呢?
- 处理的就是当前模式下的source0、source1、observers、timers这些东西,那这些东西是什么呢?
- Source0
- 触摸事件处理
- performSelector:onThread:
- Source1
- 基于Port的线程间通信
- 系统事件捕捉
- Timers
- NSTimer
- performSelector:withObject:afterDelay:
- Observers
- 用于监听RunLoop的状态
- UI刷新(BeforeWaiting)
- Autorelease pool(BeforeWaiting)
- Source0
- 举例使用
-
在控制器中实现触摸方法,然后打断点
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"=====%s",__func__); }
-
在控制台输入
bt
,打印所有的函数调用栈:* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 * frame #0: 0x000000010ee02b3d RunloopTest`-[ViewController touchesBegan:withEvent:](self=0x00007f910750d9f0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600000cdc7e0) at ViewController.m:22:5 frame #1: 0x0000000112f9ba09 UIKitCore`forwardTouchMethod + 353 frame #2: 0x0000000112f9b897 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49 frame #3: 0x0000000112faac48 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1869 frame #4: 0x0000000112fac5d2 UIKitCore`-[UIWindow sendEvent:] + 4079 frame #5: 0x0000000112f8ad16 UIKitCore`-[UIApplication sendEvent:] + 356 frame #6: 0x000000011305b293 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3232 frame #7: 0x000000011305dbb9 UIKitCore`__handleEventQueueInternal + 5911 //处理source0的C函数 frame #8: 0x000000010f767be1 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17 //处理source0事件 frame #9: 0x000000010f767463 CoreFoundation`__CFRunLoopDoSources0 + 243 //创建主运行循环 frame #10: 0x000000010f761b1f CoreFoundation`__CFRunLoopRun + 1231 frame #11: 0x000000010f761302 CoreFoundation`CFRunLoopRunSpecific + 626 frame #12: 0x00000001186f22fe GraphicsServices`GSEventRunModal + 65 //UIApplicationMain函数 frame #13: 0x0000000112f70ba2 UIKitCore`UIApplicationMain + 140 //调用main函数 frame #14: 0x000000010ee02bf0 RunloopTest`main(argc=1, argv=0x00007ffee0dfcf80) at main.m:14:16 //dyld系统函数调用:开启线程start,加载framework,动态静态链接库,bundle,启动图片,Info.plist,pch等 frame #15: 0x0000000111ae1541 libdyld.dylib`start + 1
- 从函数调用栈可以看出,runloop处理的是source0,因此触摸事件是属于source0
-
- 源码分析:
- 下载runloop源码,然后找到CFRunLoop.c
- 从上面的函数调用栈可以看出,UIApplicationMain调用的第一个函数是
CFRunLoopRunSpecific
,在源码中定位到改函数,就是Runloop的入口 -
将源码简化如下:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { //通知observer,进入loop if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); //具体要做的事情 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); //通知observer,退出Runloop if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); return result; } /* rl, rlm are locked on entrance and exit */ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = 0; do { //通知observer:即将处理timers if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); //通知observer:即将处理sources if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); //处理blocks __CFRunLoopDoBlocks(rl, rlm); //处理sources0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); //处理blocks if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } //判断有无source1:port属于sorce1 if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { //有source1,跳转到handle_msg,处理source1 goto handle_msg; } //通知observer:即将休眠 if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); //休眠 __CFRunLoopSetSleeping(rl); //等待别的消息来唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); //唤醒 // user callouts now OK again __CFRunLoopUnsetSleeping(rl); //通知observer:结束休眠 if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); handle_msg: //被timer唤醒 if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { CFRUNLOOP_WAKEUP_FOR_TIMER(); //处理timers __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()); } //被GCD唤醒 else if (livePort == dispatchPort) { //处理GCD __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else { //source1 //处理source1 __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) } //处理blocks __CFRunLoopDoBlocks(rl, rlm); //设置返回值retVal,是否需要继续循环 if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } } while (0 == retVal); return retVal; }
-
简化成流程图如下:
-
其中的处理blocks是什么呢? 如下:
//runloop运行我们添加一些block的 CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{ });
-
注意:一般情况下GCD是不依赖于Runloop的,GCD有自己的逻辑处理,跟Runloop无关,但是有一种情况,GCD需要交给Runloop来处理
-
线程间通信:子线程回到主线程刷新UI
dispatch_async(dispatch_get_global_queue(0, 0), ^{ //子线程处理事情。。。 //回到主线程刷新UI dispatch_async(dispatch_get_main_queue(), ^{ }); });
-
-