RunLoop

459 阅读9分钟
  •  每条线程都有唯一的一个与之对应的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

  1. 触摸事件处理
  2. performSelector:onThread

Source1

  1. 基于Port的线程间通信
  2. 系统事件捕捉(屏幕上点击事件,先是通过sourc1捕获,然后分发到source0处理)

Timers

  1. NSTimer
  2. performSelector:withObject:afterDelay:

Observers

  1. 用于监听RunLoop的状态
  2. UI刷新(BeforeWaiting)
  3. Autorelease pool(也是在runloop即将睡眠之前释放对象)

RunLoop在实际开发中的应用

控制线程生命周期(线程保活,AFNetworking)

解决NSTimer在滑动时停止工作的问题

监控应用卡顿

性能优化

触摸事件是如何从屏幕转移到APP内的?

触摸事件从触屏产生后,由IOKit将事件传递给SpringBoard进程,再由SpringBoard进程分发给当前APP处理

系统响应阶段

  1. 手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理
  2. IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoard进程

  mach port 进程端口,各进程之间通过它进行通信。  
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的 触摸事件。

  1. SpringBoard进程因接收到触摸事件,触发了主线程runloop的source1事件源的回调

      此时SpringBoard根据桌面的状态,判断应该由谁处理此次触摸事件,因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无APP运行),则触发SpringBoard本身主线程runloop的source0事件源的回调,将事件交由桌面系统去消耗;若是后者(即有app正在前台运行),则将触摸事件通过IPC传递给前台APP进程,接下来的事情便是APP内部对于触摸事件的响应了。

APP响应阶段

  1. APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的Runloop被唤醒,触发了Source1回调

  2. source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应

  3. source0回调内部将触摸事件添加到UIApplication对象的事件队列中,事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing

  4. 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应

  5. 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。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优先级高)。

具体流程:

  1. UIApplication首先将事件传递给窗口对象(UIWindow),若存在多个窗口,则优先询问后显示的窗口。

  2. 若窗口不能响应事件,则将事件传递其他窗口;若窗口能响应事件,则从后往前询问窗口的子视图。

  3. 重复步骤2。即视图若不能响应,则将事件传递给上一个同级子视图;若能响应,则从后往前询问当前视图的子视图。

  4. 视图若没有能响应的子视图了,则自身就是最合适的响应者。

示例:

视图层级如下(同一层级的视图越在下面,表示越后添加):

A
├── B
│   └── D
└── C
    ├── E
    └── F

现在假设在E视图所处的屏幕位置触发一个触摸,应用接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:

  1. UIWindow将事件传递给其子视图A

  2. A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。

  3. C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。

  4. F判断自身不能响应事件,C又将事件传递给E。

  5. 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;
}