从Runtime源码角度,探究消息传递流程

Runtime是C、C++、汇编写的一套API,特性主要是消息(方法)传递,如果消息(方法)在对象中找不到,就进行转发,我们从以下几个方法去探究以下Runtime的实现机制:

  • Runtime介绍
  • Runtime消息传递
  • Runtime消息转发
  • Runtime应用

Runtime介绍

OC是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态创建类和对象,进行消息传递和转发。理解Runtime机制可以帮助更好的了解这个语言,了解底层,帮助我们可以更好的使用上层提供的方法。

  • 编译:编译就是编译器把源代码翻译成机器能够识别的代码
  • 运行:代码跑起来,被装载到内存中去

高级编程语言想要成为可执行文件需要先编译为汇编语言再汇编为机器语言,机器语言也是计算机能够识别的唯一语言,但是OC并不能直接编译为汇编语言,而是要先转写为纯C语言再进行编译和汇编的操作,从OC到C的过渡就是由runtime来实现的。然而我们使用OC进行面向对象开发,而C语言更多的是面向过程开发,这就需要将面向对象的类,转变为面向过程的结构体。

常见结构体介绍

我们知道对象本身是一个结构体,类也是,这里先来提前了解一下相关的概念:

  • 类对象(objc_class)
    OC类是由Class类型表示的,它实际上是一个指向objc_class结构体的指针:
typedef struct objc_class *Class;

objc/runtime.hobjc_class结构体的定义如下:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass; // 父类
    cache_t cache;             // 缓存
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

  // 存储方法列表 协议列表等
    class_rw_t *data() { 
        return bits.data();
    }
...
}

类对象就是一个结构体struct objc_class,结构体中包含的信息就是这些,这个结构体存放的数据被称为元数据(metadata)。
这个结构体继承objc_object,所以第一个成员变量也是isa指针,说明Class本身其实也是一个对象,称为类对象,类对象在编译器产生,用于创建实例对象。

cache_t cache存放着类的方法缓存,cache_t是增量扩展的哈希表结构。哈希表内部存储的是bucket_t,bucket_t中存储的是SEL和IMP的键值对。

  • 如果是有序方法列表,采用二分查找
  • 如果是无序方法列表,直接遍历查找

cache_t结构体:

// 缓存曾今调用过的方法,提高查找速度
struct cache_t {
    struct bucket_t *_buckets; // 散列表
    mask_t _mask; // 散列表的长度 - 1
    mask_t _occupied; // 已经缓存的方法数量,散列表的长度是大于已经缓存的数量的
....
}
struct bucket_t {
private:
    cache_key_t _key; // SEL作为key @selector()
    IMP _imp; // 函数的内存地址
...
}

散列表查找过程,在objc-cache.mm文件中:

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);

    bucket_t *b = buckets();
    mask_t m = mask();
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            return &b[i];
        }
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}

data()里面存放着方法列表,属性列表,协议列表等信息,是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; //

这三个都是二维数组,是可读可写的,包含了类的初始内容,分类的内容。
methods中,存储 method_list_t -> method_t
这三个二维数组中的数据有一部分是从class_ro_t中合并过来的
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;
}

const class_ro_t *ro;存储了当前类在编译期就已经确定的属性、方法以及遵循的协议:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    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;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

我们知道实例对象的isa指针指向该对象所属的类,那么类对象的isa指针指向谁呢?就是元类

  • 实例(objc_object)
/// Represents an instance of a class.
struct objc_object {
private:
    isa_t isa;
....
}

/// A pointer to an instance of a class.
typedef struct objc_object *id;

通过上面我们很容易看到,对象就是一个指向类对象生成的实例对象的指针,类对象的元数据存储的都是如何创建一个实例的相关信息,那么类对象和类方法应该从哪里创建呢?

  • isa源码分析
    isa_t是共用体,简化结构如下:
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

