OC的常见知识点01

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

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

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

  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给分类添加属性

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

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

5).实现NSCoding的自动归档和解档 用处:数据持久化 原理描述:用runtime提供的函数遍历Model自身所有属性,并对属性进行encodedecode操作。 核心方法:在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并没有持有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. 事件传递链

当触摸发生后,从后向前,从里向外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). 若「响应链」上的响应者不处理该事件,则传递给下一个响应者,若下一个也不处理,则以此类推传递下去。若到最后还没有人响应,则该事件会被丢弃

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

相关推荐
Cao_Ron15 分钟前
用 SVG image symbol 画 ECharts graph 圆形节点
前端
hxy060120 分钟前
React-Call让你的React组件像异步函数一样调用
前端·react.js
Hilaku21 分钟前
为什么 AI 写前端时,总是优先选择 React,而不是 Vue?
前端·javascript·程序员
京东云开发者22 分钟前
GPT-5.6、ChatGPT Work 与 Codex 更新食用指南
前端
Cao_Ron34 分钟前
用 ECharts graph 画出卡片式矩形节点
前端
skiyee42 分钟前
换个底座的 Wot Starter 居然让 AI 更懂项目!
前端·ai编程
anyup1 小时前
这一套绝了!AI 大模型写故事,星云 SDK 驱动 3D 数字人实时演绎
前端·人工智能·aigc
耀耀切克闹灬1 小时前
短链跳转
前端
孟陬1 小时前
高质量全方位的 AI 工具 sse-stuntman — Mock SSE 接口
前端·node.js·openai
Cao_Ron1 小时前
给 ECharts 6 graph 节点叠加角标:从原理到性能优化
前端