一,OC对象本质(底层实现)
1.OC对象底层实现
OC里有两大基类,NSObject 类 和 NSProxy类,我们熟知的绝大部分类都是继承自NSObject类。通过Clang语句可以将OC代码转换成C/C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp
会发现,OC对象的底层实现是一个结构体,结构体里有一个Class类型的isa指针,Class就是一个objc_class类型的结构体指针,objc_class又继承自objc_object结构体,而objc_object内部只有一个isa指针
如下:
arduino
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
}
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
所以objc_class结构体可以转化为如下:
struct objc_class {
Class isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() {
return bits.data();
}
}
所以OC对象底层实现结构体里存放的信息有:
- isa指针,是继承自objc_object的属性
- superclass表示当前类的父类
- cache 是方法缓存表。
- bits是class_data_bits_t类型的属性,用来存放类的具体信息。(方法,属性,协议等等)
2. 两张表class_rw_t和class_ro_t的区别
在结构体class_rw_t中存放着
- 方法列表methods
- 属性列表properties
- 协议列表protocols。
- 一个class_ro_t类型的只读变量ro
class_ro_t中存放着类最原始的方法列表,属性列表等等,这些在编译期就已经生成了,而且它是只读的,在运行期无法修改。而class_rw_t不仅包含了编译器生成的方法列表、属性列表,还包含了运行时动态生成的方法和属性。它是可读可写的。
arduino
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro; //只读的属性ro
method_array_t methods; //方法列表
property_array_t properties; //属性列表
protocol_array_t protocols; //协议列表
Class firstSubclass;
Class nextSiblingClass;
}
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize; //当前instance对象占用内存的大小
const uint8_t * ivarLayout;
const char * name; //类名
method_list_t * baseMethodList; //基础的方法列表
protocol_list_t * baseProtocols;//基础协议列表
const ivar_list_t * ivars; //成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;//基本属性列表
}
3. 获取对象内存大小的方法
-
sizeof
1,sizeof是一个操作符,不是函数。 2,我们在用sizeof计算内存大小时,一般传入的是数据类型,在编译阶段就能确定大小而不是在运行时确定 3,
sizeof
最终得到的结果是该数据类型占用空间的大小例如,sizeof(int)为4,sizeof(long)为8 一个isa指针占用8个字节
-
class_getInstanceSize
是runtime提供的API,本质是
获取类的实例对象中成员变量所占用的内存大小
,采用8字节对齐方式 -
malloc_size 系统给对象实际分配的内存大小。采用16字节对齐方式。所以有的时候,实际分配的和实际占用的内存大小并不相等。
通过源码可知:
-
对于一个对象来说,其真正的对齐方式 是 8字节对齐,8字节对齐已经足够满足对象的需求了
-
apple系统为了防止一切的容错,采用的是16字节对齐的内存,主要是因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。 juejin.cn/post/694957...
4. OC对象的分类
1,
分为三大类:实例对象(instance)、类对象(class)、元类对象(meta class)
2,
实例对象存储的信息:
- isa指针(指向它的类对象)
- 其他的成员变量的具体值
类对象存储的信息:
- isa指针(指向它的mata-class对象)
- superClass(指向它的父类的class对象)
- 属性信息(properties),存放着属性的名称,属性的类型等等,这些信息在内存中只需要存放一份
- 对象方法信息(methods)
- 协议信息(protocols)
- 成员变量描述信息等等(ivars)
元类对象存储的信息:
- isa指针(指向基类对象mata-class)
- superClass(指向父类对象的mata-class)
- 类方法信息(class method)
经典图来啦。。
6. alloc、init、new源码分析
1,alloc分析:
-
通过对
alloc
源码的分析,可以得知alloc的主要目的就是开辟内存
,而且开辟的内存需要使用16字节对齐算法
,现在开辟的内存的大小基本上都是16
的整数倍 -
开辟内存的核心步骤有3步:
计算 -- 申请 -- 关联
1)计算所需内存大小
cls->instanceSize
2)申请内存,返回指向内存地址的指针
calloc
3)类与isa相关联
obj->initInstanceIsa
2,init分析:
- init是一个构造方法,主要用于给用户提供构造方法入口,初始化一些数据的,返回的是传入的self本身
3,new分析:
- 初始化除了
init
,还可以使用new
,两者本质上并没有什么区别,通过源码可以得知,new函数中直接调用了callAlloc函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [alloc init]
的结论
5. 知道NSProxy吗?
二,runtime原理
理解两个概念
1,编译时: 源代码翻译成机器代码能识别 的过程,主要是对代码进行基本的检查错误,比如语法分析等,如果有语法错误,则编译报错,是一个静态的过程
2,运行时: 代码成功跑起来后,被装载到内存里 的过程,如果出错,则程序会崩溃,是一个动态的过程
基础概念
Runtime
被称为运行时。
1,OC是一门动态性比较强的语言,允许很多操作推迟到程序运行时再进行。
2,OC的动态性就是由runtime
来支撑和实现的,runtime
是由C和汇编实现的一套API
, 封装了很多动态性相关的函数,平时编写的OC代码,底层都是转成了runtime API进行调用
在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。而在 Objective-C 中,[object foo] 语法并不会立即执行 foo 这个方法的代码。它是在运行时给 object 发送一条叫 foo 的消息。这个消息,也许会由 object 来处理,也许会被转发给另一个对象,或者不予理睬假装没收到这个消息。多条不同的消息也可以对应同一个方法实现。这些都是在程序运行的时候决定的。
OC方法的本质
OC对象的本质
是一个包含isa指针和其他信息的结构体
OC方法调用的本质
是objc_msgSend消息发送
- 方法调用又涉及到方法在类中的查找流程,
objc_msgSend
可分为快速查找
和慢速查找
消息发送之快速查找
即缓存查找流程
,走CacheLookup
,也就是所谓的sel-imp快速查找流程
arduino
struct objc_object {
Class _Nonnull isa __attribute__((deprecated)); //8字节
}
struct objc_class : objc_object {
// Class ISA; //8字节
Class superclass; //Class 类型 8字节
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
//....方法部分省略,未贴出
}
1,isa首地址平移16字节(如上,由于在objc_class中,isa首地址占8字节,superclass占8字节,所以cache距离首地址16字节),获取cache,cache(本质也是结构体类型,占8字节,即占64位)中高16位存mask,低48位存buckets,即p11 = cache
2,从cache中分别取出buckets和mask,并由mask根据哈希算法计算出存储sel-imp的bucket下标index
3,根据所得的哈希下标index和buckets首地址,取出哈希下标对应的bucket
4,根据获取的bucket,取出其中的imp存入p17,即p17 = imp, 取出sel存入p9,即p9 = sel
5,递归循环,比较获取的bucket中的sel与objc_msgSend的第二个参数的_cmd是否相等,如果相等,则直接跳转至CacheHit,即缓存命中,返回imp;如果不相等,1)如果一直都查找不到,会跳转至__objc_msgSend_uncached,即进入慢速查找流程。2)
补充个小知识: 二进制位左移和右移 左移(<<)是将一个二进制位的操作数按指定移动的位数向左移动,移出位被丢弃,右边移出的空位一律补0。 右移(>>)是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0,或者补符号位,这由不同的机器而定。 在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1。
到此,我们对objc_msgSend(reciver,_cmd)
到找到imp
做一个简单总结:
- class位移16位得到cache_t;
- cache_t与上mask掩码得到mask地址,
- cache_t与上bucket掩码得到bucket地址;
- mask与上sel得到sel-imp下标index,
- 通过bucket地址内存平移,可以得到第index位置的bucket;
- bucket里面有sel、imp;然后拿bucket里的sel和msg_msgSend的_cmd参数进行比较是否相等;
- 如果相等就执行cacheHit,cacheHit里面做的就是拿到sel对应的imp,然后进行imp^Class,得到真正的imp地址,最后调用imp函数 。
- 如果不相等,就拿到bucket进行- - (减减)平移,找到下一个bucket进行比较,如果找到了就进入7,否则就继续缓存查找。如果一直找不到,就进入__objc_msgSend_uncached慢速查找函数。
- 慢速查找流程:lookUpImpOrForward二分法查找imp,找到了就写入缓存;
当前类找遍了没有,就进入递归循环:
- 再从父类开始,快速查找、慢速查找。还是没找到,就从父类的父类开始循环这一步;
- 递归结束条件是class为空,然后给imp一个默认值。
消息发送之慢速查找
- 再次从缓存查找一次
- 如果缓存还是没找到,去类对象的方法表里查找方法,如果找到,就保存到缓存,并执行这个方法。
- 如果类对象方法表里也没找到,就先去父类的缓存表里找,如果缓存表也没找到,就取找父类的方法表,如果找到,同样缓存方法到缓存,如果还是没找到,继续往上一层父类查找。
- 以此类推,直到找到基类,即NSObject类的方法表。
- 到了基类还是没找到,那么就先判断自己是不是元类,不是元类的话调用resolveInstanceMethod方法;是元类的话,先调用resolveClassMethod方法,如果也没找到就调用resolveInstanceMethod方法,去元类的对象方法中查找,因为类方法在元类中是实例方法。
- 如果resolveInstanceMethod方法或者resolveClassMethod方法也没被调用,开启转发流程。
- 先调用forwardingTargetForSelector,如果这个方法返回nil,继续调用methodSignatureForSelector,如果返回不为空继续调用forwardInvocation;如果还是为空,调用doesNotRecognizeSelector,则闪退报错
Runtime之消息转发
Runtime的具体应用
1).利用关联对象AssociatedObject给分类添加属性
objectivec
//关联对象
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
//获取关联的对象
id objc_getAssociatedObject(id object, const void *key)
//移除关联的对象
void objc_removeAssociatedObjects(id object)
eg.
1)分类里添加属性
@interface LLPerson (Test)
@property (nonatomic, copy) NSString *className;
@end
2)重写get/set方法
@implementation LLPerson (Test)
- (void)setClassName:(NSString *)className {
objc_setAssociatedObject(self, @selector(className), className, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)className {
return objc_getAssociatedObject(self, _cmd);
}
@end
注意
- 关联对象并不是存储在被关联对象本身内存中
- 关联对象存储在全局的统一的一个
AssociationsManager
中 - 设置关联对象为
nil
,就相当于是移除关联对象 - 当
object
对象被释放,关联对象的值也会对应的从内存中移除(内存管理自动做了处理)
2).利用消息转发机制解决方法找不到的异常问题(动态方法决议->方法转发)
less
/*** a,动态方法解析 **/
//a1,如果是类方法,应实现 +(BOOL)resolveClassMethod:(SEL)sel)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
Method method = class_getInstanceMethod(self, NSSelectorFromString(@"test2"));
if (method_getImplementation(method)) {
//添加方法
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
return YES;
}
}
return [super resolveInstanceMethod:sel];
}
/*** b,消息快速转发 **/
//b1,如果上面的方法resolveInstanceMethod没实现,或者即使实现,但没增加新的方法以及其实现,不管返回YES还是NO,都会调用下面forwardingTargetForSelector:方法
//b2,如果是类方法,记得是实现+(id)forwardingTargetForSelector:(SEL)aSelector
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return [[LLStudent alloc] init];//会去调用LLStudent的对象方法test
}
return [super forwardingTargetForSelector:aSelector];
}
/*** c,消息慢速转发 **/
//c1,如果上面的forwardingTargetForSelector也没实现,或者实现了,但返回nil,就会走到下面两个方法,进行消息慢速转发
//c2,如果是类方法,记得是实现对应的+方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//会去调用LLStudent的对象方法test
[anInvocation invokeWithTarget:[[LLStudent alloc] init]];
}
如果上面-methodSignatureForSelector:
返回nil
,Runtime
则会发出 -doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime
就会创建一个NSInvocation
对象并发送 -forwardInvocation:
消息给目标对象。
3).交换方法实现
scss
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(personTestInstance);
SEL swizzledSelector = @selector(personTestInstance1);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//添加方法:旧方法的SEL--新方法的实现IMP--新方法的encoding
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
//如果添加成功,则替换方法:新方法的SEL--旧方法的实现IMP--旧方法的encoding
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
//否则,交换方法
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
4).KVO的实现
objectivec
- (void)viewDidLoad {
[super viewDidLoad];
_person = [Person alloc];
[_person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
_person.nickName = @"嘻嘻";
}
// 响应方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
NSLog(@"%@ - %@ - %@",keyPath,object,change);
}
// 移除观察者
- (void)dealloc{
[_person removeObserver:self forKeyPath:@"nickName"];
}
即键值观察。提供了一种当其它对象属性(注意是只针对属性,成员变量没用)
被修改的时候能通知当前对象的机制。在MVC大行其道的Cocoa中,KVO机制很适合实现model和controller类之间的通讯。
KVO
的实现依赖于 Objective-C
强大的 Runtime
,当观察某对象 A
时,KVO
机制动态创建一个对象A
当前类的子类,并为这个新的子类重写了被观察属性 keyPath
的 setter
方法。setter
方法随后负责通知观察对象属性的改变状况。
Apple
使用了isa-swizzling
来实现KVO
。- 当观察对象
A
时,KVO
机制动态创建一个新的名为:NSKVONotifying_A
的中间类,该类是A
类的子类,对象A
的isa
,由原有类更改为指向中间类 - 中间类重写了被观察属性的
setter 方法
、class
、dealloc
、_isKVO
方法 dealloc
方法中,移除KVO
观察者之后,实例对象isa
指向由中间类改为原有类- 中间类在移除观察者后也并不会被销毁
5).实现NSCoding的自动归档和解档 用处:数据持久化 原理描述:用runtime
提供的函数遍历Model
自身所有属性,并对属性进行encode
和decode
操作。 核心方法:在Model
的基类中重写方法:
ini
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super init]) {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[self setValue:[aDecoder decodeObjectForKey:key] forKey:key];
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
unsigned int outCount;
Ivar * ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i ++) {
Ivar ivar = ivars[i];
NSString * key = [NSString stringWithUTF8String:ivar_getName(ivar)];
[aCoder encodeObject:[self valueForKey:key] forKey:key];
}
}
6).实现字典转模型(项目里用到的YYKit里的YYModel)、修改textfield占位文字颜色等(遍历类的所有成员变量进行动态修改)
三,分类category
arduino
// 定义在objc-runtime-new.h文件中
struct category_t {
const char *name; // 比如给Student添加分类,name就是Student的类名
classref_t cls;
struct method_list_t *instanceMethods; // 分类的实例方法列表
struct method_list_t *classMethods; // 分类的类方法列表
struct protocol_list_t *protocols; // 分类的协议列表
struct property_list_t *instanceProperties; // 分类的实例属性列表
struct property_list_t *_classProperties; // 分类的类属性列表
};
好处: 1,减少单个文件的体积 2,可以把不同的功能组织到不同的category里 3,按需加载想要的category等等
底层原理:
1,Category编译之后的底层结构其实是一个category_t类型的结构体,里面存储着分类的对象方法,类方法,属性,协议等信息
2,在编译阶段分类的相关信息和现有类的相关信息是分开的,等到运行阶段,系统就会通过runtime加载现有类的所有category数据,把所有category的方法,属性,协议数据分别合并到一个数组中,然后将合并后的数据插入到现有类数据的前面。
3,(以新增方法为例),合并后分类的方法在前面(不同分类的相同方法,最后参与编译的那个分类的方法列表在最前面),本类的方法列表在最后面。所以当分类中有和本类同名的方法时,调用的实际上是分类中的方法。从这个现象来看,好像是本类的方法被分类中同名的方法覆盖了,实际上并不是,只是调用方法时最先查找到了分类的方法所以就执行分类的方法。
4,分类可以添加属性,但不能添加成员变量,定义成员变量的话编译器会直接报错。
1)从category_t
结构体里存储的信息就可以看出,并没有定义存储成员变量的列表 2)如果我们在Person
的分类
中定义一个属性@property (nonatomic , strong) NSString *name;
,编译器只会帮我们声明- (void)setName:(NSString *)name;
和- (NSString *)name;
这两个方法,而不会实现这两个方法,也不会定义成员变量。所以此时如果我们在外面给一个实例对象设置name属性值peron.name = @"Jack"
,编译器并不会报错,因为setter方法是有声明的,但是一旦程序运行
,就会抛出unrecognized selector
的异常,因为setter方法没有实现。
5,类扩展Extension和分类Category的实现是一样的吗? 不一样。 类扩展只是将.h文件中的声明放到.m中作为私有来使用,编译时就已经合并到该类中了。 分类中的声明都是公开的,而且是利用runtime机制在程序运行时将分类里的数据合并到类中
6, +load方法 和 +initialize方法区别
-
initialize
是通过objc_msgSend
进行调用的,而load
是找到函数地址直接调用的 -
如果子类没有实现
initialize
,会调用父类的initialize
- 所以父类的
initialize
可能会被调用多次,第一次是系统通过消息发送机制调用的父类initialize
,后面多次的调用都是因为子类没有实现initialize
,而通过superclass
找到父类再次调用的
- 所以父类的
-
如果分类实现了
initialize
,就覆盖类本身的initialize
调用
四,Block底层原理
方式一,typedef声明
objectivec
typedef void(^LLBlockA)(void);
typedef int(^LLBlockB)(int i,int j);
@property (nonatomic, copy) LLBlockA block;
@property (nonatomic, copy) LLBlockB blockB;
self.blockB = ^int(int i, int j) {
NSLog(@"test1:age--%ld",self.age);
return i+j;
};
NSLog(@"block1-------%d",self.blockB(10, 35));
方式二
scss
- (void)testBlockA {
void(^blockA)(void) = ^ {
NSLog(@"testBlockA");
};
//无参无返回值,全局block <__NSGlobalBlock__: 0x100004278>
NSLog(@"blockA--%@",blockA);
}
- (void)testBlockB {
int a = 10;
void(^blockB)(void) = ^ {
NSLog(@"testBlockB--%d",a);
};
//访问了外部变量,堆区block <__NSMallocBlock__: 0x10722ceb0>
NSLog(@"blockB--%@",blockB);
}
- (void)testBlockC {
int a = 10;
void(^__weak blockC)(void) = ^ {
NSLog(@"testBlockC--%d",a);
};
//使用了__weak修饰,变成了栈区block <__NSStackBlock__: 0x7ff7bfeff1e8>
NSLog(@"blockC--%@",blockC);
}
1,block类型
- 全局block
__NSGlobalBlock__
,block直接存储在全局区
,无参无返回值 - 堆区block
__NSMallocBlock__
,如果此时的block是强引用
,并且访问了外部变量 - 栈区block
__NSStackBlock__
,如果此时的block是弱引用
,使用了__weak修饰,并且访问了外部变量
2,block循环引用
-
正常释放
:是指A持有B的引用,当A调用dealloc方法,给B发送release信号,B收到release信号,如果此时B的retainCount(即引用计数)为0时,则调用B的dealloc方法 -
循环引用
:A、B相互持有,所以导致A无法调用dealloc方法给B发送release信号,而B也无法接收到release信号。所以A、B此时都无法释放
objectivec
//代码一
[UIView animateWithDuration:0.5 animations:^{
NSLog(@"%@",self.name);
} completion:nil];
//代码二
NSString *name = @"zhangsan";
self.block = ^(void){
NSLog(@"%@",self.name);
};
self.block();
以上代码会产生循环引用吗? 答案是代码一不会,代码二会。
代码一 虽然也使用了外部变量,但是self
并没有持有animation
的block
,仅仅只有animation
持有self
,所以不构成相互持有
代码二 self
持有了block
, block
实现体里又使用了self
的属性,通过编译后底层代码得知,block
持有了self
,那么self
和block
就相互持有,就会产生循环引用
3,解决循环引用
五,事件传递及响应链机制
1. 响应者(UIResponder
)
iOS里并不是所有对象都能接收和处理事件,在UIKit
中我们使用响应者对象(Responder
)接收和处理事件。一个响应者对象一般是UIResponder
类的实例或者继承UIResponder
类,例如 UIView,UIViewController,UIApplication,AppDelagate
等都继承自UIResponder
,意味我们日常使用的控件几乎都是响应者。
2. 事件(UIEvent
)
事件分为很多种,比如UITouch触摸事件、UIPress、加速计、远程控制事件
等,UIResponder
都可以处理这些事件,本篇仅讨论UITouch触摸事件,即手指触摸屏幕产生的UITouch对象
。
在 UITouch
内,存储了大量触摸相关的数据,当手指在屏幕上移动时,所对应的 UITouch
数据也会更新,例如:这个触摸是在哪个 window
或者哪个 view
内发生的?当前触摸点的坐标是?前一个触摸点的坐标是?当前触摸事件的状态是?这些都存储在 UITouch
里面。
3. 事件传递链
当触摸发生后,从后向前,从里向外 ,UIApplication
会触发 sendEvent
方法 将一个封装好的 UIEvent
传给 UIWindow
,也就是当前展示的 UIWindow
,通常情况接下来会传给当前展示的 UIViewController
,接下来传给 UIViewController
的根视图,依次往前将触摸事件传递下去。即
a. ---> UIApplication -> UIWindow -> UIViewController -> UIViewController的view -> 子view -> ...
或者
b. ---> UIApplication -> UIWindow -> UIWindow的rootView -> 子view -> ...
注意: 不止UIView可以响应事件,只要是UIResponse的子类,都可以响应和传递事件
4. 确定第一响应者
UIKit提供了命中测试(hit-test)来确定触摸事件的第一响应者。如下图
注意事项:
1). 在步骤1里,以下3种情况出现任意的一种,都无法接收事件
view.isHidden = true
view.isUserInteractionEnabled = false
view.alpha <= 0.01
2). 查看触摸点坐标是否在当前视图内部 使用了- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event
方法,可被重写
3). 如果当前视图有若干个子视图,要根据FILO原则,后添加的先遍历
以下为hitTest
的内部判断逻辑
objectivec
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//3种状态无法接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) {
return nil;
}
if ([self pointInside:point withEvent:event] == NO) {
//触摸点不在当前视图内部
return nil;
}
NSInteger count = self.subviews.count;
//FILO 后添加的先遍历
for (NSInteger i = count -1; i >= 0; i--) {
UIView *subview = self.subviews[i];
//坐标转换------------把 触摸点 在当前视图上的坐标位置转换为在子视图上的坐标位置
CGPoint subviewP = [self convertPoint:point toView:subview];
//或者 CGPoint subviewP = [subview convertPoint:point fromView:self];
//寻找子视图中的第一响应者视图
UIView *resultView = [subview hitTest:subviewP withEvent:event];
//触摸点是否在子视图内部,在就返回子视图
if (resultView) {
return resultView;
}
}
//当前视图的所有子视图都不符合要求,而触摸点又在该视图自身内部,所以返回当前视图
return self;
}
因此hitTest
的作用有两个:
一是用来询问事件在当前视图中的响应者,返回的是最终响应这个的事件的响应者对象;
二是事件传递的一个桥梁;
举个栗子如下:
整个命中测试的走向是这样的: A✅ --> D❎ --> B✅ --> C❎ >>>> B
,所以B
是触摸事件第一响应者
5. 事件响应链
确定了第一响应者
之后,整个响应链也随着确定下来了。所谓响应链是由响应者组成的一个链表,链表的头是第一响应者,链表的每个结点的下一个结点都是该结点的 next
属性。
以上响应链
就是:B -> A ->UIViewController的根视图 -> UIViewController对象 -> UIWindow对象 -> UIApplication对象 -> App Delegate
. 或者 B -> A -> UIWindow对象 -> UIApplication对象 -> App Delegate
事件按照响应链依次响应,触发touchesBegan
等方法。若第一响应者在这个方法中不处理这个事件,则会传递给响应链中的下一个响应者触发该方法处理,若下一个也不处理,则以此类推传递下去。若到最后还没有人响应,则会被丢弃(比如一个误触)。
总结:
触摸屏幕后事件的传递可以分为以下几个步骤:
1). 通过「命中测试」来找到「第一响应者」
2). 由「第一响应者」来确定「响应链」
3). 事件沿「响应链」响应
4). 若「响应链」上的响应者不处理该事件,则传递给下一个响应者,若下一个也不处理,则以此类推传递下去。若到最后还没有人响应,则该事件会被丢弃
还有 多线程,内存管理,性能优化,离屏渲染 等知识整理未完待续...