关于iOS开发中RunLoop的知识,归纳起来还是蛮多的。这一次花大力气,系统地总结下,也算是给自己复习功课。
本篇文章主要介绍以下知识点:
1.RunLoop的基本概念。
2.获取RunLoop的源码。
3.RunLoop的底层实现。
4.SrollView滑动,NSTimer失效分析。
一、RunLoop的基本概念:
1、什么是RunLoop
RunLoop,即运行循环。官方给的定义是:“RunLoop是与线程相关的基本基础结构的一部分。RunLoop是一个事件处理循环,用于调度工作和协调接收传入事件。RunLoop的目的是在有工作要做时,让线程保持忙碌;在没有工作时,让线程进入睡眠状态”。
具体原文在以下链接。
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
从上面这段话可看出,RunLoop有三个方面的含义:
1).说明了RunLoop与线程的关系-----RunLoop与线程相关。
2).本质上说,RunLoop是一个事件循环,能够处理输入的事件。
3).RunLoop的作用就是保证线程一直运行着。
2、RunLoop和线程之间的关系
通过前面的概念,我们知道:
1).RunLoop与线程相关,和线程是一一对应的。
2).主线程的RunLoop由系统创建并默认启动。其实研究过App启动源码就知道:在Main.m的UIApplicationMain()函数内部,系统帮忙做了三件事情中,其中一件就是创建RunLoop并启动。
3).对于子线程的RunLoop必须手动创建,且是在第一次获取时创建,在线程结束时被销毁。
这里有个面试题。
App的主线程为什么一直运行?
答案就是在刚才说到的RunLoop与线程关系的第二点。
二、RunLoop的源码获取
在我们开发中,使用RunLoop的类是NSRunLoop,NSRunLoop本身是基于 CFRunLoopRef 的二次封装,而CFRunLoopRef 是属于 CoreFoundation 框架内的,这个框架主要提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
CoreFoundation 框架的下载地址是https://opensource.apple.com/tarballs/CF/ 可以下载最新的源码。
在源码中,RunLoop模块的代码主要在CFRunLoop.h/c文件内。
三、RunLoop的底层实现
在了解RunLoop的底层实现前,需要了解两个非常重要的概念:事件源(Sources)和模式(Modes)。
事件源
事件源(Sources)指的是事件触发的源头,可以理解为触发事件的方式,苹果官方文档将其分为两种:输入源(Input Sources)和定时器源(Timer Sources)。其中输入源表示触发异步事件的源,例如一个线程和另一个线程之间的消息;定时器源表示触发同步事件的源,例如预定时间内触发的事件或重复触发的事件,最典型的例子就是NSTimer。
下图为RunLoop的结构图
简单说明下,上图概要地展示了RunLoop的事件运行机制:一个线程在循环运行并等待事件源的到来,当事件源到来时,处理对应的事件。
上图中左边 为线程在RunLoop上运行的过程,其中runUntilDate表示执行一段时间后结束,timerFired表示处理定时器事件,mySelector处理perform事件,handlePort处理Port事件,start表示创建并开启一个RunLoop,end表示RunLoop停止。
上图中右边为事件源,其中输入源有三种类型,第一种是通过端口(Port)触发,第二种是用户(Custom)自定义事件触发,第三种是通过performSelector:onThread触发。定时器源只有一种,就是Timer。
对应到源码就是如下
从__CFRunLoopSource的结构体中可以看到有一个联合体union,里面有version0和version1两个成员变量,这两个变量对应了Source0和Source1,表示事件源有两种类型。
联合体的作用是共享存储空间,也就是说,version0和version1两个变量(即Source0和Source1)共享一段存储空间,也就是说一个__CFRunLoopSource结构体要么为Source0事件源,要么为Source1事件源。
而对于Source0和Source1的区别如下:
1)Source0对应的是手动触发的事件,就是用户自定义(Custom)和performSelector:onThread两类事件源。
2)Source1为基于端口触发的事件,主要为端口(Port)的事件源。
模式
模式(Mode)指的是一个包括输入源(Inputsource)、定时器(Timer)、观察者(Observer)的模型对象。一个RunLoop在运行的时候,需要配置一个模式来告诉RunLoop需要执行哪些事件。简单点来说,模式就是用来存储RunLoop需要响应的事件,这些事件包括许多输入源、定时器和观察者。具体格式如下图
再到源码中看下模式的定义:
从上图可以看出:
_sources0和_sources1是使用Set集合存储,对应的是事件源;
_observers和_timers 是用数组存储,分别对应观察者和定时器。
模式类型
根据网络资料,模式一般定义以下5种类型。
1.Default
2.Connection
3.Modal
4.Event tracking
5.Common modes
以下各个模式类型的举例和说明
讲完了两个概念,讲讲RunLoop的运行情况。
当一个RunLoop运行时,该RunLoop会执行当前模式中的事件源、定时器和通知观察者。简单来说,一个RunLoop,执行模式中三种类型的事件。
而对于当前模式的定义。苹果提供了三种方式:
1.默认模式,即上图中的NSDefaultRunLoopMode,在用户没有输入其他模式模式下,会执行默认模式中的事件源、定时器和通知观察者。
2.公共模式,即上图中的NSRunLoopCommonMode,在我们实际开发中,使用该模式解决了ScrollView滑动,NSTimer就失效的问题?
3.UI跟踪模式,即NSEventTrackingRunLoopMode,这个模式在实际中用得少,就不说了。
下面分别介绍下模型对象
观察者
观察者为检测RunLoop状态的对象,当RunLoop状态发生变化时会通知观察者。
那RunLoop又有哪些状态呢?查看源码得知,有7个状态。每一个状态发生变化都会通知观察者。具体状态如下:
/* 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), //表示退出状态。
kCFRunLoopAllActivities = 0x0FFFFFFFU //表示所有的状态。
};
也就是说当上面7个状态任意一个发生改变,都会通知RunLoop中的观察者对象。有人就会问了,这个过程是怎么实现的呢?这个就是观察者者运行机制了,
观察者者运行机制
观察者在底层最终实现的是CFRunLoopObserverRef 对象。该对象将要观察的活动注册到_activities中,而_activities用不同的标识位来表示不同的活动,因此,活动的注册其实是通过操作某些位置实现的,类似于位操作。当一个RunLoop的状态发生变化时,通过回调函数_callout来,通知所有监听这个状态的观察者,这样就实现了RunLoop状态的监听和上报。
下面再介绍下定时器。
定时器
Timer在源码下如下定义:
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
Timer在底层代码中就是一个CFRunLoopTimer对象, 这个对象的运行,本质来说是由XNU 内核的 mk_timer来驱动的,在通过_callout回调实现定时执行任务
简单来说,一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
4. ScrollView滑动,NSTimer就失效的问题分析
先解释下mode的新增和删除方式。对于mode来说,只能通过mode name来操作内部的 mode,当传入一个新的 mode name 但RunLoop 内部没有对应mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef。对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。
在我们ScrollView开发过程中, 在主线程一般在kCFRunLoopDefaultMode下,当ScrollView滑动时,会进入NSEventTrackingRunLoopMode,如果有一个 timer 加入到了 defaultMode 下,那么在滑动时 timer 就会失效,要想在滑动时还能继续使用 timer,可以将 timer 分别加入到这两个 mode,还有一种方式,就是加入到kCFRunLoopCommonModes组中,组中包含的三种 mode 都会同步加入此 timer。
以上就是RunLoop的相关技术介绍,感谢阅读。