iOS 消息机制

1,696 阅读9分钟

消息机制

概述

在 OC 中, 方法在类中以 方法名:sel + 函数指针:imp 的形式存储. 方法的调用底层实际是在发送消息, 消息接收者就是调用者, 消息主体就是 sel, 即方法名和参数的信息. 发送消息实际就是在调用 objc_msgSend 这个函数, 第一个参数是消息接收者, 第二个参数就是消息主体. 所以说 方法的调用 底层是 消息机制.

objc_msgSend 函数首先是根据调用者的 isa 信息, 沿着继承链找到方法存储的位置, 然后根据 sel 去方法列表中找到对应的 imp, 然后进行调用; 如果整个继承链都没找到, 系统最后会给三次挽救没有实现的方法的机会, 如果这三次机会都没把握住, 就会报一个经典的错误, 即方法找不到; 所以整个方法调用过程主要分为两个部分, 一是查找 imp, 二是挽救方案;

对象方法和类方法的区别 :
在具体讲解消息机制前有必要再说一下对象方法和类方法的区别, 这样后边讲解起来比较方便, 对象方法存在类的方法列表中, 类方法存在元类的方法列表中, 类也可以理解为类对象, 元类是系统生成的. 所以没有特殊说明的话, 后边说的方法就包含 对象方法类方法, 对象也包含类对象.

消息流程

如果找到 imp 就去调用了, 没什么可说的, 所以主要说的是, 如果找不到, 下一步怎么找, 去哪里找, 最终怎么处理, 这样一个过程.

首先会在外部定义一个 imp, 找到就赋值. 然后去调用.

一. 发送消息 - 查找 imp

1. 快速查找

objc_msgSend 会根据传入对象的 isa, 找到类, 再找到类的 cache, cache 是类的方法的缓存结构, 调用过的方法会缓存 cache 中可, 然后根据 selcache 中查找对应的 imp 函数指针, 这个过程是在内存里的操作, 所以称为快速查找, 另处一点, 苹果为了效率更高, 这段代码是用汇编写的, 这在 objc 源码文件都是可以找到的.

imp 就是函数的指针地址, 也可以理解成函数名.

如果在 cache 中找到了 imp, 就去调用 imp, 如果没有找到, 说明这个方法是第一次调用, 接着就会进入慢速查找过程.

从父类继承过来的方法在父类的 cache 中存着, 会在慢速查找过程中找;

2. 慢速查找

当方法在 cache 中没找到的时候, 就会跳转到 lookUpImpOrForward 进入慢速查找, 这个方法是 C++ 写的, 即从汇编跳转到C++.

注意 : 在开始慢速查找之前, 还会再进行一次快速查找, 这是因为受多线程的影响, 有可能就在这个间隔中其他线程调用了, 刚好已经插入到 cache 中.

开始正式的慢速查找:

  1. 在本类的 bits 结构中取出 method_list, 根据 sel 查找 imp, 当找到后还要做一件事, 从当前下标往前找, 直到找到最前边的, 这是因为这个类的分类有可能重写的这个方法, 而分类的方法在 method_list 是排在前边的, 这也是分类方法优先级高的原因.
  2. 如果本类中找不到, 就会沿着继承链到 父类cache 中查找;
  3. 如果在父类的 cache 中也找不到, 就会沿着继承链重复前两步;

forward_imp :
如果最终到 NSObject 基类整个过程中都没能找到, 此时 imp 这个临时变量就会先赋值一个系统函数指针 forward_imp, 这个函数就是会抛出 unrecognized selector sent to instance xxx 这个错误, 然后系统主动停止程序. 实际是方法找不到并不会导致程序崩溃, 可以说是人为的停止, 因为这个错误已经是影响程序的正确执行了.

注意:

在查找类方法时, 实际是在元类中查找, 如果在根元类中都没有找到, 因为根元类的父类是 NSObject, NSObject 的元类也是根元类, 所以最终会找到 NSObject, 看看 NSObject 有没有同名的对象方法, 如果有同名的对象方法, 也算是找到了, 就会调用这个对象方法.

所以就会有这么一种情况, 当调用一个类方法 A 时, 整个继承链都没有这个类方法, 而 NSObject (包括分类) 有一个对象方法 A, 此时并不会报错崩溃 , 而是这个对象方法会被调用.

补充: 在第 1 步中的 method_list 中查找的时候, 用的是二分查找算法, sel 经过转换后是有序的;

二. 挽救方案

如果在整个继承链中都没能找到 sel 对应的 imp, 接下来就会进入挽救方案的流程, 系统给了 3次挽救的机会, 也可以说是容错机制.

2.1 方法决议

imp 这个临时变量被赋值 forward_imp 之后, 就会进入方法决议阶段. 所谓的方法决议就是有两个类方法, 一个是解决对象方法的 +(BOOL)resolveInstanceMethod:(SEL)sel, 一个解决类方法的 +(BOOL)resolveClassMethod:(SEL)sel, 系统会给当前类的发一个消息, 调用对应的决议方法, 这个消息和上边是一样的, 也会沿着继承链向上查找, 一直找到 NSObject 基类.

