欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS触摸点击事件之runloop做了什么?

程序员文章站 2022-08-17 15:10:22
ios触摸点击事件之runloop做了什么? 事件的产生 我们都知道,当点击屏幕时,会产生一个事件,也就是uievent对象 //事件类型 @property(nonatomic,rea...

ios触摸点击事件之runloop做了什么?

事件的产生

我们都知道,当点击屏幕时,会产生一个事件,也就是uievent对象

//事件类型

@property(nonatomic,readonly) uieventtype     type 
@property(nonatomic,readonly) uieventsubtype  subtype 
//事件产生的时间
@property(nonatomic,readonly) nstimeinterval  timestamp;

它承载着事件的类型与事件产生的时间
再来看看事件的类型

typedef ns_enum(nsinteger, uieventtype) {
    uieventtypetouches,
    uieventtypemotion,
    uieventtyperemotecontrol,
};

typedef ns_enum(nsinteger, uieventsubtype) {
    // available in iphone os 3.0
    uieventsubtypenone                              = 0,

    // for uieventtypemotion, available in iphone os 3.0
    uieventsubtypemotionshake                       = 1,

    // for uieventtyperemotecontrol, available in ios 4.0
    uieventsubtyperemotecontrolplay                 = 100,
    uieventsubtyperemotecontrolpause                = 101,
    uieventsubtyperemotecontrolstop                 = 102,
    uieventsubtyperemotecontroltoggleplaypause      = 103,
    uieventsubtyperemotecontrolnexttrack            = 104,
    uieventsubtyperemotecontrolprevioustrack        = 105,
    uieventsubtyperemotecontrolbeginseekingbackward = 106,
    uieventsubtyperemotecontrolendseekingbackward   = 107,
    uieventsubtyperemotecontrolbeginseekingforward  = 108,
    uieventsubtyperemotecontrolendseekingforward    = 109,
};
在这里,可以看出,事件被苹果分为3种大类型:
触摸事件,加速计事件以及远程遥控事件
子类型事件里都是苹果已经封装好,无需我们自己判断的一些事件
这里我们主要还是讨论触摸事件。

事件的传递

事件已经生成了,那谁来处理它呢?
    首先,我们知道事件不是谁都可以处理的
    所以,系统需要找到能处理事件的对象
    系统把事件加入到一个由uiapplication管理的事件队列中
    之所以加入队列而不是栈是因为队列先进先出,意味着先产生的事件,先处理
    然后,事件会按照uiapplication -> uiwindow -> superview -> subview    
    的顺序不断的检测
    而检测就是靠两个方法hittest与pointinside
    那么检测的顺序是什么呢?
    首先,判断窗口能不能处理事件? 如果不能,意味着窗口不是最合适的view,而
      且也不会去寻找比自己更合适的view,直接返回nil,通知uiapplication,没
      有最合适的view。
    当通过hittest检测能够响应事件后,还得知道点在不在自己身上,也就是通过
      pointinside来判断
    当点在自己身上后
    遍历自己的子控件,寻找有没有比自己更合适的view
    如果子控件不接收事件,意味着子控件没有找到最合适的view,然后返回nil,告诉窗
      口没有找到更合适的view,窗口就知道没有比自己更合适的view,就自己处理事件。
    如果子控件接收事件,那么将继续上面的过程,一直找到满足所有条件的控件
    最终把事件交由该控件处理
    整个传递过程就此完结
比较详细的解说这个博客里面有例子和解析。

事件与runloop

我们都知道runloop在没有事件的时候会处于休眠状态
而休眠时,调用的就是下面这个函数:
__cfrunloopservicemachport(waitset, &msg, sizeof(msg_buffer), &liveport) {
                  mach_msg(msg, mach_rcv_msg, port); // thread wait for receive msg
              }
这个函数等待接收mach_port消息
ios中有很多进程通信的方式mach ports,distributed notifications,distributed objects,xpc等等
这个 mach_port就是其中一种
core foundation和foundation为mach端口提供了高级api。在内核基础上封装的cfmachport / nsmachport可以用做runloop源
而这个源,正是我们经常在调用栈里看到的source0与source1
苹果注册了一个 source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __iohideventsystemclientqueuecallback()
当我们触发了事件(触摸/锁屏/摇晃等)后
由iokit.framework生成一个 iohidevent事件
而iokit是苹果的硬件驱动框架
由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件
它专门处理用户交互设备,由iohidservices和iohiddisplays两部分组成
其中iohidservices是专门处理用户交互的,它会将事件封装成iohidevents对象,详细请看这里
然后这些事件又由springboard接收,它只接收收按键(锁屏/静音等),触摸,加速,接近传感器等几种 event
接着用mach port转发给需要的app进程
随后苹果注册的那个 source1 就会触发回调,并调用 _uiapplicationhandleeventqueue()进行应用内部的分发
_uiapplicationhandleeventqueue()把iohidevent处理包装成uievent进行处理分发,我们平时的uigesture/处理屏幕旋转/发送给 uiwindow/uibutton 点击、touchesbegin/move/end/cancel这些事件,都是在这个回调中完成

iOS触摸点击事件之runloop做了什么?点击事件的调用栈.png

