当前位置: 移动技术网 > IT编程>移动开发>IOS > iOS触摸点击事件之runloop做了什么?

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

2018年10月14日  | 移动技术网IT编程  | 我要评论

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这些事件,都是在这个回调中完成

这里写图片描述点击事件的调用栈.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,再到一些底层发生了什么,科普一下就好。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

  • ios uicollectionview实现横向滚动

    现在使用卡片效果的app很多,之前公司让实现一种卡片效果,就写了一篇关于实现卡片的文章。文章最后附有demo实现上我选择了使用uicollectionview ... [阅读全文]
  • iOS UICollectionView实现横向滑动

    本文实例为大家分享了ios uicollectionview实现横向滑动的具体代码,供大家参考,具体内容如下uicollectionview的横向滚动,目前我使... [阅读全文]
  • iOS13适配深色模式(Dark Mode)的实现

    iOS13适配深色模式(Dark Mode)的实现

    好像大概也许是一年前, mac os系统发布了深色模式外观, 看着挺刺激, 时至今日用着也还挺爽的终于, 随着iphone11等新手机的发售, ios 13系统... [阅读全文]
  • ios 使用xcode11 新建项目工程的步骤详解

    ios 使用xcode11 新建项目工程的步骤详解

    xcode11新建项目工程,新增了scenedelegate这个类,转而将原appdelegate负责的对ui生命周期的处理担子接了过来。故此可以理解为:ios... [阅读全文]
  • iOS实现转盘效果

    本文实例为大家分享了ios实现转盘效果的具体代码,供大家参考,具体内容如下demo下载地址: ios转盘效果功能:实现了常用的ios转盘效果,轮盘抽奖效果的实现... [阅读全文]
  • iOS开发实现转盘功能

    本文实例为大家分享了ios实现转盘功能的具体代码,供大家参考,具体内容如下今天给同学们讲解一下一个转盘选号的功能,直接上代码直接看viewcontroller#... [阅读全文]
  • iOS实现轮盘动态效果

    本文实例为大家分享了ios实现轮盘动态效果的具体代码,供大家参考,具体内容如下一个常用的绘图,主要用来打分之类的动画,效果如下。主要是ios的绘图和动画,本来想... [阅读全文]
  • iOS实现九宫格连线手势解锁

    本文实例为大家分享了ios实现九宫格连线手势解锁的具体代码,供大家参考,具体内容如下demo下载地址:效果图:核心代码://// clockview.m// 手... [阅读全文]
  • iOS实现卡片堆叠效果

    本文实例为大家分享了ios实现卡片堆叠效果的具体代码,供大家参考,具体内容如下如图,这就是最终效果。去年安卓5.0发布的时候,当我看到安卓全新的material... [阅读全文]
  • iOS利用余弦函数实现卡片浏览工具

    iOS利用余弦函数实现卡片浏览工具

    本文实例为大家分享了ios利用余弦函数实现卡片浏览工具的具体代码,供大家参考,具体内容如下一、实现效果通过拖拽屏幕实现卡片移动,左右两侧的卡片随着拖动变小,中间... [阅读全文]
验证码:
移动技术网