如果这个类的继承链中有实现了对应的决议方法. 那么就会调到这个方法中来, 同时 sel 也通过参数传了过来, 可以在这个方法中做些处理, 比如利用 runtime 可以给 sel 绑定一个 imp, 然后返回 YES, 这样有了 imp, 就会调用到相应的方法中, 而不至于崩溃. 比如还可以将 sel 相关的信息, 类, 方法名 这些信息反馈到服务器后, 返回 NO 让程序继续往下走.

例如: 调用一个对象方法 - (void)say666;, 这个方法又没有实现, 就会来到下面这个对象决议方法里, 但是这个方法有 - (void)sayHello; 这个方法的实现, 就可以按如下方案处理; 类方法同理会进入类决议方法中, 然后根据我们自己的需求去处理就行;

// 对象决议方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {

    NSLog(@"%@ 来了", NSStringFromSelector(sel));

    if ([NSStringFromSelector(sel) isEqual: @"say666"]) {
        // 获取 sayHello 的 imp
        IMP imp = class_getMethodImplementation(self, @selector(sayHello));
        // 获取 sayHello 的 Method
        Method sayM = class_getInstanceMethod(self, @selector(sayHello));
        // 获取 sayHello 签名类型
        const char *type = method_getTypeEncoding(sayM);
        // 把 imp 绑定到 sel 上
        return class_addMethod(self, sel, imp, type);
    }
    
    return [super resolveInstanceMethod:sel];
}

// 类决议方法
+ (BOOL)resolveClassMethod:(SEL)sel {

    // 处理流程 ...
    
    return [super resolveClassMethod:sel];
}

注意: 当进行类方法决议的时候, 实际是沿着元类的继承链在找, 如果到 NSOject 基类, 都还没有找到, 会在 NSOject 进行一个对象方法决议, 因为元类的父类也是 NSOject, 最终会找一下 NSOject 有没有实现 (BOOL)resolveInstanceMethod:(SEL)sel.

如果最终没有找到方法决议. 就会进入下面的消息转发流程, 首先进入的是 快速转发, 如果还没有解决就进入慢速转发.

2.2 快速转发

快速转发会调用当前类或者继承链中的 - (id)forwardingTargetForSelector:(SEL)aSelector, 返回一个有能力处理这个消息的对象. 就是说要么这个新对象有 sel 这个同名方法的对象(类对象), 要么给这个 sel 绑定一个 imp, 返回 self 自己继续处理这个消息. 总之这个消息可以被处理. 程序就可以继续执行.

- (id)forwardingTargetForSelector:(SEL)aSelector {

    // 假设 Teacher 这个类能处理这个消息, 
    // 也就是说 Teacher 这个类有 aSelector 这个方法
    return [Teacher alloc];
}

2.3 慢速转发

慢速转发会先调用 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 要求返回一个方法的签名, 然后会调用 - (void)forwardInvocation:(NSInvocation *)anInvocation 这个方法, 抛出 事务, 如果这两个方法实现了, 方法签名返回的合法(正确的), 那么程序就不会崩溃了. 系统将事务抛出, 开发者接收, 就说明已经处理了, 所以就不会崩溃. 至于这个事务最终是否处理取决于开发者;

开发者可以修改这个事务的消息接收者 target, selector, 然后去执行这个事务; 也可以暂时先将这个事务保存起来, 找到合适时机再执行;

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {

    anInvocation.target = [Teacher alloc];
    //anInvocation.selector
    
    [anInvocation invoke];
}
  • 关于方法签名的 type, 表示返回值类型, 参数类型什么的, 有一张对照表. image.png

三. 抛出错误

如果三种挽救方案的机会都没能把握住, 那么最后只能抛出错误. 就是调用 imp, 即慢速查找结束时赋值的系统函数 forward_imp, 终止程序.

forward_imp 指向的就是下面这个函数, 这里也说明了, 在底层根本就没有什么 + - 方法之分, 是人为加上去的.

__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
    _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
                "(no message forward handler is installed)", 
                class_isMetaClass(object_getClass(self)) ? '+' : '-', 
                object_getClassName(self), sel_getName(sel), self);
}

总结

主要流程

  1. 对象 -> isa -> 类 -> cache;
  2. 类的bits -> methodlist;
  3. 沿着继承链向父类的方向查找, cachemethodlist;
  4. 方法决议: resolveInstanceMethod 或者 resolveClassMethod;
  5. 快速转发: forwardingTargetForSelector;
  6. 慢速转发: methodSignatureForSelectorforwardInvocation;
  7. 抛出错误: forward_imp -> unrecognized selector sent to instance xxx.

结束语

至此消息机制主要流程结束. 当然这个过程中省去很多细节, 比如线程控制, 加锁, 判断, 断言, 消息流程的状态等等, 有兴趣的可以下载 objc 源码去看看, 研究一下.

如果想对消息机制理解的更透彻些, 那就少不了 类的结构isa 走位图这两块知识. 请参考其他几篇文章;

isa 指针走向
类内存布局之 bits
类内存布局之 cache

  • isa 走位图 image.png