【iOS面试#3】方法与消息的简单分析

761 阅读4分钟

1. 消息的本质

调用objc_msgSend,第一个参数是消息接收者,第二个是方法编号

objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
objc_msgSend(person,sel_registerName("NB"));

2. 方法查找

1). 开始查找

  • ① 开始objc_msgSend

  • ② 判断消息接收者是否为空,为空直接返回

  • ③ 判断tagged_pointers(之后会讲到)

  • ④ 取得对象中的isa

  • ⑤ 根据isa进行mask地址偏移得到对应的上级对象(类、元类)

  • ⑥ 开始在缓存中查找imp——开始了快速流程

2). 快速查找流程

objc_class结构

struct objc_class : objc_object {

    // Class ISA; //8            //继承了一个ISA
    
    Class superclass; //8

    cache_t cache;//16           // formerly cache pointer and vtable

    class_data_bits_t bits;// 存放数据    // class_rw_t * plus custom rr/alloc flags
    
    class_rw_t *data() const { //获取数据的方法
        return bits.data();
    }
}

cache_t结构

struct cache_t {

private:
    //显示原子性,保证增删改查线程安全
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; //4
#if __LP64__
            uint16_t                   _flags;     //2
#endif
            uint16_t                   _occupied;  //2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
}

image.png

  • ① 通过receiver消息接收者首地址平移16字节(isa 8字节 + superClass 8字节)获得cache

  • ②从cache中分别取出bucketsmask,并由mask根据哈希算法计算出哈希下标

    • 通过cache掩码(即0x0000ffffffffffff)& 运算,将高16位mask抹零,得到buckets指针地址
    • cache右移48位,得到mask
    • objc_msgSend的第二个参数_cmd & msak,通过哈希算法,得到需要查找存储sel-impbucket下标index = _cmd & mask,存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取,如下所示

image.png

  • ③ 根据所得的哈希下标index 乘以 单个bucket占用的内存16字节(sel占8字节,imp占8字节)得到偏移地址, 和通过buckets首地址加偏移地址,取出哈希下标index对应的bucket,通过获得的bucket取出selimp
  • ④ 循环比对 ③ 中得到的 selobjc_msgSend的第二个参数的_cmd是否相等,相等则缓存命中,返回imp,不等,则匹对 buckets的其他bucket,仍然没找到,则跳转至__objc_msgSend_uncached,进入 ==> 3). 慢速查找流程

image.png

3). 慢速查找流程

2). 快速查找流程 中进行查找没有找到,进行下面步骤:

  • ① 判断cls

    • 是否是已知类,如果不是,则报错
    • 类是否实现,如果没有,则需要先实现,确定其父类链,此时实例化的目的是为了确定父类链、ro、以及rw等,方便后续数据的读取以及查找的循环
    • 是否初始化,如果没有,则初始化
  • ② 循环查找,按照类继承链 或者 元类继承链的顺序查找

    • 当前cls的方法列表中使用二分查找算法查找方法,如果找到,则进入cache写入流程,并返回imp,如果没有找到,则返回nil

    • 当前cls被赋值为父类,如果父类等于nil,则imp = 消息转发,并终止递归,进入③

    • 如果父类链中存在循环,则报错,终止循环

    • 父类缓存中查找方法

      • 如果未找到,则直接返回nil,继续循环查找
      • 如果找到,则直接返回imp,执行cache写入流程
  • 判断是否执行过动态方法解析

    • 如果没有,执行动态方法解析
    • 如果执行过一次动态方法解析,则走到消息转发流程

总结

  • 对于对象方法(即实例方法),即在类中查找,其慢速查找的父类链是:类--父类--根类--nil
  • 对于类方法,即在元类中查找,其慢速查找的父类链是:元类--根元类--根类--nil
  • 如果快速查找、慢速查找也没有找到方法实现,则尝试动态方法决议
  • 如果动态方法决议仍然没有找到,则进行消息转发

4). 动态方法解析

resolveClassMethod

// 未实现的方法为A
// 替换实现的方法为B
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector("未实现A")) {
        // 获取已实现的方法B的IMP
        IMP imp = class_getMethodImplementation(self, @selector("已实现B"));
        // 获取实例
        Method m = class_getInstanceMethod(self, @selector("已实现B"));
        // 获取方法签名
        const char *type = method_getTypeEncoding(m);
        // 返回实现
        return class_addMethod(self, sel, imp, type);
    }
    // 未处理
    return [super resolveInstanceMethod:sel];
}

- (void)B {
    // B的实现
}

resolveClassMethod

+ (BOOL)resolveClassMethod:(SEL)sel{

    if (sel == @selector("未实现A")) {
        IMP imp = class_getMethodImplementation(objc_getMetaClass("类名"), @selector("已实现B"));
        Method m  = class_getInstanceMethod(objc_getMetaClass("类名"), @selector("已实现B"));
        const char *type = method_getTypeEncoding(m);
        return class_addMethod(objc_getMetaClass("类名"), sel, imp, type);
    }

    // 未处理
    return [super resolveClassMethod:sel];
}

+ (void)B {
    // B的实现
}

image.png

5). 消息转发

a). 快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == "未实现的方法") {
        return ["能解决问题的类" alloc]; //比如上传bug
    }
    return [super forwardingTargetForSelector:aSelector];
}

b). 慢速转发

慢速转发流程就是先methodSignatureForSelector提供一个方法签名,然后forwardInvocation通过对NSInvocation来实现消息的转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"]
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    anInvocation.target = ["解决问题的类" alloc];
    [anInvocation invoke];
}

image.png

总结:

未命名文件 (2).png