iOS触摸点击事件之runloop做了什么?
程序员文章站
2022-04-30 19:57:50
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这些事件,都是在这个回调中完成
点击事件的调用栈.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,再到一些底层发生了什么,科普一下就好。