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,它使的我们作为应用层面的开发人员,可以调用内核层面的 函数,线程休眠就是一种内核层面的操作。