#if SUPPORT_PACKED_ISA
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1; // 0 代表普通指针 1:表示优化过的,可以存储更多的信息
        uintptr_t has_assoc         : 1; // 是否设置过关联对象
        uintptr_t has_cxx_dtor      : 1; // 是否有C++的析构函数
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6; // 用于在调试时分析对象是否未完成初始化
        uintptr_t weakly_referenced : 1; // 是否有被弱引用之香果
        uintptr_t deallocating      : 1; // 是否正在释放
        uintptr_t has_sidetable_rc  : 1; // 引用计数器是否过大无法存储在ISA中,如果为1,那么引用计数器会存储在一个叫做sideTable的结构体中
        uintptr_t extra_rc          : 19; // 里面存储的值是引用计数器减1
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };

# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
#       define RC_ONE   (1ULL<<56)
#       define RC_HALF  (1ULL<<7)
    };

就是从类对象的isa指针指向的结构体创建,上面我们提到过,类对象的isa指针指向元类(metaclass),元类中保存了创建类对象以及类方法所需的所有信息,下面可以看下isa的走位图:

isa走位流程.png

通过上面这个图,我们可以看出

  • 对象的isa指针指向所属类,类中存储着对象可以调用的方法,也就是实例方法

  • 类对象的isa指针指向所属的元类,元类中存储着类对象可以调用的方法,类方法,但是类方法是以实例方法的形式存储在元类中

  • 所有元类的isa指针指向根元类

  • 根元类的父类是NSObject

  • NSObject的isa指针指向根元类

  • 元类(meta class)
    在上面我们多次提到了元类这个概念,元类(meta class)是一个类对象的类。
    上面我们提到过,类也可以看成一个对象,就是类对象,我们给类对象发送消息(即调用类方法)。为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体,因此,就引出了meta class的概念,元类中保存了一个创建类对象以及类方法所需的所有信息。
    NSObject的元类就是根元类,也是其余类的元类的父类,根元类的isa指针还是指向自己的,根元类的父类是NSObject。

我们可以看下面获取类方法的源码:

Method class_getClassMethod(Class cls, SEL sel)
{
    if (!cls  ||  !sel) return nil;
    return class_getInstanceMethod(cls->getMeta(), sel);
}

实际上也是获取该类所属元类的实例方法。
或者直接打印使用class_getClassMethod和class_getInstanceMethod两种方法获取到的方法的method地址进行验证。

  • Method(objc_method)方法
    直接上定义:
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 方法名;
    char * _Nullable method_types                            方法类型;
    IMP _Nonnull method_imp                                    方法实现;
}  

这个结构体重,我们看到了SELIMP,说明SELIMP都是Method的一部分。

  • SEL(objc_selector)
    定义如下:
/// An opaque type that represents a method selector.代表一个方法的不透明类型
typedef struct objc_selector *SEL;

方法选择器,也是objc_msgSend函数的第二个参数,它是selecotrOC中的标识类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的ID,而这个ID的数据结构是SEL

@property SEL selector;

可以看到selector是SEL的一个实例。

其实selector就是个映射到方法的C字符串,可以使用OC编译器命令@selector()或者Runtimesel_registerName函数来获得一个SEL类型的方法选择器。

  • 同一个类,selector不能重复
  • 不同的类,selector可以重复

所以,我们不能跟C一样,使用函数重载,同一个类中不能拥有两个方法名相同的方法。

  • IMP
/// A pointer to the function of a method implementation.  指向一个方法实现的指针
typedef id (*IMP)(id, SEL, ...); 
#endif

就是指向最终实现程序的内存地址的指针

在iOS的Runtime中,Method通过selecotrIMP,实现了快速查询方法及实现

  • 类缓存(objc_cache)
    Objective-C运行时,通过跟踪它的isa指针检查对象时,它可以找到包含所有方法的方法列表,然而,可能只用到其中的一部分,每次都要查所有的,很费时间。所以类实现一个缓存,这个缓存中存储着之前访问过的方法,因为你可能以后再次调用该消息,提高方法查找性能。

  • Category(objc_category)
    Category表示一个指向分类的结构体的指针:

struct category_t { 
    const char *name; 
    classref_t cls; 
    struct method_list_t *instanceMethods; 
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
};

分类以及其相关的探究在另一篇文章中有介绍。

