您好,欢迎来电子发烧友网! ,新用户?[免费注册]

您的位置:电子发烧友网>源码下载>通讯/手机编程>

关于iOS的事件处理机制解析

大小:0.3 MB 人气: 2017-09-26 需要积分:1

  RunLoop主要处理以下6类事件:

  static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(); static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

  Observer事件,runloop中状态变化时进行通知。(微信卡顿监控就是利用这个事件通知来记录下最近一次main runloop活动时间,在另一个check线程中用定时器检测当前时间距离最后一次活动时间过久来判断在主线程中的处理逻辑耗时和卡主线程)。这里还需要特别注意,CAAnimation是由RunloopObserver触发回调来重绘,接下来会讲到。

  Block事件,非延迟的NSObject PerformSelector立即调用,dispatch_after立即调用,block回调。

  Main_Dispatch_Queue事件:GCD中dispatch到main queue的block会被dispatch到main loop执行。

  Timer事件:延迟的NSObject PerformSelector,延迟的dispatch_after,timer事件。

  Source0事件:处理如UIEvent,CFSocket这类事件。需要手动触发。触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。

  Source1事件:处理系统内核的mach_msg事件。(推测CADisplayLink也是这里触发)。

  RunLoop执行顺序的伪代码

  SetupThisRunLoopRunTimeoutTimer(); // by GCD timer //通知即将进入runloop__CFRUNLLOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(KCFRunLoopEntry); do { __CFRunLoopDoObservers(kCFRunLoopBeforeTimers); __CFRunLoopDoObservers(kCFRunLoopBeforeSources); __CFRunLoopDoBlocks(); //一个循环中会调用两次,确保非延迟的NSObject PerformSelector调用和非延迟的dispatch_after调用在当前runloop执行。还有回调block __CFRunLoopDoSource0(); //例如UIKit处理的UIEvent事件 CheckIfExistMessagesInMainDispatchQueue(); //GCD dispatch main queue __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting); //即将进入休眠,会重绘一次界面 var wakeUpPort = SleepAndWaitForWakingUpPorts(); // mach_msg_trap,陷入内核等待匹配的内核mach_msg事件 // Zzz.。。 // Received mach_msg, wake up __CFRunLoopDoObservers(kCFRunLoopAfterWaiting); // Handle msgs if (wakeUpPort == timerPort) { __CFRunLoopDoTimers(); } else if (wakeUpPort == mainDispatchQueuePort) { //GCD当调用dispatch_async(dispatch_get_main_queue(),block)时,libDispatch会向主线程的runloop发送mach_msg消息唤醒runloop,并在这里执行。这里仅限于执行dispatch到主线程的任务,dispatch到其他线程的仍然是libDispatch来处理。 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() } else { __CFRunLoopDoSource1(); //CADisplayLink是source1的mach_msg触发? } __CFRunLoopDoBlocks(); } while (!stop && !timeout); //通知observers,即将退出runloop __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBERVER_CALLBACK_FUNCTION__(CFRunLoopExit);

  结合上面的Runloop事件执行顺序,思考下面代码逻辑中为什么可以标识tableview是否reload完成

  dispatch_async(dispatch_get_main_queue(), ^{ _isReloadDone = NO; [tableView reload]; //会自动设置tableView layoutIfNeeded为YES,意味着将会在runloop结束时重绘table dispatch_async(dispatch_get_main_queue(),^{ _isReloadDone = YES; }); });

  提示:这里在GCD dispatch main queue中插入了两个任务,一次RunLoop有两个机会执行GCD dispatch main queue中的任务,分别在休眠前和被唤醒后。

  iOS 为什么必须在主线程中操作UI

  因为UIKit不是线程安全的。试想下面这几种情况:

  两个线程同时设置同一个背景图片,那么很有可能因为当前图片被释放了两次而导致应用崩溃。

  两个线程同时设置同一个UIView的背景颜色,那么很有可能渲染显示的是颜色A,而此时在UIView逻辑树上的背景颜色属性为B。

  两个线程同时操作view的树形结构:在线程A中for循环遍历并操作当前View的所有subView,然后此时线程B中将某个subView直接删除,这就导致了错乱还可能导致应用崩溃。

  iOS4之后苹果将大部分绘图的方法和诸如 UIColor 和 UIFont 这样的类改写为了线程安全可用,但是仍然强烈建议讲UI操作保证在主线程中执行。

  事件响应

  苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

  当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。

  SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

  _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

  CALayer

  在iOS当中,所有的视图都从一个叫做UIVIew的基类派生而来,UIView可以处理触摸事件,可以支持基于Core Graphics绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。

  CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView最大的不同是CALayer不处理用户的交互。CALayer并不清楚具体的响应链。

  UIView和CALayer是一个平行的层级关系,每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作。实际上这些背后关联的Layer图层才是真正用来在屏幕上显示和做动画,UIView仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口

  UIView 的 Layer 在系统内部,被维护着三份同样的树形数据结构,分别是:

  图层树(这里是代码可以操纵的,设置属性的最终值会立刻在这里更新);

  呈现树(是一个中间层,系统就在这一层上更改属性,进行各种渲染操作。比如一个动画是更改alpha值从0到1,那么在逻辑树上此属性会被立刻更新为最终属性1,而在动画树上会根据设置的动画时间从0逐步变化到1);

  渲染树(其属性值就是当前正被显示在屏幕上的属性值);

  CADisplayLink 和 NSTimer

  NSTimer 其实就是 CFRunLoopTimerRef。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。

  RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。

  RunLoop 是用GCD的 dispatch_source_t 实现的 Timer。 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

  CADisplayLink 是一个和屏幕刷新率(每秒刷新60次)一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,造成界面卡顿的感觉。

  iOS 渲染过程

  关于iOS的事件处理机制解析

  图2-1

  通常来说,计算机系统中 CPUGPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号如下图1-4所示,逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

  关于iOS的事件处理机制解析

  图2-2

  在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。从上图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

  关于iOS的事件处理机制解析

  iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

非常好我支持^.^

(0) 0%

不好我反对

(0) 0%

      发表评论

      用户评论
      评价:好评中评差评

      发表评论,获取积分! 请遵守相关规定!