预计内容:
1、Runloop
2、autorelease
3、事件传递 & 响应链(不一样的 super 寓意)
4、UIResponder 分类与线程保活
关于以上内容:不总结不知道、一总结内容还真不少。
0x01 Runloop 小节
一、概念
(略过)
二、监听
代码先行:
// Runloop 监听回调函数
void hgRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
// 创建一个 Runloop 状态监听
- (void)runLoopObserverCreate {
// 手动创建一个 observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, hgRunLoopObserverCallBack, NULL);
// 添加到 MainRunLoop 中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);
}
以上的代码还是比较简单的,但是五脏俱全,各个数据类型通过名字都能看出其意思。具体在项目中的使用可以参考 HGMonitorStuck 文件。 在开发中可能比较关注的是 hgRunLoopObserverCallBack 中对所监听到的不同状态做不同的逻辑处理,关于 CFRunLoopActivity 的完整定义如下:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
这里需要注意的是这个枚举类型是 *_OPTIONS 类型的。还有其它的数据类型,具体如下:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
这些具体的用法,直接搜索都有很多的,相关代码,在这里:CF-1153.18.tar.gz
三、机制是什么
机制主要就是这张图片,就是运行循环,更多的信息可以看CF-1153.18.tar.gz
那么问题来了:在 iOS 中哪里用到了 NSRunloop,这个问题的答案是:所有的地方都用到了。比如将第二小节的代码放到一个视图控制器中调用,然后直接点击屏幕,都能监听到其状态的改变,这些东西都是很好理解的。
四、Runloop 总结
简单的介绍就到此结束了,上面除了代码与图片,貌似也没有什么。这种东西主要就是理解,要想理解那就看 CF-1153.18.tar.gz 中的代码。更多的内容,比如与线程、定时器,还有其它的 Source0与 Source1都是什么关系,这些网上随便一搜都有,但是尽量还是以看源代码的逻辑为准。
其实刚开始想要介绍这个 NSRunloop 的初衷是想直接介绍 autorelease 的,后来发现如果不先介绍 NSRunloop 的话,对 autorelease 根本就解释不通。现在可以开始对 autorelease 小节的欣赏了。
0x02 autorelease 小节
一、概念
(略过)
二、问题
技术交流中可能会出现这样的问题:接收 autorelease 消息后的对象,是在什么时候被销毁的?
关于这个问题,是否会有小伙伴这样考虑过:这个是 MRC 环境的语法了,可以不用考虑。但是别忘了在 ARC 还有一个与之对应的关键字是 __autoreleasing,只能是理解了autorelease 之后就能能更好的理解 __autoreleasing。
知道这个问题的答案很简单,但完全弄清楚这个问题需要对 ****autorelease 与 NSRunloop 要有深层次的理解。
别人的答案:
1、autoreleasepool不是一个大栈,是分一个一个固定大小的page,双向链表连起来的。
2、在runLoop睡眠之前释放(kCFRunLoopBeforeWaiting),也就是说:如果在主线程开启一个自动释放池,那么就会在主线程即将进入休眠的时候清理一遍自动释放池。如果是在子线程添加的自动释放池,那么就在子线程即将进入休眠的时候清理.线程的runloop下次再开始运行循环的时候再去创建这些池子中的对象及释放池就可以了。
这些答案都是有道理的,但是如何去理解这些答案才是关键。比如:双向链表结构的自动缓存池对一个 autorelease 的对象有什么影响,与 NSRunloop 又有什么关系,上面提到 在主线程即将进入休眠的时候清理一遍自动释放池 里面提到的清理一遍缓存池,是如何清除的?等等一系列的问题将在下一小节详细介绍。
三、详细介绍
3.1 缓存池
其实,我意思是介绍 autorelease ,但是如果不介绍 AutoreleasePool 那肯定是没有意义的。在现在的网上的技术文章,几乎都是一上来就介绍重点,反正我 刚开始的时候是没有看懂。
先介绍在没有 NSRunloop 的参与下的 autorelease。那么就直接创建一个没有 Runloop 的项目(终端项目),首先就切换成 MRC 环境。
3.1.1 自动释放池的简单认识
刚创建的项目代码是这样的:
直接转成 cpp 代码看一下:
因为代码很简单,所以转成的 cpp 代码也很简单,但是在上面的代码中有我们想要的关键代码:
__AtAutoreleasePool __autoreleasepool;
通过仔细的观察发现所谓的自动释放池,变成了一个大括号与这个局部变量的初现了。再通过全文搜索 __AtAutoreleasePool ,找到了其定义:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
原来 __AtAutoreleasePool 是一个 C++ 类型的结构体,那么问题来了:对应这个结构体,仅仅申明一个局部变量就可以了?是的,很容易看到在这个结构体的定义中有一个成员变量、一个无参构造函数以及虚构虚构,并在无参构造函数中通过另一个函数给成员变量赋值,在虚构函数中将这个成员变量放到另一个函数中。
根据 C++的语法,可以模拟一下这个自动释放池最终在内存中的伪代码,应该是这样的:
可以自行的分析一下,自动缓存池嵌套的情况,分析都是一样的。
到这里,对一个自动释放池应该有一个基本的了解了,在上面的介绍中认识了三个东西:
1、__AtAutoreleasePool 数据结构
2、objc_autoreleasePoolPush() 函数
3、objc_autoreleasePoolPop(atautoreleasepoolobj) 函数
关于上面的两个陌生函数,大家可以到源代码中查看。
关于看源代码,最近很多的小伙伴都在使用源代码来分析各种的技术点,我有一个建议:尽量使用最新的版本,前不久我一直使用的 723 的,昨天发现已经更新到 750了,这是因为刚看到小伙伴们的文章,发现代码不一样,仔细看才知道版本不对,当然不管版本怎么更新,流程肯定都是相似的。仅仅是建议。
3.1.2 AutoreleasePoolPage
如果到源代码中看了哪两个陌生的函数,会看到一个关键的数据结构 AutoreleasePoolPage,这是一个 Class。总之上面的两个陌生函数实际调用的是AutoreleasePoolPage中的 Push() 与 pop 函数。
到这里可以理解成,自动释放池实际上就是在操作 AutoreleasePoolPage。其实看到这里,如果到浏览器搜索一下 AutoreleasePoolPage 会发现,技术文章那是相当的丰满,但是很多的文章是直接介绍的,没有像这篇文档一样还介绍了怎么找到这个数据结构的。
看到这里,建议先到浏览器浏览一遍那些丰满的文章,因为接下来我就会直接介绍核心的东西了。也可以选择在源代码中按照这样的顺序全文搜索看一下:
1、objc_autoreleasePoolPush 与 objc_autoreleasePoolPop,主要看 NSObject.mm 文件中的。
2、AutoreleasePoolPage 中只要能看到下图中的内容就够了。
3.1.3 自动释放池中的双向链表
大概是这样的:
是的,具体的双向链表大概就是这个样子的了,每个节点就是一个 Page,各个 Page 就是通过 parent 与 child 指针串起来的。
那么问题来了:每个 Page 的下半部分(大红括号)到底什么东西?可以在 AutoreleasePoolPage 中找到这样的代码:
由上图,可以得出以下结论:
1、begin 返回的是 Page 成员变量所占的所有内容地址的下一个指针位置。
2、end 就是整个 Page 的末尾地址。
那么就知道用大红括号括起来的地方应该是 SIZE - sizeof(*this) 了。关于 SIZE 的值,通过代码的跟踪,你会跟踪到这个地方:
那么问题又来了,这部分空间有什么作用?这部分就是为了在以后有对象发送 autorelease 的话,会将那个对象的地址直接依次的放入下面的地址中。可以先幻想一下将地址放到这里到底有什么作用。
到这里,对自动释放池的理解又进了一公里了。
3.2 autorelease 后的变化
到这里,依旧直接到源代码NSObject.mm 中看 autorelease 的实现。入口在这里:
看到了一个熟悉的东西:AutoreleasePoolPage,在看的过程中也会发现只要是 isTaggedPointer() 为真的都直接 return 了,进一步的证明 Tagged Pointer 的性能高是有一定的道理的,省去了很多的内存操作。
看到上面的这张图,要注意,最终调用的是 autorelease 函数,还将当前的 this 传进去了。依然一路的狂飙到这里:
这个方法,大家猜都能猜到了,我大概的翻译一下:
获取当前的 hotPage,如果这个 page 有值并且不满(full)的情况,直接添加(add)到这一页,其次如果这个页有值,那么就到 autoreleaseFullPage 中创建一个节点,然后串起来。否则通过 autoreleaseNoPage 函数创建一个新的节点。
到这里,应该完全的明白了上面的双向链表的意思了。总之在 autorelease 之后会将当前的对象添加到一个 Page中,如果发现没有任何的 Page,那么就创建第一个 Page 节点,如果有、但是已经满了,就创建下一个 Page 节点,如果没有满、那么就直接添加到当前的 Page 节点中。
3.2.1 _objc_autoreleasePoolPrint 打印看本质
到这里可以借助一个私有函数来帮我们理解,就是 _objc_autoreleasePoolPrint,这个是私有函数,可以直接使用,但是需要在使用的地方这样声明一下:
extern void _objc_autoreleasePoolPrint(void);
然后这样使用:
主要是看图,上图中可以看到当前代码之后内存中自动缓存池的数据布局,结合以上的介绍应该会有一个比较综合的认识。从上图中可以看出这个内存布局,很清晰的就把每个自动缓存池中的对象划分了:
这个就代表开始了,这个地方还是挺重要的,在上面浏览源代码的过程中跳过了一个地方:
AutoreleasePoolPage中的 push 与 autorelease 中所做的事情貌似很像是,仅仅是参数不同而已。到这里很轻松的就能想到,在 push 的时候是将 POOL_BOUNDARY 的东西弄进去了,autorelease 的时候是将当前的对象弄进去了。POOL_BOUNDARY就是代表着上面每页的开始,关于POOL_BOUNDARY可以自行浏览。再结合最最最开始介绍的,在AutoreleasePoolPage中有一个成员变量 atautoreleasepoolobj ,这个变量是在自动缓存池的开始返回,然后是在自动缓存池结束的地方放入到一个叫 Pop 的函数中,理解到这里似乎明白了 atautoreleasepoolobj 的用意了,估计在这个 Pop 函数中拿到这个成员变量之后就是给当前缓存池中的对象发送 release 的吧。为了证明这一点,再次看看这个 pop 函数,于是找到这个地方:
到了这里似乎明白了,在 pop 中是通过逆向的遍历一个 Page 中的对象,然后发送 release 方法,一直遍历到 POOL_BOUNDARY 的时候停止,因为这个地方是当前 Page 的开始。
这张图片应该还算形象,大红箭头表示 pop 以后 next 的移动方向,向左的箭头(除第三个)代表一个缓存池的开始位置。当第最里面的缓存池结束的时候,会调用一下这个缓存池中的 pop,然后开始通过 next 逆向遍历,找到一个 POOL_BOUNDARY 的时候就知道这个缓存池中的对象已经 release 结束了,当最外层的缓存池结束的时候,同理。
看到这里,直接弄一个思考:
以上代码后 log 打印顺序是什么样的?
name_11
name_10
中间的 @autoreleasepool 就要结束了
name_00
这样对么?为什么不对?理解了这个问题,那么上面的所有介绍应该就已经精通了。
3.2.2 autorelease 小节
到这里,在一个没有 NSRunloop 的项目中对 autorelease 就介绍结束了。如果没有看明白的,那肯定是我的描述有问题,只能是再看一遍或者多看源代码。
3.3 有 Runloop 的 autorelease
在上面的介绍中,虽然只是一个简单代码的介绍,但是也差不多清楚了自动释放池(@autoreleasepool)的简单结构以及 autorelease 消息发送之后的一些简单步骤。
在上面是在终端项目中使用了 两个 @autoreleasepool 来做介绍,在 @autoreleasepool 的开始有一个Page 的 Push操作,结束有一个 Page 的 pop 操作。
那么换到 iOS 项目中怎么样呢?首先需要清楚的一点是:iOS 的 App 是一直在 Runloop 中的,如果在不手动 @autoreleasepool {} 一个池子的话,那么整个 APP 的运行都是在 main 函数中的那个池子中。在这里一定要注意 一个自动释放池 不等于一个 AutoreleasePoolPage,这个仅仅是一个 池子中的其中一页。从上面的介绍中,不难看出也可以在不同的池子中共用一个 AutoreleasePoolPage。其实从源代码中也可以看出,所有的池子都是在一个双向链表中,只是不同的池子中的所有对象在链表中的不同地方。
那么问题来了:在 iOS 项目中,autorelease 对象在什么时候被释放的?
关于这个问题的答案,其实在网上都是使用上面的小节来回答的,说到了双链表,也说到了 AutoreleasePoolPage 的 Push 与 Pod,但是到底执行这些函数的时机是什么时候呢?有的说是跳出离自己最近的大括号,这分明就是错误的,在上面的小节中已经提到。也有说是在运行循环中,在睡眠之前清理了一遍,这个答案比较靠谱,单并不完整。
先来看一个很经典的现象吧:
看了这张图,就知道了 NSRunloop 与 autorelease 的结合真的不简单。
看到这个显示,已经清楚的一个事实是:autorelease 对象的销毁肯定是 AutoreleasePoolPage 中的 next 指针逆向移动了。
接下来看一下在 主线程中的 Runloop 中都有什么信息,发现信息很多,但是有一个 callout 值得关注:_wrapRunLoopWithAutoreleasePoolHandler,可以看出这是在主线程中的一个 observer 。关于这个 observer 可以发现 activities 的值由两种: 0x1 与 0xa0。通过对 NSRunloop 的了解,0x1 就是 kCFRunLoopEntry,0xa0 就是 kCFRunLoopBeforeWaiting | kCFRunLoopExit。
直接上一张 ibireme 大佬 的文章节选吧:
在那些年看到这么金典的内容,只能一脸的懵逼,死记硬背的记住了答案。
至此、关于 autorelease 的全部内容已经介绍完了。文档有些凌乱,细心的看的话,应该也会有所收获的,感兴趣的话就回头再看一遍。
0x03 事件 & 事件传递 & 响应链
一、认识 UIResponder
在 iOS 开发中很少看到 UIResponder 这个东西,但是都清楚 AppDelegate 、 UIView 与UIViewController 都是直接继承自她,凡是继承自她的,都叫响应者[对象],都能接收与处理事件。在接下来的介绍中,不会特意的提 UIResponder,但是应该清楚的是 事件传递与响应链是离不开 UIResponder 的。
二、触摸方法
提到触摸方法,肯定能想到如下四个方法:
- (void)touchesBegan:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(nullable UIEvent *)event;
以上的四个方法特别的简单,大概的调用流程如下:
其中 B 节点,如果不移动的话就不会触发。所以就有如下两种可能:
1、A-[B]-C1
2、A-[B]-C2
在这里需要注意的是,在同一次操作中,各个方法中的两个参数值是一样的。
2.1 参数介绍
touches
是一个 UITouch 集合,这个集合的数量就是当前的触摸由多少个触摸点(手指的个数)。
UIEvent
这个就是具体的事件,虽然 UIEvent 中的属性不是太多,但是包括了第一个参数的信息(allTouches)。还有两个比较关键的属性:
@property(nonatomic,readonly) UIEventType type NS_AVAILABLE_IOS(3_0);
@property(nonatomic,readonly) UIEventSubtype subtype NS_AVAILABLE_IOS(3_0);
这就是事件类型,当然当前文档主要介绍的是 UIEventTypeTouches。
三、事件传递
为了接下来的介绍,我准备了一个这样的项目:responder
从上图中可以清楚的了解到这些自定义视图的父子关系,这些自定义视图都是直接继承于 UIView 的。都重写了 touchesBegan 方法,都是这样的:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(@"%s", __func__);
}
请不要问为什么不弄一个基类,看到后面会明白的。
现在可以顺便乱点,看看控制台信息。主要关注圆圈圈区域:
经过试验,应该理解了大概的规律,可能比较难理解的就是 标号为 4 的点击。想必小伙伴们都会有自己的思考,这个简单的试验隐藏了 iOS 中的事件传递。接下来直接给出最终的解释:
当点击屏幕的时候,首先是 UIApplication 收到这个事件,然后传递给 KeyWindow,然后通过控制器以及视图相互的传递下去。对于父子控制器的顺序是:父控制器传给父控制器的视图然后再传给子控制器。对于父子视图的顺序是:父视图传给子视图。
比如点击上面 标号 2 的区域,其顺序为:AppDelegate --> UIApplication --> KeyWindow --> 控制器 --> HGVcView --> HGRedView。
同理 标号 9 的顺序为:AppDelegate --> UIApplication --> KeyWindow --> 控制器 --> HGVcView --> HGGrayView --> HGYellowView.
那么问题来了,是所有的视图都能传递事件么?不是的,在以下的情况是不能传递事件的:
1、userInteractionEnabled = NO;
2、hidden = YES;
3、alpha < 0.01
比如我将 HGGrayView 的 userInteractionEnabled = NO;,那么点击标号9的区域,事件是这样的:AppDelegate --> UIApplication --> KeyWindow --> 控制器 --> HGVcView。 看完效果 记得将 HGGrayView 的 userInteractionEnabled = YES;
关于事件传递,还需要清楚的是,当我点击 标号8 区域的时候,为什么是 HGGrayView 接收了事件呢?首先要清楚一点:在事件的传递过程中、这个传递对象是不处理事件的,除非找到了一个合适的响应对象,也称第一响应者(First Responder)。那么什么样的响应者对象才算合适呢?需要满足以下几点:
1、这个响应者对象已经没有可用的子响应者对象
2、触摸点在这个响应者对象的空间范围之类
到这里、关于事件的传递就简单的介绍结束了。
看到这里,细心的小伙伴可能就会抛出一个疑问:如果将 HGGrayView 中的 touchesBegan 注释之后,当点击 标号8 区域的时候,尽然是 HGVcView 处理了这个事件,这是为什么呢?这个问题的答案在下一小节,这个问题已经牵扯到了响应链的概念。
四、响应链
在看响应者链之前,先来复习一下再 OC 中的 super 关键字吧。
复习 super 中 。。。。。
复习 super 中 。。。。。
4.1 响应链中的 super
当你复习结束之后,想要问问你,上面提到touchesBegan。。。。touchesEnded这些方法,都是系统方法,按照道理来说这种系统方法,苹果是推荐在使用的时候调用 super 的。但是在开发中,你有调用过 super 么?相反,一旦调用可能会出现一些莫名其妙的问题。
好的,接下来将 HGGrayView 中的 touchesBegan 打开,并给 HGGrayView 添加一个自定义的父类 HGBaseView, 让 HGGrayView 继承于这个 HGBaseView。在 HGBaseView 中重写 touchesBegan 方法,然后在 HGGrayView 中调用 super。
最后你发现了什么?是不是 HGBaseView 中的 touchesBegan 根本没有被调用,而是调用了 HGVcView 中的 touchesBegan、这就厉害了(画线部分是我忘记将 HGGrayView 继承于HGBaseView了,通过 HGResponder 项目可以看出,也许已经有小伙伴发现了。所以才没有调用 HGBaseView 中的方法,实际上是会调用父类方法的,会在父类方法中通过 super 将事件传给下一个响应者对象)。通过对事件传递的理解,HGGrayView 的事件是由 HGVcView 传递来的、差不多知道是怎么回事了。
响应链就是事件传递的反方向。比如在 HGYellowView 中的 touchesBegan 方法中添加如下代码:
NSMutableString* stringM = [NSMutableString string];
UIResponder * r = self;
[stringM appendFormat:@"%@", NSStringFromClass(r.class)];
while (r) {
r = r.nextResponder;
[stringM appendFormat:@" --> %@", NSStringFromClass(r.class)];
}
NSLog(@"%@", stringM);
点击 标签 9 区域的打印如下:
HGYellowView --> HGGrayView --> HGVcView --> ViewController --> UIWindow --> UIApplication --> AppDelegate --> (null)
到这里可以介绍在上面小节中提出的一个疑问:
如果将 HGGrayView 中的 touchesBegan 注释之后,当点击标号8 区域的时候,尽然是 HGVcView 处理了这个事件,这是为什么呢?
这是因为在默认的情况下 HGGrayView 中的事件通过响应链传给了 HGVcView,因为不重写 touchesBegan 方法的话,默认是这样的:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
}
4.2 响应链小节
其实对应响应链的介绍到这里已经结束了。
总之,要理解好响应链,那么必须要理解好事件传递。这两者的区别是,一个是属于寻找的过程,一个事件执行的过程。他们的方向正好是相反的。还有一点是要正确的理解在响应链的过程中对 super 关键字的理解,之所以在以后的技术交流中又能在 super 这个技术点上添点油加点醋了。
到这里也应该也能解释当点击标签4 的现象了,其实这个问题是我在 15年的时候亲自遇到的一个 BUG,具体的现象是有一个按钮,在有的设备上只能下半部分点击有反应,在按钮的下半部分点击是没有问题的,就是因为上半部分超出了父控件了。
这里有一些的技术交流中会出现的问题:
如何让UIView处理事件的同时继续传递事件.
如何判断一个UIView是View Controller的Root View
这些答案就显得 手一日 了。
关于事件传递与响应链,在开发中很有可能会遇到这两个系统方法,在现有项目中都有使用,可以看一下:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
关于时间传递与响应链的内容,还能想到的是手势,还有 UIButton 的 Action ,暂时感觉这些除了都是由触摸发起的之外,貌似没有多大的关系,毕竟属于不同的机制(可能是我的理解还不够深入)。比如在 UIButton 中会遇到这些现象:
1、按钮添加手势,那么 action 就得不到执行。
2、按钮没有添加手势,想要屏蔽 action 的话就可以重写 touchesBegan 不调用 super。
反正就是各种的冲突,但是话又说回来,一般情况谁会在同一个控件上搞这些事件呢,除非像图片浏览器,可以有点击、可以有双击、也可以有旋转,但是这些都是属于手势范畴。在 LongLong Ago,还会出现 UIButton 的 superView 添加手势的情况会影响到 UIButton,但是现在苹果在很久之前就已经优化。
当然在一些特殊的场景,可能手势与触摸冲突了,苹果也已经考虑到冲突的情况,所以也提供了这样的 delegate 方法:
// called before touchesBegan:withEvent: is called on the gesture recognizer for a new touch. return NO to prevent the gesture recognizer from seeing this touch
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch;
五、官方参考
在网上有很多大佬都在使用这张图片来对响应链做介绍:
这个对响应链的解释算是最权威的了,在GitHub 找到了一 大佬 的翻译 Event-Handling-Guide-for-iOS, 可以参考一下。
这里有一个最新的官方介绍,请看这张图片与上面的图片是一样的:
仔细观察,这张图与上面的那张图还是有区别的,多了一个 UIApplicationDelegate。难道在很久之前的响应链直接到 UIApplication 就结束了??这个就不是太清楚了,确实在之前大佬们的介绍中没有提到 UIApplicationDelegate 的。但是同时也要清楚一点 UIApplicationDelegate 是 Delegate 过来的。
更多的详细信息在这里 using_responders_and_the_responder_chain_to_handle_events
0x04 金典案例
在这里有两个例子:线程保活与UIResponder分类分别介绍了 NSRunloop 与 UIRespomder 的应用。具体的代码在这里:UIResponder
一、UIResponder分类
核心代码:
/**
响应链
@param eventName 事件名称
@param eventInfo 事件信息
*/
- (void)routerEventWithName:(NSString *)eventName eventInfo:(NSDictionary *_Nullable)eventInfo {
[[self nextResponder] routerEventWithName:eventName eventInfo:eventInfo];
}
突然一看死循环、仔细一看很经典。这也是有使用场景的,这个使用场景也可用通知来实现。具体的使用,可以自行品味。
二、线程保活
我们知道,一个线程只要是任务结束了,那么当前的线程马上就退出了。如果有一批的任务需要在一个单独的线程中处理,但是又不想每次使用都创建一个新的线程,那么久需要考虑将同一个线程一直处于活跃状态而不退出。这个功能主要就是结合 NSRunloop 来处理。
具体的代码,在这里:
代码不多,但是技术点还真不少,主要集中于这三条:
1、NSThread 的使用
2、NSRunloop 的 run 方法
3、waitUntilDone参数选择
接下来一一介绍。
2.1 NSThread 的使用
现在使用的是 initWithBlock 方法 很像 NSTimer,那么第一个问题来了,这个 block 中可否使用如下的代码:
__strong typeof(weakSelf) self = weakSelf;
答案是不可以的,即使这样使用了,貌似还是内存泄漏了,这个具体的原因有待进一步的研究,应该是这个 Block 的内部实现 很强大,所以只能使用 weakSelf。
第二个问题是:是否可以使用 initWithTarget?
其实这个也要注意,我在之前的文档中也介绍过,这种方式会像 NSTimer 一样导致指针循环, 但是还不好解决的样子。我使用了 YYWeakProxy 都不行。。。。。
这样看来,这个 NSThread 确实不简单,有待进一步的研究。所以当前的实现来看、只可以使用 initWithBlock + weak 的方式。
2.2 NSRunloop 的 run 方法
是的, NSRunloop 有三个去激活的方式:
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
这里使用了 第三个方法,第二个方法不符合现在的场景。那么第一个方法为什么不可以呢?这是因为第一个方法是永远停不下来的,一旦 run 了,那么这个线程就永远的保活了,这个可以从苹果对这个方法的注释中可以看出。所以只能放弃这种方式。
在看代码的过程中,还有一个属性 stopped。关于这个属性,是与 runMode 这个方法的机制息息相关的:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
这个方法的意思是将当前的线程处于 run 的状态在 beforeDate 之前,distantFuture很大,所以能保持永远的处于 run 状态的等待状态,但是有一个关键的点是:这个线程每执行一次任务,这个方法就返回了。所以这里需要加一个 stopped 属性,当 runMode 返回之后再 runMode。
其次 while (weakSelf && !weakSelf.isStopped) 能换成 while (!weakSelf.isStopped) 答案是不可以的,当 weakSelf 为 nil 的时候 !weakSelf.isStopped 的值为 YES. 所以。。。。。。
2.3 waitUntilDone参数选择
主要在两个地方使用了 performSelector:onThread:withObject:waitUntilDone: 方法,但是在 waitUntilDone 的参数却各不相同,为啥?
首先要明白这个参数的意义:代表从当前的线程执行另一个线程方法的时候,是否要等另一个线程的方法执行结束之后,再返回。为 YES 的时候代表必须要等另一个线程执行结束了,才会返回。为 NO 的时候代表,不等另一个线程的方法执行结束,立马就返回。
那这里为啥要使用 YES 呢?
这是因为这个 __stop 的方法可能是在 HGPermenantThread 的 dealloc 方法中调用的。如果使用 NO 的话,那么HGPermenantThread 对象已经没有了,但是在 __stop 中又访问了 HGPermenantThread 对象,所以坏内存访问错误了。
0x05 GCD 中的 Runloop 的 autorelease
直接看一段代码:
NSLog(@"开始上班了");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"我应该做点啥~");
});
NSLog(@"下班了");
可以尝试回答一下这个打印顺序是不是这样的?
- 开始上班了
- 下班了
- 我应该做点啥~