Runtime消息传递 & 消息查找

比如我们给一个Person类的.h文件增加了一个方法:

- (void)play;

我们创建一个Person类的实例,去调用play方法:

[person play];

编译一下,完全没问题,运行的时候,会发现,崩溃了,在调用play方法,到产生崩溃之间,系统是做了什么,又是如何处理的呢?

首先,我们或多或少都知道,对象的方法调用,编译器都会转成消息发送objc_msgSend(obj, foo),Runtime时执行的流程大概是这样的:

  • 通过person的isa指针,找到person所属的类,也就是Person
  • 在Person这个类的 method list(方法列表)中找 play方法
  • 如果Person类的方法列表没有找到foo,继续递归往它的superclass中找,一直找到NSObject的方法列表
  • 如果这个过程中能找到那就直接去执行方法的实现IMP
  • 如果这个过程(消息查找)没有找到,那么就会进入消息转发流程(下面会介绍)
  • 如果消息转发没有处理,那么就会报错:没有找到这个方法的实现

我们这里先分析消息查找的流程,是如何通过sel查找到对应的imp的?
我们肯定是要从objc_msgSend这个方法的实现入手,这个方法是由汇编和C共同完成的,这里我们主要看C的实现的部分,也能体现出方法的查找流程。

objc_msgSend 会有两种方式查找:

  • 快速 汇编 在缓存中找 通过SEL找imp(哈希表),找不到就下面那个过程
  • 慢速 C C++,找到了会存入缓存,没找到,另外一个复杂的过程:消息转发

objc_msgSend 是汇编写的,为什么用汇编写的?

  • C不可能写一个函数,保留一个未知的参数(比如一个实例对象,运行时才知道它是什么类型的),跳转到任意的指针
  • 汇编可以啊,有寄存器

首先,汇编语言,缓存中找那个imp,如果没有找到会调用 MethodTableLoopUp方法,方法列表中查找。

MethodTableLoopUp 中调用 _class_lookupMethodAndLoadCache3(核心方法)
去掉一个
来搜索,查找这个_class_lookupMethodAndLoadCache3方法
这个方法我们在下面还会用到,因为它也包含了消息转发后的逻辑处理:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

next>

// cls 是一个类对象
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 再从缓存中查找
    if (cache) {
        // 这次也是汇编查找
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    runtimeLock.lock();
    checkIsKnownClass(cls);

// 判断实现
    if (!cls->isRealized()) {
// 实现类 DATA里面一系列 赋值
        realizeClass(cls);
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
        // If sel == initialize, _class_initialize will send +initialize and 
        // then the messenger will send +initialize again after this 
        // procedure finishes. Of course, if this is not being called 
        // from the messenger then it won't happen. 2778172
    }

    
 retry:    
    runtimeLock.assertLocked();

    // Try this class's cache.
// 为什么再缓存获取一次?(remap(cls) 重映射)
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 查找当前类的方法列表
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
           // 如果找到了,就存如缓存,然后返回
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 递归查找父类的缓存 和 父类的方法列表
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                // 缓存赋值
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.
// 没有找到imp,会进行动态解析,注意triedResolver,初始为NO,解析一次设置为YES了,所以只会动态解析一次
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlock();
         // 这一步,如果处理之后,也就是进行了消息动态方法解析之后
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.lock();
        // Don't cache the result; we don't hold the lock so it may have 
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
// 执行完消息动态方法解析之后,会再retry,再次进入上面递归查找方法,此时如果动态方法解析中处理了,那么这一次就能被找到,然后执行了
        goto retry;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.
    // 如果没有找到这个方法,动态解析也没有解决,就进入消息转发

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);
 done:
    runtimeLock.unlock();
    return imp;
}

通过上面我们可以得出方法查找的流程是:

  • 先从缓存中找
  • 缓存中没有,从该类的方法列表中找
  • 该类的方法列表中没有,递归从父类的缓存和父类的方法列表中找
  • 如果在第2、3步找到,就会加入到方法缓存中,然后返回

Runtime消息转发

