OC的常见知识点01

1,513 阅读18分钟

一,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指针

如下:

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不仅包含了编译器生成的方法列表、属性列表,还包含了运行时动态生成的方法和属性。它是可读可写的。

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)

经典图来啦。。

image.png

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吗?

juejin.cn/post/684790…

二,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快速查找流程

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
    
    //....方法部分省略,未贴出
}

image.png

pic1.webp

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做一个简单总结:

  1. class位移16位得到cache_t;
  2. cache_t与上mask掩码得到mask地址,
  3. cache_t与上bucket掩码得到bucket地址;
  4. mask与上sel得到sel-imp下标index,
  5. 通过bucket地址内存平移,可以得到第index位置的bucket;
  6. bucket里面有sel、imp;然后拿bucket里的sel和msg_msgSend的_cmd参数进行比较是否相等;
  7. 如果相等就执行cacheHit,cacheHit里面做的就是拿到sel对应的imp,然后进行imp^Class,得到真正的imp地址,最后调用imp函数 。
  8. 如果不相等,就拿到bucket进行- - (减减)平移,找到下一个bucket进行比较,如果找到了就进入7,否则就继续缓存查找。如果一直找不到,就进入__objc_msgSend_uncached慢速查找函数。
  9. 慢速查找流程:lookUpImpOrForward二分法查找imp,找到了就写入缓存;
    当前类找遍了没有,就进入递归循环:
  • 再从父类开始,快速查找、慢速查找。还是没找到,就从父类的父类开始循环这一步;
  • 递归结束条件是class为空,然后给imp一个默认值。
消息发送之慢速查找
  • 再次从缓存查找一次
  • 如果缓存还是没找到,去类对象的方法表里查找方法,如果找到,就保存到缓存,并执行这个方法。
  • 如果类对象方法表里也没找到,就先去父类的缓存表里找,如果缓存表也没找到,就取找父类的方法表,如果找到,同样缓存方法到缓存,如果还是没找到,继续往上一层父类查找。
  • 以此类推,直到找到基类,即NSObject类的方法表。
  • 到了基类还是没找到,那么就先判断自己是不是元类,不是元类的话调用resolveInstanceMethod方法;是元类的话,先调用resolveClassMethod方法,如果也没找到就调用resolveInstanceMethod方法,去元类的对象方法中查找,因为类方法在元类中是实例方法。
  • 如果resolveInstanceMethod方法或者resolveClassMethod方法也没被调用,开启转发流程。
  • 先调用forwardingTargetForSelector,如果这个方法返回nil,继续调用methodSignatureForSelector,如果返回不为空继续调用forwardInvocation;如果还是为空,调用doesNotRecognizeSelector,则闪退报错

Runtime之消息转发

Runtime的具体应用

1).利用关联对象AssociatedObject给分类添加属性

//关联对象 
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).利用消息转发机制解决方法找不到的异常问题(动态方法决议->方法转发)

/*** 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:返回nilRuntime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation 对象并发送 -forwardInvocation:消息给目标对象。

3).交换方法实现

+ (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);

        }
    });
}

image.png

4).KVO的实现

- (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当前类的子类,并为这个新的子类重写了被观察属性 keyPathsetter 方法。setter 方法随后负责通知观察对象属性的改变状况。

  • Apple 使用了 isa-swizzling 来实现 KVO
  • 当观察对象A时,KVO机制动态创建一个新的名为:NSKVONotifying_A的中间类,该类是A类的子类,对象Aisa,由原有类更改为指向中间类
  • 中间类重写了被观察属性的setter 方法classdealloc_isKVO方法
  • dealloc 方法中,移除 KVO 观察者之后,实例对象 isa指向由中间类改为原有类
  • 中间类在移除观察者后也并不会被销毁

5).实现NSCoding的自动归档和解档 用处:数据持久化 原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encodedecode操作。 核心方法:在Model的基类中重写方法:

- (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

// 定义在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方法区别

image.png

  • initialize是通过objc_msgSend进行调用的,而load找到函数地址直接调用的

  • 如果子类没有实现initialize,会调用父类的initialize

    • 所以父类的initialize可能会被调用多次,第一次是系统通过消息发送机制调用的父类initialize,后面多次的调用都是因为子类没有实现initialize,而通过superclass找到父类再次调用的
  • 如果分类实现了initialize,就覆盖类本身的initialize调用

四,Block底层原理

方式一,typedef声明

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));

方式二

- (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此时都无法释放

//代码一
[UIView animateWithDuration:0.5 animations:^{
    NSLog(@"%@",self.name);
} completion:nil];

//代码二
NSString *name = @"zhangsan";
self.block = ^(void){
    NSLog(@"%@",self.name);
 };
 self.block();

以上代码会产生循环引用吗? 答案是代码一不会,代码二会。

代码一 虽然也使用了外部变量,但是self并没有持有animationblock,仅仅只有animation持有self,所以不构成相互持有

代码二 self持有了block, block实现体里又使用了self的属性,通过编译后底层代码得知,block持有了self,那么selfblock就相互持有,就会产生循环引用

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. 事件传递链

image.png

当触摸发生后,从后向前,从里向外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)来确定触摸事件的第一响应者。如下图

image.png

注意事项:

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的内部判断逻辑

- (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的作用有两个:

一是用来询问事件在当前视图中的响应者,返回的是最终响应这个的事件的响应者对象;

二是事件传递的一个桥梁;

举个栗子如下:

image.png

整个命中测试的走向是这样的: 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). 若「响应链」上的响应者不处理该事件,则传递给下一个响应者,若下一个也不处理,则以此类推传递下去。若到最后还没有人响应,则该事件会被丢弃

还有 多线程,内存管理,性能优化,离屏渲染 等知识整理未完待续...