23、RunLoop学习

RunLoop的定义

RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。

维护的事件循环可以用来不断的处理消息/事件,对它们进行管理。当没有消息需要处理时,会发生从用户态到内核态的切换,休眠以避免资源占用。在有消息需要处理时,会发生从内核态到用户态的切换,当前的用户线程会被唤醒。

它不是简单的while循环或者for循环。

一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop让线程能随时处理事件但并不退出。

用户态和内核态

我们的应用程序一般是运行在用户态上的,当我们发生系统调用,需要使用系统命令或者底层内核命令时就触发了一个系统调用,有些系统调用就会发生一个状态空间的切换,内核态的目的就是要统一调度和管理计算机资源。如果每个app都能促使手机关机,这种场景是无法想象的,所以要有用户态到内核态的区分。

内核态就是拥有资源多的状态,或者说访问资源多的状态,我们也称之为特权态。相对来说,用户态就是非特权态,在此种状态下访问的资源将受到限制。如果一个程序运行在特权态,则该程序就可以访问计算机的任何资源,即它的资源访问权限不受限制。如果一个程序运行在用户态,则其资源需求将受到各种限制。

RunLoop的作用

1、保证程序不退出
2、监听事件,有任务的时候执行任务,没有任务的时间休眠以避免资源占用

RunLoop与线程的关系

1、线程是和RunLoop一一对应的。自己创建的线程默认是没有RunLoop的,我们需要自己创建和开启。
2、子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建
3、RunLoop会在线程销毁时销毁
4、RunLoop并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop

为什么main函数可以保持一直运行不退出

因为在main函数中会调用的UIApplicationMain函数,UIApplicationMain函数内部会启动主线程的RunLoop,RunLoop是对运行循环的维护机制,它可以做到有事情的时候做事,没有事情做的时候就休眠。在有消息需要处理时,会发生从内核态到用户态的切换,当前的用户线程会被唤醒。没事情的做的时候通过从用户态到内核态的切换达到避免资源浪费的目的,此时线程处于休眠状态。

RunLoop主要接口

@interface NSRunLoop : NSObject {

@property (class, readonly, strong) NSRunLoop *currentRunLoop;
@property (class, readonly, strong) NSRunLoop *mainRunLoop API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

@property (nullable, readonly, copy) NSRunLoopMode currentMode;

- (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED;

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;

- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;

- (nullable NSDate *)limitDateForMode:(NSRunLoopMode)mode;
- (void)acceptInputForMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;

@end

RunLoop数据结构

在OC里面为我们提供了两个RunLoop:NSRunLoop和CFRunLoop,NSRunLoop是对CFRunLoop的封装,提供了面向对象的API。它们分别位于Foundation框架和CoreFoundation框架中。

RunLoop数据结构:

(1)CFRunLoop:代表RunLoop对象

(2)CFRunLoopMode:代表runLoop的运行模式

(3)CFRunLoopSource:RunLoop 的输入源 / 事件源
(4)CFRunLoopTimer:RunLoop 的定时源
(5)CFRunLoopObserver:RunLoop的观察者,能够监听RunLoop的状态改变。

CFRunLoop

CFRunLoop的成员变量主要包含了pthread、currentMode、modes、commonModes、commonModeItems。

  • pthread代表了线程,runLoop和线程是一一对应的关系。
  • currentMode是CFRunLoopMode的数据结构
  • modes是一个NSMutableSet
  • commonModes是一个NSMutableSet
  • commonModeItems也是一个集合,这个集合包含多个元素,包括多个Observer、多个Timer、多个Source。

CFRunLoopMode

CFRunLoopMode的成员变量主要是namesources0sources1observerstimers

name:就是mode的名称,比如NSDefaultRunLoopMode,是一个字符串的别称。

sources0sources1都是集合,而observerstimers都是数组

RunLoop有不同的Mode(RunLoop模式),在同一时间只能在一种特定的Mode下运行,每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换Mode,需要先停止RunLoop,修改RunLoopMode,再重新启动RunLoop.这样做的主要目的是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

系统定义的Mode

  • NSDefaultRunLoopMode: :系统默认的mode,通常主线程在这个Mode下运行
  • UITrackingRunLoopMode:界面追踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响。
  • NSRunLoopCommonModes:它并不是一个实际存在的一种Mode,它是同步Source/Timer/Observer到多个Mode中的一个解决方案
  • UIInitializationRunLoopMode:在刚启动程序时进入的第一个mode,是一个私有的mode,苹果没有对外开放,启动后就不在使用
  • GSEventReceiveRunLoopMode:接受系统事件的内部的Mode,这个Mode由GraphicsServices调用在CFRunLoopRunSpecific

CFRunLoopSource

source是RunLoop的数据源(输入源)的抽象类(protocol)

RunLoop定义了两个不同的Source

source0 : 处理App内部事件,App自己负责管理(触发),需要手动唤醒线程,如UIEvent、CFSocket

source1 : 由RunLoop和内核管理,Mach Port(进程间通讯端口)驱动,具备唤醒线程的能力,如CFMachPort、CFMessagePort

CFRunLoopTimer

基于事件的定时器,和平时使用的NSTimer是可以进行桥接转换的。

CFRunLoopObserver

观察者,可以用于监听Runloop的状态,状态有以下6种:

  • kCFRunLoopEntry:RunLoop准备启动的时候,系统会给我们一个回调通知
  • kCFRunLoopBeforeTimers:将要处理timer相关事件了
  • kCFRunLoopBeforceSources:将要处理sources相关事件了
  • kCFRunLoopBeforeWaiting:将要进入休眠,这个观测点是非常重要的,即将要发生用户态到内核态的切换
  • kCFRunLoopAfterWaiting:休眠即将结束,发生在内核态切换到用户态不久
  • kCFRunLoopExit:RunLoop退出的通知
    通过以下方式添加观察者
let block = { (ob: CFRunLoopObserver?, ac: CFRunLoopActivity) in
                if ac ==.entry {
                    NSLog("进入 Runlopp")
                }
                else if ac ==.beforeTimers {
                    NSLog("即将处理 Timer 事件")
                }
                else if ac ==.beforeSources {
                    NSLog("即将处理 Source 事件")
                }
                else if ac ==.beforeWaiting {
                    NSLog("Runloop 即将休眠")
                }
                else if ac ==.afterWaiting {
                    NSLog("Runloop 被唤醒")
                }
                else if ac ==.exit {
                    NSLog("退出 Runloop")
                }
            }
let ob = try createRunloopObserver(block: block)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), ob,.defaultMode)

