来自掘金 《由 NSObject *obj = [[NSObject alloc] init] 引发的一二事儿》
本文是为了准备在实习公司新人串讲中部分内容的草稿,主要介绍一些 Objective-C 和 iOS 基础的东西,纯属抛砖引玉~
Objective-C 基础
接下来跟大家分享一下 Objective-C 和 iOS 开发的基础内容,而且主要会围绕一句普通的代码进行展开:
NSObject *obj = [[NSObject alloc] init];
其实这部分内容大都是我自己对这行代码冒出的一些的问题和想法进行的解释,而且准备得有些仓促,所以难免会有些不全面和错漏的地方,请多多见谅~
基本含义
我们先来看看这句代码的基本含义,尝试从 NSObject 这个角度去解读
这行代码中写有两个 NSObject ,但他们表示的意思是不一样的。
等号左边表示:创建了一个 NSObject 类型的指针 obj 。(开辟一个 NSObject 类型大小的内存空间,并用指针变量 obj 指向它)
等号右边表示:调用 NSObject 对象的类方法 alloc 进行内存空间的分配,调用实例方法 init 进行构造工作,如成员变量的初始化等。
等号右边的 NSObject 对象初始化完成之后将内存地址赋值给左边的 obj 。
new 方法和 alloc/init
感觉使用 Java 的人经常会说 new 一个对象,虽然 Objective-C 也给我们提供了这个方法,但我们却很少直接使用 new ,而是使用 alloc init ,为什么?
使用 new 和 使用 alloc init 都可以创建一个对象,而且在 new 的内部其实也是调用 alloc 和 init 方法,只是 alloc 会在分配内存时使用到 zone ,其实总体来看没啥区别。
NSZone 是 Apple 用来处理内存碎片化的优化方式,处理对象的初始化及释放等问题,以提高性能。但据说效果并不好。
使用 new 的好处是什么?
- 简单,比使用 alloc init 少码一个单词和一对中括号
使用 alloc init 的好处是什么?
- 显式调用,
少了 new 内部实现的转换,可能速度更快(速度慢) - 支持自定义构造方法。一般在我们的自定义类的构造方法中都会使用到如
initWithXxx
为啥 Objective-C 的构造方法都是 initWithXxx
,而 swift 可以使用 init
?
- Objective-C 不支持函数重载,所以使用
initWithXxx
替代 - swift 支持函数重载
只是声明一个变量 NSObject *obj;
?swift 呢 ?
在 Objective-C 中允许只声明一个变量并被使用,编译器不会报错。如果声明的是一个 Objective-C 对象,输出的值是 null ,如果是基本类型,输出的值是 0 ,如果是结构体如 CGRect ,会用 0 填充。
但是在 swift 中,情况就不一样了,声明一个变量如 let a : Int
并被使用时,编译器会对这种行为报以错误提示,Variable 'a' used before being initialized
,表示变量 a 在使用前未被初始化。
为什么?
其实在 Objective-C 中声明一个变量时,它是会有一个默认值的,但在 swift 中则不会提供默认值,因为 swift 作为一种强类型语言,它总是强制类型定义,并且要求变量的使用要严格符合定义,所有变量都必须先定义,且初始化后才能被使用。
这里有一个例外 -- 可选类型,如 let b : Int?
,可选属性不需要设置初始值,默认的初始值都是 nil ,不管是基础类型还是对象类型的可选类型属性的初始值都是 nil 。而且可选类型是在设置数值的时候才分配空间,是一种 lazy-evaluation 即延迟计算的行为。
Objective-C 和 swift 构造方法顺序的区别?
- 在 swift 的构造方法中,先正确初始化本类的成员变量,再调用父类的构造方法
- 在 Objective-C 的构造方法中,先调用父类的构造方法,再正确初始化本类的成员变量
构造方法 init 中使用 if(self = [super init])
写法
平常在构造方法里做一些初始化工作时都会写上这样的代码, self = [super init]
这里先调用父类的构造方法也符合上述的构造顺序问题,但疑惑的是,为什么 [super init]
要赋值给 self ?为什么需要使用 if 作校验?
Objective-C 方法的调用,会转换成消息发送的代码,如 id objc_msgSend(id self, SEL op, ...);
MyClass *myObject = [[MyClass alloc] initWithString:@"someString"];
上述代码会被编译器转换成:
class myClass = objc_getClass("MyClass");
SEL allocSelector = @selector(alloc);
MyClass *myObject1 = objc_msgSend(myClass, allocSelector);
SEL initSelector = @selector(initWithString:);
MyClass *myObject2 = objc_msgSend(myObject1, initSelector, @"someString");
可以看到,当调用 objc_msgSend(myObject1, initSelector, @"someString")
时 self 已经有值了,它的值是 myObject1 。
回到 [super init]
这句代码,要注意,它不是被编译器转换成 objc_msgSend(super, @selector(init))
,而是会被转换成 objc_msgSendSuper(self, @selector(init))
。
是的,self 在初始化方法开始执行时已经有值了。
这里的 super 是一个编译器指令,和 self 指向同一个消息接受者,即当前调用方法的实例。他们两个的不同点在于:super 会告诉编译器,执行
[super xxx]
时转换成objc_msgSendSuper
,即要去父类的方法列表找,而不是本类。
那么为什么要将 [super init] 方法的返回值赋值给 self 呢?
来看一段常见的构造方法代码片段:
- (id)initWithString:(NSString *)aString
{
self = [super init];
if (self)
{
instanceString = [aString retain];
}
return self;
}
经典解释:执行 [super init]
会产生以下三种结果中的一种:
- 返回方法的隐含参数 self ,且初始化完成继承的实例变量
- 返回一个不同的对象,且初始化完成继承的实例变量
- 返回 nil ,初始化失败
第一种结果,赋值操作对 self 没有影响,后面的实例变量赋值在了原始对象上。
第三种结果,初始化失败,self 被赋值为 nil ,返回。
至于第二种结果,如果返回的对象不一样,那么就需要将 instanceString = [aString retain]
(被转换成 self->instanceString = [aString retain]
)方法实现里的 self 指向新的值。
那么问题来了,[super init]
会返回不同的对象?
是的!在以下情况会返回不同的对象(所谓不同对象,是内存地址的不同):
- 单例对象
- 一些特别的对象(如
[NSNumber numberWithInteger:0]
总是返回全局的 "zero" 对象) - 类簇
- 根据传入初始化方法的参数重新分配原(或兼容)类。这种情况下如果继续初始化返回的已经改变的对象是一种错误的行为,因为这时返回的对象已经被完全初始化了,并且跟本身的类不再相关。
现在,根据返回的对象是否不同,执行 [super init]
产生的结果扩展为以下四种:
- 返回方法的隐含参数 self ,且初始化完成继承的实例变量
- 返回一个相同的对象,但须要进一步的初始化工作
- 返回一个不同的已经初始化完成的对象
- 返回 nil ,初始化失败
可以看到,case 2 和 3 其实是互斥的,我们一般无法使用一种途径来满足所有的这四种 case 。
常见的能够满足 case 1,2 和 4 的做法是: self = [super init];
即上面的做法。
这里展示一种能够满足 case 1,3,和 4 的途径,即平常会被问到能否用一个变量替代 self 的做法是:
- (id)initWithString:(NSString *)aString
{
id result = [super init];
if (self == result)
{
instanceString = [aString retain];
}
return result;
}
所以类簇,单例和特殊的对象都是 case 3 ,NSManagedObject 是 case 2 。
可以看到 case 3 非常常见,但是在构造方法中满足 case 1,2 和 4 变成了一种 standard (虽然在某些隐藏条件下是错误的做法)。
Objective-C 中的基础类型和对象类型
在这行代码 NSObject *obj = [[NSObject alloc] init];
等号左边的 NSObject 表示的是对象类型,那么在 Objective-C 中常见的基础类型和对象类型有哪些
基础类型:
- 整型:int(32位)、Integer(根据计算机位数调整)
- 浮点型:float(4 字节)、double(8 字节)、CGFloat(根据计算机位数调整)
- 字符型:1 字节,Objective-C 字符变量不支持中文字符,字符需要使用 ` ` 包起来,char 类型也可以看作整型值来使用,它是一个 8 位无符号整数
- 布尔型:YES、NO
- 枚举型
对象类型:
- NSObject
- NSString 及可变版本 NSMutableString
- NSArray 及可变版本 NSMutableArray
- NSDictionary 及可变版本 NSMutableDictionary
这里有一个注意点是,有可变与不可变类型的对象,为了安全起见,用 copy 修饰不可变版本,用 strong 修饰可变版本,这样做的原因是,如果有一个不可变的字符串 str 且用 strong 修饰,这时被赋值了一个可变字符串 mStr ,这样可能会发生这样的情况:一个本来预想中不可变的字符串 str 会因 mStr 的改变而改变。所以这里要仔细考量一下使用 copy 还是 strong 去修饰。
Objective-C 世界中的 “非 0 即真” ?
在 Objective-C 中,BOOl 的定义是这样的:
typedef signed char BOOL;
#define YES (BOOL)1
#define NO (BOOL)0
其他相关的布尔型如下:
bool :
C99标准定义了一个新的关键字_Bool,提供了布尔类型
#define bool _Bool
#define true 1
#define false 0
Boolean:
typedef unsigned char Boolean;
enum DYLD_BOOL { FALSE, TRUE };
Objective-C 对象存储在堆区
上面谈到了 NSObject 作为类型展开的一些内容,现在我们来看看 NSObject 作为对象来延伸出 Objective-C 对象存储位置相关的内容。
先来看看内存的五大区:
- 栈
- 堆
- 静态区(BSS 段)
- 常量区(数据段)
- 代码段
还是回到最开始的那行代码来进行解释:
NSObject *obj = [[NSObject alloc] init];
我们知道,在 Objective-C 中,对象通常是指一块有特定布局的连续内存区域。这行代码创建了一个 NSObject 类型的指针 obj 和一个 NSObject 类型的对象,obj 指针存储在栈上,而其指向的对象则存储在堆上。
在栈上就不能创建对象吗?
* 不能直接创建,但可通过在结构体中的 isa 来间接创建对象。
- 其实 block 也可以存储在栈上
那么这里又带来了几个问题,isa 和 block ,这在后面会单独聊。
栈对象的优缺点
那么为什么 Objective-C 会选择使用堆来存储对象而不是栈,来看看栈对象的优缺点。
优点:
- 创建速度和运行时速度快:相对于堆对象创建时间快几十倍;编译期能确定大部分内存布局,因而在运行时分配空间几乎不耗时
- 生命周期固定:对象出栈就会被释放,不会存在内存泄漏
缺点:
- 生命周期固定,可能会出现这种情况:一个栈对象被创建之后被传递到别的方法,当栈对象的创建方法返回时,栈对象会被一起 pop 出栈而释放,导致没法在别处被继续持有,此时 retain 会失效,因此,栈对象会给对象的内存管理造成相当大的麻烦。
- 空间:栈跟线程具有绑定关系,而栈的可用空间非常有限的。因此对象如果都在栈上创建不太现实,而堆只要物理内存不警告即可使用。
- 512 KB (secondary threads)
- 8 MB (OS X main thread)
- 1 MB (iOS main thread)
综上,Objective-C 选择使用堆存储对象。
NSString 的存储位置
关于 NSString 的存储位置非常复杂,可以分配在栈区、堆区、常量区,粗略的理解如下:
- 当创建的 NSString 类型底层是 NSTaggedPointerString 时,其本质不再是一个对象,而是真正的值,存储在栈区
- 当创建的 NSString 类型底层是 __NSCFConstantString 时,其 retainCount 极大,意味着不会被释放,存储在常量区(一般通过字面量进行创建)
block 的存储位置
block 可以存储在栈上,也可以存储在堆上。通常我们会使用 copy
将一个栈上的 block 复制到堆上。
顺便谈谈关于 block 的其他内容:
block 的意思是拥有自动变量的匿名函数。
- 在 Objective-C 中称为 block
- 在 swift 中称为闭包
- 在 Java 中称为
lambda(也称闭包)
这里要注意的是,由于栈对象的有效区域仅限于其所在的块 {} ,即其捕获自动变量的范围也仅限于所在块。
还有一个修改自动变量时的注意点是:
- 静态全局变量,全局变量由于作用域的原因,可以直接在 block 里被修改
- 静态变量由于传递给 block 的是内存地址值,所以也能在 block 里被修改
- 默认情况下是不允许修改捕获到的自动变量值,但我们可以通过使用 __block(storage-class-specifier,存储域说明符) 修饰变量来使该变量也能在 block 里被修改
Mansory 的 block 为什么不循环引用
到这儿由 block 联想到了我们项目中使用到的自动布局框架 Mansory ,比如项目中的一个代码片段:
[self.carousel mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(self.headerView);
make.left.mas_equalTo(self.headerView).offset(20);
make.right.mas_equalTo(self.headerView).offset(-20);
make.height.mas_equalTo(CGFLOAT_MIN);
}];
通常在使用 block 时都会避免在 block 内部使用 self ,以免产生循环引用,造成内存泄漏,所以通常会在 block 外部对 self 进行一次弱引用,再在内部进行一次强引用,用这种组合做法来避免产生循环引用现象,这里的循环引用现象可能是:self -> block -> self 。
然后我们通过观察其源码实现来进一步了解:
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.removeExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
结合前面介绍的关于 block 存储位置的内容,我们可以知道,虽然 block 内部引用了 self ,但由于这是一个局部的 block ,存储在栈上而不是堆上,因而在出了 block 所在作用域后会被 pop 出栈而自动销毁,所以不存在引用环。
聊聊 block 和 Delegate
讲完 block 的存储位置,自然会想到它的一些使用场景,特别是选在择使用 block 还是代理的一些争执吧。
其实我觉得如何进行选择更多的是依据个人的一些编码风格和习惯,还有就是要符合原有项目的需要,说到底使用 block 和代理都没问题。但由于一些函数式框架的出现,比如 RAC 、RxSwift 、 promisekit ,里面链式调用 + 闭包的操作实在是很方便,而且也更加符合低耦合高内聚的编程理念,所以可能选择使用 block 又多了一个理由。
下面再聊一下 block 和 delegate 的一些本质区别,这部分内容主要是引述微信技术群里面的一位大佬的解释:
代理的 debug 追踪性确实会比 block 好,但是如果跟 block 在可读性方面比较的话其实算是弱项。
代理和 block 实际上都是函数 imp 的调用,但区别是,代理就等价于 weak 持有一个代理对象,你不写 protocol 不写 delegate ,一股脑把所有方法全写在 header 里,然后把代理对象本身直接传过去给另一个对象,在另一个对象中 weak 持有这个代理对象,这种写法和代理是没有区别的。
而 block 是一种还原上下文环境,甚至自动包裹一些自由变量的闭包概念,换句话说,block 的回调代码,和写 block 的代码,是可以同处于一个函数内,在一个可读代码上下文内,即 block 在代码上是一个连续的过程。
代理方法实际上传值传的是一整个对象,你把 a.delegate = self 其实是把 self 传给了 a 持有,跟一般的属性赋值无异,如果再次传递,完全可以继续传递 self 给别人。
block 继续传递,实际上是把 imp 和上下文环境的自动变量打包进行传递,这个过程中不一定会传递一个对象。从这个角度看 block 的控制力度更强一些。
这里会涉及到一个安全性方面的考虑,你把 self 传给了一个不知名的三方库,他虽然只是 id<XxxDelegate> 看起来只能调用 protocol 里限定的方法,但其实 OC 这个约束只是骗骗编译器的。如果你把一个 self 传给了一个三方,设定为代理,如果三方有其他意图,他其实可以直接控制你的 self 对象的任意值或者方法。但 block ,你传过去的 block ,他只能操作 block 本身包裹的上下文环境。
ARC 下编译器自动补 __strong 修饰
扯得有点远了,咱回头看回最开始的那行代码 NSObject *obj = [[NSObject alloc] init];
,在 ARC 下会变成 __strong NSObject *obj = [[NSObject alloc] init];
,这涉及到 iOS 开发的内存管理相关内容。
在早期 macOS 开发中使用 GC 进行内存管理但现在都跟 iOS 开发一样已统一使用引用计数进行内存管理:当对象的引用计数为 0 时会被销毁,当对象被引用时其引用计数会 +1 ,当对象的引用被销毁时引用计数 -1 。
- ARC(automatic reference counting),自动引用计数
- MRC(manual reference counting),手动引用计数
__strong 是一个变量修饰符,但这里不打算列举其他的变量修饰符,而会在下一条聊聊关于内存管理相关的属性修饰符。
属性修饰符
内存管理相关的变量修饰符都有相对应的属性修饰符,一般的写法是在属性修饰符前添加两个下划线。
这里列举一下内管管理语义的属性修饰符:
- assign
- strong
- weak
- unsafe_unretained
- copy
设置上述属性修饰符会在属性自动生成 setter 方法的时候为我们添加内存管理语义,明确内存管理所有权,如果我们自定义 setter 访问器,则需手动指定。
- assign:在 setter 方法中只是进行简单的赋值操作。适用于一些标量类型和 id 类型,如 CGFloat、NSInteger 。
- strong:会指定所有权关系。在 setter 方法中,当一个新值要被设置时,首先 retain 新值,然后 release 旧值,最后再进行赋值。
- weak:会指定无所有权关系。在 setter 方法中,当一个新值要被设置时,既不会 retain 新值,也不会 release 旧值,只会进行简单的赋值操作。在属性所指向的对象销毁时,属性值会清空。
- unsafe_unretained:跟 assign 的语义相同,但适用于 OC 对象。并且与 weak 不同的是,在属性所指向的对象销毁时,属性值不会被清空。
- copy:与 strong 类似,但在 setter 方法中不会 retain 新值,而是将其拷贝。
标量类型缺省是 assign ,对象类型缺省是 strong 。
再聊聊属性 @property
我们知道,属性 = 实例变量(ivar) + setter + getter ,他的具体过程是这样的:
完成属性定义后,编译器会自动编写访问这些属性所需的访问器,此过程称为“自动合成”(autosynthesize)。需要强调的是,这个过程由编译器在编译期执行,所以在编译器中看不到自动生成的源代码。除了生成访问器 getter 、setter 之外,编译器还要自动向类中添加适当类型的实例变量,实例变量名称是在属性名前加下划线,我们也可以在类的实现代码里通过 @synthsize 语法来指定实例变量的名字,如:
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
这里有一个注意的地方,@synthesize firstName;
像这样不指定实例变量的名字,那么生成的实例变量名会跟属性名一致,而不会再加下划线。
还有一个要注意的关键字:@dynamic ,他不会在编译阶段自动生成 getter 和 setter 方法,而且使用点语法或者赋值操作在编译阶段仍能够通过,但是该属性的访问器必须在运行时由用户自己实现,否则会 crash 。
isa
在前面我们有说到 Objective-C 对象通常是一块有特定布局的连续内存区域,所以接下来牵扯的内容可能会扯得比较远。
在计算机网络中有一堆协议,遵守同一个协议,那么他们之间便可以知晓对方的身份,接着愉快的进行通信。
那么 Objective-C 作为一门面向对象语言,他是怎样判断一个东西是不是对象,又是如何进行对象间的通信?
Objective-C 中的对象是一个指向 ClassObject 地址的变量:
id obj = &ClassObject
这个地址其实就是在最高位的 isa 指针。
而对象的实例变量则是:
void *ivar = isa + offset(N)
所以 isa 就相当于对象之间的一个协议。
Objective-C 面向对象的一个 bug :比如一个 Person 实例,她调用一个 talk 方法,按理说,这个 talk 方法应该直接在该实例里面调用对应的实现,但是实际却不是这样,她会通过实例自己的 isa 指针找到对应的类,然后在类或及其类的继承结构一直往上寻找 talk 方法,该方法被找到后就会调用,这样看来就并不是最初的那个实例进行调用。
atomic 不安全
属性还有一类原子性修饰符,atomic 和 nonatomic ,原子性和非原子性,缺省是原子性的,但在 iOS 开发中,几乎所有属性都会主动声明为 nonatomic 。
原因有两个:
- atomic 有性能消耗
- atomic 无法保证更大粒度时的安全问题
第一点,是因为使用 atomic 修饰的属性由编译器所合成的方法会通过锁机制(底层使用自旋锁)来确保原子性。
第二点,是因为即便原子操作阻止了属性被多个线程同时进行访问,但这并不代表我们最终使用它们的代码时时线程安全的,比如并发访问粒度更大的实例中的属性,举个例子:
// Person.h
@property(atomic, copy) NSString *firstName;
@property(atomic, copy) NSString *lastName;
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t) q;
// Person.m
- (void)updateWithFirstName:(NSString *)firstName lastName:(NSString *)lastName delay:(double)t on:(dispatch_queue_t)q{
if (firstName != nil) {
self.firstName = firstName;
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(t * NSEC_PER_SEC)), q, ^{
if (lastName != nil) {
self.lastName = lastName;
}
});
}
// ViewController.m
- (void)viewDidLoad {
[super viewDidLoad];
Person *p = [[Person alloc] init];
p.firstName = @"Holy";
p.lastName = @"H";
dispatch_queue_t queueA = dispatch_queue_create("queueA", 0);
dispatch_queue_t queueB = dispatch_queue_create("queueB", 0);
dispatch_async(queueA, ^{
[p updateWithFirstName:@"John" lastName:@"J" delay:0.0 on:queueA];
});
dispatch_async(queueB, ^{
[p updateWithFirstName:@"Ben" lastName:@"B" delay:0.0 on:queueB];
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@ %@", p.firstName, p.lastName);
});
}
现在运行结果可能会出现
Ben J
这显然不是我们最初想要得到的。
到目前为止,Apple 公司的开发者们并没有为 swift 的属性提供标记 atomic/nonatomic 的方法,也没有下面提到的 @synchronized 块那样去做互斥操作,而我们可以通过使用 @synchronized 底层使用到的 objc_sync_enter(obj)
和 objc_sync_exit(obj)
去实现,但因为 objc_sync_xxx
是相当底层的方案,一般不推荐直接使用,而应选择其他高阶的方案。
并发
那么如何解决上述示例的问题?我们可以通过在修改时添加 @synchroized(obj) {} 块,将原子操作的粒度扩大到 obj 对象的修改域。
还有其他一些常见的同步机制如:NSLock、pthread、OSSpinLock、信号量等。
iOS 基础
事件产生、传递和响应链
最后简单介绍一下 iOS 开发中事件的产生、传递和响应链。
事件产生:
系统注册了一个 Source 1(基于 mach port)用来接收系统事件,其回调函数为 _IOHIDEventSystemClientQueueCallback() 。当一个硬件事件(比如触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、传感器等几种 event ,随后用 mach port 转发给需要的 APP 进程。随后苹果注册的哪个 Source 1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会以先进先出的顺序把 IOHIDEvent 处理并包装成 UIEvent 进行处理分发,其中包括识别 UIGesture /处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegan/Moved/End/Cancel 等都是在这个回调中完成的。
触摸事件传递,大致是从父控件传递到子控件:
UIApplication -> UIWindow -> UIView (or Gesture recognizer 这时会被当前 vc 截断) -> 寻找处理事件最合适的 view
那么如何寻找处理事件最合适的 view ?步骤:
- 首先判断主窗口能否接收事件
- 触摸点是否在自己身上
- 若在,从后往前遍历自己的子控件,重复前两个步骤(能否接收事件?是否在控件上?)
- 直到找不到合适的子控件,那么自己就成为最合适的 view ,之后调用具体的如 touches 方法处理
底层实现主要涉及两个方法:func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
和 func point(inside point: CGPoint, with event: UIEvent?) -> Bool
hitTest 方法会根据视图层级结构往上调用 pointInside 方法,确定能否接收事件。如果 pointInside 返回 true ,则继续调用子视图层级结构,直到在最远的视图找到点击的 point 。如果一个视图没有找到该 point ,则不会继续它往上的视图层级结构。
我们可以通过调用这个方法来截获和转发事件。
事件响应,大致是从子控件传递到父控件:
过程:
- 如果当前 view 是控制器的 view ,那么控制器就是上一个响应者,事件就传递给控制器;如果当前 view 不是控制器的 view ,那么父视图就是当前 view 的上一个响应者,事件就传递给它的父视图
- 在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给 window 对象进行处理
- 如果 window 对象也不处理,则其将事件或消息传递给 UIApplication 对象
- 如果 UIApplication 也不能处理该事件或消息,则将其丢弃
在上述过程中,如果某个控件实现了 touchesXxx 方法,则这个事件将由该控件接管,如果调用 super 的 touchesXxx ,就会将事件顺着响应者链继续往上传递,接着会调用上一个响应者的 touchesXxx 方法。
一般我们会选择使用 block 或者 delegate 或者 notification center 去做一些消息事件的传递,而现在我们也可以利用响应者链的关系来进行消息事件的传递。