上面👆介绍了发送消息之后,方法查找的流程,如果上面那些操作都没有找到对应的方法实现,那么就会进入到消息转发的流程:

  • 动态方法解析
  • 备用接受者
  • 完整消息转发

下面,我们一个一个进行介绍:

  • 动态方法解析
    首先,OC运行时会调用+resolveInstanceMethod:或者+resolveClassMethod:,让你有机会提供一个函数实现,如果你添加了函数并返回YES,那运行时系统就会重新启动一次消息发送的过程。
    例子如下:
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    //执行foo函数
    [self performSelector:@selector(test:)];
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(test:)) {//如果是执行foo函数,就动态解析,指定新的IMP
        class_addMethod([self class], sel, (IMP) testMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void testMethod(id obj, SEL _cmd) {
    NSLog(@"Doing test");//新的test函数
}

从上面可以看到,虽然没有实现test:这个函数,但是我们通过class_addMethod动态添加testMethod函数,并执行testMethod这个函数的IMP,也就不会报错了。

我们从Runtime源码入手,看一下这一块的源码,在上面的_class_lookupMethodAndLoadCache3方法实现中,我们发现消息查找无果后,会进入消息动态解析的流程,调用_class_resolveMethod这个方法:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
// 这个里面去查找cls的元类是否有 resolveInstanceMethod,又是递归查找,但是这一次不会再有消息动态解析,因为会产生死循环,也是因为NSObject已经实现了resolveInstanceMethod,同时也有外部传入参数resolver的控制
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        //(类方法) - 元类(实例) - 根元类(实例) - NSObject (实例方法)
        // 这里要体会一下根元类的父类是NSObject NSObject的类方法以实例方法的形式存储在根元类中
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

之后,调用动态方法解析的方法

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    // 判断cls->isa 元类 及其父类 是否实现了 SEL_resolveInstanceMethod,如果自己没有实现,会查找到NSObject
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    ****这里是调用方法 SEL_resolveInstanceMethod*****
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
// 调用动态方法解析完成之后,如果用户已经处理了,给sel添加了对应的imp,此时会再次进入消息递归查找,此时就能找到了,然后可以执行了,很完美
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}

看上面的代码,最终会调用SEL_resolveInstanceMethod,如果自己实现了这个方法,我们可以在这里面去动态添加那个方法了。
添加了那个方法之后,会再次递归查找本类 父类的方法列表,此时就能找到然后执行了。

  • 消息转发类方法处理
    我们再次需要明白:
    对象方法的存储 - 类
    类方法的存储 - 元类,相当于对象方法
    所以类方法的查找就需要找该类所属的元类的方法列表是否有同名的对象方法。

比如Person 继承NSObject,Person 有 +walk 类方法声明,但是没有实现,NSObject+Test.m 分类里面有 -walk实例方法的实现,这样在执行[Person walk]的时候,也不会进入消息动态解析。
因为Person的+walk类方法查找是,先查找Person所属的元类的实例方法,如果没有,查其父类,一直到根元类,也没有,再查找根元类的父类:NSObject,因为我们通过NSObject+Test.m这个分类中,添加了-walk的方法实现,所以就找到了walk的方法实现,可以执行。

类方法查找的时候,也会执行下面这个方法:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)

此时 cls 是元类,inst 是一个类对象,在查找一个类的类方法的时候,它会查找类所属的元类,及其元类的父类,一直到根元类,最后到根元类的父类NSObject。

比如上面那个例子,我们想要找Person的+walk这个类方法,因为类方法存储在元类里面,我们会找Person的元类,递归找到Person元类的父类,一直到根元类,都没有,此时根元类的父类是NSObject,发现有-walk 这个实例方法(我们知道类方法,在元类中以实例方法进行存储),然后就调用-walk方法了。

NSObject的isa指向根元类,也就是NSObject的元类是根元类,所以NSObject的类方法,会存到根元类里面,以实例方法的形式。