也可以使用

CFRunLoopObserverCreateWithHandler(_ allocator: CFAllocator!,
                                    _ activities: CFOptionFlags,
                                    _ repeats: Bool,
                                    _ order: CFIndex,
                                    _ block: ((CFRunLoopObserver?, CFRunLoopActivity) -> Void)!) -> CFRunLoopObserver!

RunLoop事件循环的实现机制

在这里插入图片描述

点击一个app图标打开app到app杀死,经历了哪些过程

当我们点击图标之后会调用main函数,调用main函数时会调用UIApplicationMain函数,在这个函数内部会启动主线程的RunLoop,然后经过一系列的处理,主线程的RunLoop处于休眠状态,如果此时点击一个屏幕,会产生一个Mach port的Source1事件,会把我们的主线程唤醒,处理唤醒时收到的消息。当我们把程序杀死时会执行一个RunLoop退出机制,这个时候也会发送一个通知,即将退出RunLoop,RunLoop退出之后线程也就销毁掉了

滑动TableView的时候,我们的定时器还会生效吗?

我们的UITableView在正常情况下是运行在NSDefaultRunLoopMode下的,当我们进行滑动的时候,会进行一个Mode的切换,切换到UITrackingRunLoopMode上面。执行的也是UITrackingRunLoopMode下的任务,而timer默认是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer.

解决办法

可以把timer添加到我们当前线程的commonMode模式下,commonMode不是一个实际的mode,只是把一些mode打上一个commonMode标记,我们可以把一个事件源(Timer)同步到多个mode上面。

var timer = Timer(timeInterval: 1.0, target: self, selector: #selector(test), userInfo: nil, repeats: true)
RunLoop.current.add(timer, forMode: .commonModes)
timer.fire()

我们在子线程中使用timer也可以解决这个问题,但是需要运行runloop,否则,timer仅仅执行一次。
每一个线程都有一个对应的runloop,每个runloop可能会有多个mode.CPU会在多个线程间切换来执行任务,呈现出多个线程同时执行的效果。执行的任务其实就是RunLopp去各个Mode里执行各个Item。因为两个runloop是相互独立的,不会相互影响,所以在子线程添加timer,滑动视图,timer能正常运行。

subThread = Thread(target: self, selector:#selector(createTimer), object: nil)
subThread?.start()

@objc private func createTimer() {
    let runLoop = RunLoop.current //获取子线程的RunLoop
    Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(test), userInfo: nil, repeats: true)
    runLoop.run() //开启子线程的runloop
}

RunLoop的应用

后台常驻线程(很常用)

我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。
那么怎么做呢?
添加一条强引用的子线程,在该线程的RunLoop下添加一个Sources(比如NSPort),开启RunLoop。

具体实现过程如下:
1、在项目的ViewController.m中添加一条强引用的thread线程属性

@interface ViewController ()

@property (strong, nonatomic) NSThread *thread;

@end

2、在viewDidLoad中创建线程self.thread,使线程启动并执行run1方法。

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建线程,并调用run1方法执行任务
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    // 开启线程
    [self.thread start];    
}