在这个方法、函数调用栈中,其他都入前面我们所说
但是细心的人会注意到里面runloop调用的是source0,而不是source1
这个之前我也很费解,后来查资料才知道
首先是由那个source1 接收iohidevent,之后再回调__iohideventsystemclientqueuecallback()内触发的source0,source0再触发的 _uiapplicationhandleeventqueue()。所以uibutton事件看到是在 source0 内的
从别处弄来的部分runloop代码,可以解释mach_msg引起的休眠以及观察者引发的回调,整个runloop的运作过程在里面也有详细的解释
callback()内触发的source0,source0再触发的 _uiapplicationhandleeventqueue()。所以uibutton事件看到是在 source0 内的。

从别处弄来的部分runloop代码,可以解释mach_msg引起的休眠以及观察者引发的回调,整个runloop的运作过程在里面也有详细的解释:

/// 用defaultmode启动
void cfrunlooprun(void) {
    cfrunlooprunspecific(cfrunloopgetcurrent(), kcfrunloopdefaultmode, 1.0e10, false);
}

/// 用指定的mode启动,允许设置runloop超时时间
int cfrunloopruninmode(cfstringref modename, cftimeinterval seconds, boolean stopafterhandle) {
    return cfrunlooprunspecific(cfrunloopgetcurrent(), modename, seconds, returnaftersourcehandled);
}

/// runloop的实现
int cfrunlooprunspecific(runloop, modename, seconds, stopafterhandle) {

    /// 首先根据modename找到对应mode
    cfrunloopmoderef currentmode = __cfrunloopfindmode(runloop, modename, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__cfrunloopmodeisempty(currentmode)) return;

    /// 1. 通知 observers: runloop 即将进入 loop。
    __cfrunloopdoobservers(runloop, currentmode, kcfrunloopentry);

    /// 内部函数,进入loop
    __cfrunlooprun(runloop, currentmode, seconds, returnaftersourcehandled) {

        boolean sourcehandledthisloop = no;
        int retval = 0;
        do {

            /// 2. 通知 observers: runloop 即将触发 timer 回调。
            __cfrunloopdoobservers(runloop, currentmode, kcfrunloopbeforetimers);
            /// 3. 通知 observers: runloop 即将触发 source0 (非port) 回调。
            __cfrunloopdoobservers(runloop, currentmode, kcfrunloopbeforesources);
            /// 执行被加入的block
            __cfrunloopdoblocks(runloop, currentmode);

            /// 4. runloop 触发 source0 (非port) 回调。
            sourcehandledthisloop = __cfrunloopdosources0(runloop, currentmode, stopafterhandle);
            /// 执行被加入的block
            __cfrunloopdoblocks(runloop, currentmode);

            /// 5. 如果有 source1 (基于port) 处于 ready 状态,直接处理这个 source1 然后跳转去处理消息。
            if (__source0diddispatchportlasttime) {
                boolean hasmsg = __cfrunloopservicemachport(dispatchport, &msg)
                if (hasmsg) goto handle_msg;
            }

            /// 通知 observers: runloop 的线程即将进入休眠(sleep)。
            if (!sourcehandledthisloop) {
                __cfrunloopdoobservers(runloop, currentmode, kcfrunloopbeforewaiting);
            }

            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// ? 一个基于 port 的source 的事件。
            /// ? 一个 timer 到时间了
            /// ? runloop 自身的超时时间到了
            /// ? 被其他什么调用者手动唤醒
            __cfrunloopservicemachport(waitset, &msg, sizeof(msg_buffer), &liveport) {
                mach_msg(msg, mach_rcv_msg, port); // thread wait for receive msg
            }

            /// 8. 通知 observers: runloop 的线程刚刚被唤醒了。
            __cfrunloopdoobservers(runloop, currentmode, kcfrunloopafterwaiting);

            /// 收到消息,处理消息。
            handle_msg:

            /// 9.1 如果一个 timer 到时间了,触发这个timer的回调。
            if (msg_is_timer) {
                __cfrunloopdotimers(runloop, currentmode, mach_absolute_time())
            } 

            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __cfrunloop_is_servicing_the_main_dispatch_queue__(msg);
            } 

            /// 9.3 如果一个 source1 (基于port) 发出事件了,处理这个事件
            else {
                cfrunloopsourceref source1 = __cfrunloopmodefindsourceformachport(runloop, currentmode, liveport);
                sourcehandledthisloop = __cfrunloopdosource1(runloop, currentmode, source1, msg);
                if (sourcehandledthisloop) {
                    mach_msg(reply, mach_send_msg, reply);
                }
            }

            /// 执行加入到loop的block
            __cfrunloopdoblocks(runloop, currentmode);


            if (sourcehandledthisloop && stopafterhandle) {
                /// 进入loop时参数说处理完事件就返回。
                retval = kcfrunlooprunhandledsource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retval = kcfrunloopruntimedout;
            } else if (__cfrunloopisstopped(runloop)) {
                /// 被外部调用者强制停止了
                retval = kcfrunlooprunstopped;
            } else if (__cfrunloopmodeisempty(runloop, currentmode)) {
                /// source/timer/observer一个都没有了
                retval = kcfrunlooprunfinished;
            }

            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retval == 0);
    }

    /// 10. 通知 observers: runloop 即将退出。
    __cfrunloopdoobservers(rl, currentmode, kcfrunloopexit);
}

小结

上面这些其实要挖可以挖得很深,但是在实际开发中并不会用到,只要了解在我们触发事件时,从我们平时用的api,到系统的runloop,再到一些底层发生了什么,科普一下就好。