所以类方法可以在NSObject中,以类方法或者实例方法实现。
再比如上面那个例子,如果我们在NSObject分类中 实现 +walk类方法,不去用实例方法了,此时+walk这个方法,会以实例方法的姿态存储到NSObject的元类(根元类)中,当Person 调用 +walk 方法查找的时候,会查找到根元类中,在根元类中找到 - walk方法,去调用,不会再去查找NSObject是否有这个方法。
所以以上面的例子举例,NSObject分类中,实现 -walk 或者 +walk 都能调用,但是 用 +walk类方法的实现,比 -walk实例方法的实现,要少查找一步,可以再次结合下面这张图去仔细体会:

4989398-31fa4e9295e53805.png

根元类的父类是NSObject NSObject的元类是根元类,这一点挺重要。

我们继续看下类方法的动态方法解析,有什么不同:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // 此时会进入到这里 cls 也就是元类
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
// 会走到这一步,这一步会调用NSObject的+resolveInstanceMethod:
// 它会给 cls 发送 SEL_resolveInstanceMethod 这个方法,又是方法查找
// 一直到根元类的父类,NSObject  发送 SEL_resolveInstanceMethod 消息
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

_class_resolveClassMethod实现:

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
    assert(cls->isMetaClass());

    if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    // 给 _class_getNonMetaClass(cls, inst) 这个对象 发送了消息 ,其实内部也是一个类对象 不是 元类
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveClassMethod adds to self->ISA() a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
  • 动态方法解析,防崩溃处理
    可以给NSObject添加分类,重写
    + resolveInstanceMethod:方法,因为不管是任何类的实例方法或者类方法,没有实现的时候,都可以走到NSObject分类中+resolveInstanceMethod: 这个方法中。

  • 备用接受者
    如果动态解析没处理,进入消息转发,这个只有汇编调用,没有源码实现
    如果上面那个没有新增方法,那就问问是否有别人会处理这个方法?
    调用的是:

+ (id)forwardingTargetForSelector:(SEL)aSelector; // 类
- (id)forwardingTargetForSelector:(SEL)aSelector; // 实例

比如一个DDPerson类,其中有-run方法声明,但是没有实现,我们可以指定一个别的接收者:

- (id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"instance - forwardingTargetForSelector");
    DDStudent *student = [DDStudent new];
    if ([student respondsToSelector:aSelector]) {
        return student;
    }
    return self;
}

这样就会调用到DDStudent中的run方法。student能够处理这条消息,所以这条消息被student成功处理,消息转发流程提前结束。

  • 最后的消息转发
    但是如果forwardingTargetForSelector方法中返回的是nil或者self呢?说明没有别的处理者,调用
    - (void)forwardInvocation:(NSInvocation *)anInvocation,
    在调用forwardInvocation:之前会调用
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法
    来获取这个选择子的方法签名,然后在
    -(void)forwardInvocation:(NSInvocation *)anInvocation方法
    中你就可以通过anInvocation拿到相应信息做处理,实例代码如下:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"forwardInvocation");
    DDStudent *student = [DDStudent new];
    anInvocation.target = student;
    anInvocation.selector = @selector(run);
    [anInvocation invoke];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"methodSignatureForSelector");
    if (aSelector == @selector(run)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

这个消息就会被Student处理掉了。

完整的转发流程如下:


image.png

那么最后消息未能处理的时候,还会调用到- (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,如果这个方法也没有实现,那么就会抛出没有找到该方法实现的错误了。所以我们也可以在这个方法中做些文章,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来。

Runtime的使用场景就不多介绍了,因为用的太多了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 一、Runtime简介 Runtime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消...
    林安530阅读 1,059评论 0 2
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,170评论 0 7
  • 本文详细整理了 Cocoa 的 Runtime 系统的知识,它使得 Objective-C 如虎添翼,具备了灵活的...
    lylaut阅读 792评论 0 4
  • 文中的实验代码我放在了这个项目中。 以下内容是我通过整理[这篇博客] (http://yulingtianxia....
    茗涙阅读 909评论 0 6
  • 概述 为项目编写文档,网上比较多的推荐是使用 ReadTheDocs ,以及配合 sphinx 来使用,然后经过一...
    speculatecat阅读 2,907评论 0 2