-
每条线程都有唯一的一个与之对应的RunLoop对象
-
RunLoop保存在一个全局的NSDictionary里,线程作为key,RunLoop作为Value
-
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
-
RunLoop会在线程结束时销毁 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
RunLoop相关的类
-
CFRunLoopRef
-
CFRunLoopModeRef
-
CFRunLoopSourceRef
-
CFRunLoopTimerRef
-
CFRunLoopObserverRef
- CFRunLoopModeRef代表RunLoop的运行模式
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
- RunLoop启动时只能选择其中一个Mode,作为CurrentMode
- 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
- 不同组的Source0/Source1/Timer/Observer能分离开,互不影响
- 如果Mode里面没有任何Source0/Source1/Timer/Observer,RunLoop会立刻退出
Source0
- 触摸事件处理
- performSelector:onThread
Source1
- 基于Port的线程间通信
- 系统事件捕捉(屏幕上点击事件,先是通过sourc1捕获,然后分发到source0处理)
Timers
- NSTimer
- performSelector:withObject:afterDelay:
Observers
- 用于监听RunLoop的状态
- UI刷新(BeforeWaiting)
- Autorelease pool(也是在runloop即将睡眠之前释放对象)
RunLoop在实际开发中的应用
控制线程生命周期(线程保活,AFNetworking)
解决NSTimer在滑动时停止工作的问题
监控应用卡顿
性能优化
触摸事件是如何从屏幕转移到APP内的?
触摸事件从触屏产生后,由IOKit将事件传递给SpringBoard进程,再由SpringBoard进程分发给当前APP处理
系统响应阶段
- 手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理
- IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoard进程
mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的 触摸事件。
- SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调
此时SpringBoard根据桌面的状态,判断应该由谁处理此次触摸事件,因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。
APP响应阶段
-
APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的Runloop被唤醒,触发了Source1回调
-
source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应
-
source0回调内部将触摸事件添加到UIApplication对象的事件队列中,事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing
-
寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应
-
触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒
触摸、事件、响应者
UITouch
-
一个手指一次触摸屏幕,就对应生成一个UITouch对象。多个手指同时触摸,生成多个UITouch对象。
-
多个手指先后触摸,系统会根据触摸的位置判断是否更新同一个UITouch对象。若两个手指一前一后触摸同一个位置(即双击),那么第一次触摸时生成一个UITouch对象,第二次触摸更新这个UITouch对象(UITouch对象的 tap count 属性值从1变成2);若两个手指一前一后触摸的位置不同,将会生成两个UITouch对象,两者之间没有联系。
-
每个UITouch对象记录了触摸的一些信息,包括触摸时间、位置、阶段、所处的视图、窗口等信息。
//触摸的各个阶段状态 //例如当手指移动时,会更新phase属性到UITouchPhaseMoved;手指离屏后,更新到UITouchPhaseEnded typedef NS_ENUM(NSInteger, UITouchPhase) { UITouchPhaseBegan, // whenever a finger touches the surface. UITouchPhaseMoved, // whenever a finger moves on the surface. UITouchPhaseStationary, // whenever a finger is touching the surface but hasn't moved since the previous event. UITouchPhaseEnded, // whenever a finger leaves the surface. UITouchPhaseCancelled, // whenever a touch doesn't end but we need to stop tracking (e.g. putting device to face) };
-
手指离开屏幕一段时间后,确定该UITouch对象不会再被更新将被释放。
UIEvent
- 触摸的目的是生成触摸事件供响应者响应,一个触摸事件对应一个UIEvent对象,其中的 type 属性标识了事件的类型(之前说过事件不只是触摸事件)。
- UIEvent对象中包含了触发该事件的触摸对象的集合,因为一个触摸事件可能是由多个手指同时触摸产生的。触摸对象集合通过 allTouches 属性获取。
UIResponder
每个响应者都是一个UIResponder对象,即所有派生自UIResponder的对象,本身都具备响应事件的能力。因此以下类的实例都是响应者:
- UIView
- UIViewController
- UIApplication
- AppDelegate
响应者之所以能响应事件,因为其提供了4个处理触摸事件的方法:
//手指触碰屏幕,触摸开始
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指在屏幕上移动
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//手指离开屏幕,触摸结束
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
//触摸结束前,某个系统事件中断了触摸,例如电话呼入
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
这几个方法在响应者对象接收到事件的时候调用,用于做出对事件的响应
寻找事件的最佳响应者(Hit-Testing)
APP接收到触摸事件后,会被放入当前应用的一个事件队列中(PS为什么是队列而不是栈?很好理解因为触摸事件必然是先发生先执行,切合队列FIFO的原则)。
每个事件的理想宿命是被能够响应它的对象响应后释放,然而响应者诸多,事件一次只有一个,谁都想把事件抢到自己碗里来,为避免纷争,就得有一个先后顺序,也就是得有一个响应者的优先级。因此这就存在一个寻找事件最佳响应者(又称第一响应者 first responder)的过程,目的是找到一个具备最高优先级响应权的响应对象(the most appropriate responder object),这个过程叫做Hit-Testing,那个命中的最佳响应者称为hit-tested view。
事件自下而上的传递
应用接收到事件后先将其置入事件队列中以等待处理.出队后,application首先将事件传递给当前应用最后显示的窗口UIWindow)询问其能否响应事件.若窗口能响应事件,则传递给子视图询问是否能响应,子视图若能响应则继续询问子视图。子视图询问的顺序是优先询问后添加的子视图,即子视图数组中靠后的视图。事件传递顺序如下:
UIApplication ——> UIWindow ——> 子视图 ——> ... ——> 子视图
事实上把UIWindow也看成是视图即可,这样整个传递过程就是一个递归询问子视图能否响应事件过程,且后添加的子视图优先级高(对于window而言就是后显示的window优先级高)。
具体流程:
-
UIApplication首先将事件传递给窗口对象(UIWindow),若存在多个窗口,则优先询问后显示的窗口。
-
若窗口不能响应事件,则将事件传递其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。
-
重复步骤2。即视图若不能响应,则将事件传递给上一个同级子视图;若能响应,则从后往前询问当前视图的子视图。
-
视图若没有能响应的子视图了,则自身就是最合适的响应者。
示例:
视图层级如下(同一层级的视图越在下面,表示越后添加):
A
├── B
│ └── D
└── C
├── E
└── F
现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:
-
UIWindow将事件传递给其子视图A
-
A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
-
C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
-
F判断自身不能响应事件,C又将事件传递给E。
-
E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。
Hit-Testing的本质
首先要知道的是,以下几种状态的视图无法响应事件:
- 不允许交互:userInteractionEnabled = NO
- 隐藏:hidden = YES 如果父视图隐藏,那么子视图也会隐藏,隐藏的视图无法接收事件
- 透明度:alpha < 0.01 如果设置一个视图的透明度<0.01,会直接影响子视图的透明度。alpha:0.0~0.01为透明。
hitTest:withEvent:
每个UIView对象都有一个 hitTest:withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。
hitTest:withEvent: 方法返回一个UIView对象,作为当前视图层次中的响应者。默认实现是:
- 若当前视图无法响应事件,则返回nil
- 若当前视图可以响应事件,但无子视图可以响应事件,则返回自身作为当前视图层次中的事件响应者
- 若当前视图可以响应事件,同时有子视图可以响应,则返回子视图层次中的事件响应者
一开始UIApplication将事件通过调用UIWindow对象的 hitTest:withEvent: 传递给UIWindow对象,UIWindow的 hitTest:withEvent: 在执行时若判断本身能响应事件,则调用子视图的 hitTest:withEvent: 将事件传递给子视图并询问子视图上的最佳响应者。最终UIWindow返回一个视图层次中的响应者视图给UIApplication,这个视图就是hit-testing的最佳响应者。
系统对于视图能否响应事件的判断逻辑除了之前提到的3种限制状态,默认能响应的条件就是触摸点在当前视图的坐标系范围内。因此,hitTest:withEvent: 的默认实现就可以推测了,大致如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
//3种状态无法响应事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event] == NO) return nil;
//从后往前遍历子视图数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--)
{
// 获取子视图
UIView *childView = self.subviews[i];
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint childP = [self convertPoint:point toView:childView];
//询问子视图层级中的最佳响应视图
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView)
{
//如果子视图中有更合适的就返回
return fitView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}