- (void) run1
{
    // 这里写任务
    NSLog(@"----run1-----");

    // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"未开启RunLoop");
}

运行之后发现打印了——run1——-,而未开启RunLoop则未打印。
这时,我们就开启了一条常驻线程,下边我们来试着添加其他任务,除了之前创建的时候调用了run1方法,我们另外在点击的时候调用run2方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{   
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) run2
{
    NSLog(@"----run2------");
}

经过运行测试,除了之前打印的——run1——-,每当我们点击屏幕,都能调用——run2———。
这样我们就实现了常驻线程的需求。

#import "MCObject.h"

@implementation MCObject

static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;

+ (NSThread *)threadForDispatch{
    if (thread == nil) {
        @synchronized(self) { //线程安全的方式创建thread
            if (thread == nil) {
                // 线程的创建
                thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                [thread setName:@"com.imooc.thread"];
                //启动
                [thread start];
            }
        }
    }
    return thread;
}

+ (void)runRequest
{
    // 创建一个Source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);


    /*
    CFRunLoopGetCurrent函数会为当前线程创建一个RunLoop,这一操作是由系统来实现的。在我们第一次调用的时候系统会把RunLoop给创建出来,然后我们才可以使用RunLoop.
    对于主线程来说,系统早就帮我们创建了好了一个主运行循环,我们在使用CFRunLoopGetCurrent的时候要注意当前试下 哪个线程中
    */
    // 创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

    /*
    通过一个While循环维持RunLoop的事件循环
    */
    // 如果可以运行
    while (runAlways) {
        @autoreleasepool { //达到每次运行循环达到一圈的时候,对内存进行释放
            // 令当前RunLoop运行在DefaultMode下面
            /*
            我们运行的Mode要和上面添加资源的Mode保持一致,否则你把RunLoop运行在另外一个Mode上面,是没有办法维持RunLoop进行的,这可能会导致while的死循环。实际上是这个函数内部会调用Match Message发生一个有用户态和内核态的切换,我们当前线程就进入休眠状态,所以这个whil循环就停在这里了。
            第二个参数是指这个RunLoop循环运行到指定事件退出,这个1.0e10表示未来的一个无限大的时间
            第三个参数是资源处理后是否立刻返回
            */
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }

    // 某一时机 静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出
    //当一个RunLoop的Mode里面没有对应的事件源要处理的时候,RunLoop就会自动退出。
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}

@end

RunLoop解决tableView滚动停止时加载图片

有时候有大量高清图片在加载,在滑动的时候,请求网络,下载完图片之后设置的时候会很卡,往常的解决方案是添加delegate,检测什么时候滑动结束去设置图片。

也可以通过Runloop解决,在defaultMode模式下设置图片,在滑动tableView的时候,主线程是在UITrackingRunLoopMode模式下工作的,滑动结束时会切换到defaultMode模式下。

avatarImageView.perform(#selector(setImage:), with: downloadImage, afterDelay: 0, inModes: [RunLoopMode.defaultRunLoopMode])

RunLoop解决在tableView中同时加载多个大图的使用

runloop中最先响应的是UI绘制,tableView在滑动时就需要在一次runloop循环中绘制所有的屏幕上的图片,如果在cellForxxx方法中有imageView.image = image等代码,这部分会非常消耗资源(尤其图片较大的时候),一次runloop执行时间太长,就会导致UI响应变慢,直观感受就是app卡

解决思路:

每次runloop只绘制一张图片,这样runloop需要完成的内容少,UI才不会卡顿

注意:图片优化有更好的方法,这里只是提供了一张使用RunLoop进行优化的思路。

实现思路:

(1) 把加载图片等代码保存起来,先不执行

(2) 监听RunLoop循环(CFRunLoopObserver),等到runLoop空闲的时候

(3) 每次都从任务数组中取出一个加载图片等代码执行

当滑动tableView的时候不加载图片,当tableView滑动结束,进入beforeWaiting状态,即将休眠时,加载显示图片,也就是滑动即将结束的时候加载图片。

import UIKit

typealias RunloopBlock = (()->Bool)

class LCARunLoop {

    var maxQueue:Int = 18
    
    private var timer:Timer?
    private var observer:CFRunLoopObserver?
    private var tasks:[RunloopBlock] = []

    static let shared:LCARunLoop =  {
        let shared = LCARunLoop()
        return shared
    }()

    func start() {
        tasks = []
        
        timer?.invalidate()
        timer = nil
        timer = Timer.scheduledTimer(timeInterval: 0.001, target: self, selector: #selector(LCARunLoop.nothing), userInfo: nil, repeats: true)
        addRunLoopObserver()
    }
    func invalidate() {
        timer?.invalidate()
        timer = nil
        tasks.removeAll()
    }
    func addTask(task:@escaping RunloopBlock){
        self.tasks.append(task)
        if self.tasks.count > self.maxQueue {
            self.tasks.removeFirst(1)
        }
    }

    /// 添加监听
    func addRunLoopObserver() {
        //获取当前的runloop
        let current = CFRunLoopGetCurrent()
        //创建一个观察者
        /*
        allocator:该参数为对象内存的分配器,一般使用默认的分配器kCFAllocatorDefault或者nil
        activities:该参数配置观察者监听runloop的哪种运行状态,在这里我们监听runLoop的所有运行状态
        repeats:该参数标识观察者只监听一次还是每次runloop运行时都监听
        order:观察者优先级,当runloop中有多个观察者监听同一个运行状态时,那么根据该优先级判断,0为最高优先级
        callout:观察者的回调函数,在Core Foundation框架中用CFRunLoopObserverCallBack重定义回调函数的闭包
        context:观察者的上下文。 (类似与KVO传递的context,可以传递信息,)因为这个函数创建ovserver的时候需要传递进一个函数指针,而这个函数指针可能用在n多个oberver 可以当做区分是哪个observer的状机态。(下面的通过block创建的observer一般是一对一的,一般也不需要Context,),还有一个例子类似与NSNOtificationCenter的 SEL和 Block方式。
        */
        /*
        /* Run Loop Observer Activities */
        public struct CFRunLoopActivity : OptionSet {
            public init(rawValue: CFOptionFlags)
            即将进入runloop
            public static var entry: CFRunLoopActivity { get }
            即将处理timer
            public static var beforeTimers: CFRunLoopActivity { get }
            即将处理source
            public static var beforeSources: CFRunLoopActivity { get }
            即将休眠
            public static var beforeWaiting: CFRunLoopActivity { get }
            休眠结束,被唤醒
            public static var afterWaiting: CFRunLoopActivity { get }
            runloop退出
            public static var exit: CFRunLoopActivity { get }
            所有活动
            public static var allActivities: CFRunLoopActivity { get }
        }
        */
        
        var context = CFRunLoopObserverContext(version: 0, info: nil,retain: nil, release: nil, copyDescription: nil)
        context.info = Unmanaged.passUnretained(self).toOpaque()
        observer = CFRunLoopObserverCreate(kCFAllocatorDefault, CFRunLoopActivity.beforeWaiting.rawValue | CFRunLoopActivity.exit.rawValue, true, 0xFFFFFF, observerCallBack,&context)
        CFRunLoopAddObserver(current, observer, CFRunLoopMode.defaultMode)
    }

    private init(){
    
    }

    @objc private func nothing() {
    
    }

    private let observerCallBack:CFRunLoopObserverCallBack = {_,_,info in
        if let info = info {
            let runLoop = Unmanaged<LCARunLoop>.fromOpaque(info).takeUnretainedValue()
            if runLoop.tasks.count == 0 {
                return
            }
            var result = false
            while(result == false && runLoop.tasks.count != 0) {
                let task = runLoop.tasks.first
                result = task?() ?? false
                runLoop.tasks.removeFirst(1)
            }
        }
    }
}
LCARunLoop.shared.start()

LCARunLoop.shared.addTask {
    if cell.indexPath == indexPath {
        (cell as? MorePicCell)?.setIcon()
        return true
    }else{
        return false
    }
}

RunLoop监控App卡顿,并查找具体卡顿函数的方法

主线程的RunLoop是在应用启动时自动开启的,也没有超时时间,所以正常情况下,主线程的RunLoop只会在2-9之间无限循环下去。
我们只需要在主线程的RunLoop中添加一个observer,检测从kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting花费的时间是否过长。如果花费的时间大于某一个阈值,我们就认为有卡顿,并把当前的线程堆栈信息转存到文件中,并在以后某个合适的时间,将卡顿信息文件上传到服务器。

苹果用RunLoop实现的功能

界面更新

当操作UI时,比如改变了Frame、更新了UIView/CALayer的层次时,或者手动调用了UIView/CALayersetNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标记为待处理,并被提交到一个全局的容器中去。

苹果注册了一个Observer 监听BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

定时器

NSTimer 其实就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个NSTimer注册到RunLoop后,RunLoop会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。

PerformSelecter

当调用NSObjectperformSelecter:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop中。所以如果当前线程没有RunLoop,则这个方法会失效。

当调用performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有RunLoop该方法也会失效。

事件响应

苹果注册了一个1Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

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

手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用Cancel将当前的touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。

当有UIGestureRecognizer的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值