iOS Runtime底层之消息传递

Runtime是C,C++汇编一起写成的API,有两个版本Modern和Legacy,OC2.0之后用的是Modern Version版本,可以运行在iOS2.0和macOS 10.5之后的系统中,本文解析的是objc4-723版本的源码。

objc4-723 可调式源码

都说Objective-C是一门动态运行时语言,那什么是运行时?什么是编译时?
运行时:代码跑起来之后会装载到内存中,提供运行时功能
编译时:正在编译的时间,就是把源代码(高级语言)翻译成机器能识别的语言->机器语言->二进制

Runtime可以做的事情有很多,如

  • 黑魔法:动态交换方法、KVO实现等
  • 关联对象:给分类添加属性等
  • 消息转发:项目中一些防崩溃处理等

它可以做的事情有很多,在这里就探索Runtime的底层是如何实现的,也便深入理解Objective-C这门语言。

Runtime的消息传递过程

在OC调用方法时,我们clang一下,看看对应的C++实现是什么

clang -rewrite-objc main.m -o main.cpp
Person *p = [Person new];
[p sayHello];

//C++实现
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));

可以看出,在调用方法时,编译器将它转成了objc_msgSend消息发送了,在Runtime的执行过程如下

  • 1、Runtime先通过对象p找到isa指针,判断isa指针是否为nil,为nil直接return。
  • 2、若不为空则通过isa指针找到当前实例的类对象,在类对象下查找缓存是否有sayHello方法。
  • 3、若找到sayHello方法,则直接调用IMP方法。
  • 4、若没找到,则查找当前类对象的方法列表methodlist,若找到方法则将其添加到缓存中
  • 5、若没找到,则继续到当前类的父类中以相同的方式查找,直到追溯到最上层NSObject
  • 6、若还是没有找到,则启用动态方法解析、备用接收者、消息转发三部曲,给程序最后一个机会
  • 7、若还是没找到,则Runtime会抛出异常doesNotRecognizeSelector

以上就是Runtime的消息传递过程,我们从上往下慢慢分析。

isa指针

首先是isa指针,什么是isa指针?
打开Runtime源码,全局搜索isa_t,锁定objc-private.h类,里面有这么一段代码,isa指针是isa_t的实例。

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      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;                                       \
      uintptr_t extra_rc          : 19
    };
#endif
};

OC对象的本质,每个OC对象都含有一个isa指针,__arm64__之前,isa仅仅是一个指针,保存着对象或类对象内存地址,在__arm64__架构之后,apple对isa进行了优化,变成了一个联合体union结构,同时使用位域来存储更多的信息。
它是通过isabits进行位运算,取出响应位置的值,runtime中的isa是被联合体位域优化过的,它不单单是指向类对象了,而是把64位中的每一位都运用了起来,其中的shiftcls为33位,代表了类对象的地址,其他的位都有各自的用处。

  • nonpointer:表示是否对isa指针开启指针优化
    0:不开启,表示纯isa指针。 1开启,不单单是类对象的地址,isa中包含了类信息和对象的引用计数等。
  • has_assoc:关联对象标识位,0没有,1有,没有关联对象会释放的更快
  • has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更快的释放对象
  • shiftcls:存储类指针class的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤用来存储类指针。
  • magic:固定值为0xd2,用于在调试时分辨对象是否完成初始化
  • weakly_referenced:表示对象是否被指向或者曾经指向一个 ARC 的弱引用变量,
    没有弱引⽤用的对象可以更更快释放。 deallocating:标志对象是否正在释放内存
  • has_sidetable_rc:当对象的引用计数大于10,以至于无法存储在isa指针中时,用散列表去计数
  • extra_rc:表示该对象的引用计数,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数⼤于 10, 则需要使⽤到has_sidetable_rc

isa 指针走位图


isa走位流程.png

objc_msgSend的方法调用本质

好了现在清楚了isa指针是怎么一回事儿了,接下来探索方法调用的本质是什么。
打开Runtime源码,全局搜索objc_msgSend(,找到objc-msg-arm64.s这个类

objc_msgSend的方法调用分为两部分,汇编和C语言。

1、汇编部分

1、ENTRY _objc_msgSend
2、进入LNilOrTagged,当数据类型是NSTaggedPointer或消息名为空时,直接END_ENTRY _objc_msgSend,不为空则继续执行下一步,NSTaggedPointer类型的对象采用和isa一样的联合体位域的方式,可直接从地址中读取出想要的值,一般当数据类型的"value"足够小时,系统会自动转换为NSTaggedPointer类型,比如NSString转换为NSTaggedPointerString

#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif

3、LGetIsaDone 处理完isa,这一步就是isa指针已经处理完,要么从缓存中找到IMP,要么执行objc_msgSend_uncached从方法列表中获取IMP

LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

4、CacheLookup NORMAL 去缓存中找IMP,会有以下2种情况
1)CacheHit在缓存中找到了则直接calls imp
2)CheckMiss没找到则执行objc_msgSend_uncached
CacheLookup执行如下,主要看关键字就行。

//代码剔除了混淆理解的部分
.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

    ldp p17, p9, [x12]      // {imp, sel} = *bucket
1:  cmp p9, p1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: p12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp p12, p10        // wrap if bucket == buckets
    b.eq    3f
    ldp p17, p9, [x12, #-BUCKET_SIZE]!  // {imp, sel} = *--bucket
    b   1b          // loop
.endmacro

CacheHit执行如下

.macro CacheHit
.if $0 == NORMAL
    TailCallCachedImp x17, x12  // authenticate and call imp
.elseif $0 == GETIMP
    mov p0, p17
    AuthAndResignAsIMP x0, x12  // authenticate imp and re-sign as IMP
    ret             // return IMP
.elseif $0 == LOOKUP
    AuthAndResignAsIMP x17, x12 // authenticate imp and re-sign as IMP
    ret             // return imp via x17
.endmacro

CheckMiss执行如下

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached
.endmacro

5、objc_msgSend_uncached,缓存中没找到则去MethodTableLookup到方法列表中寻找

STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band p16 is the class to search
    
    MethodTableLookup
    TailCallFunctionPointer x17

    END_ENTRY __objc_msgSend_uncached

6、MethodTableLookup中会到objc-runtime-new.mm中调用_class_lookupMethodAndLoadCache3方法,从这里开始便从汇编到C语言执行了

.macro MethodTableLookup
    bl  __class_lookupMethodAndLoadCache3
.endmacro

2、C语言部分

1、执行_class_lookupMethodAndLoadCache3方法,该方法调用了lookUpImpOrForward函数

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

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    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.read();

    if (!cls->isRealized()) {
        // Drop the read-lock and acquire the write-lock.
        // realizeClass() checks isRealized() again to prevent
        // a race while the lock is down.
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }

    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
        // 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.assertReading();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists.
    {
        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.

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 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;
        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.unlockRead();

    return imp;
}  

2、lookUpImpOrForward 查找imp,主要分为以下几步

  • 1)getMethodNoSuper_nolock(cls, sel);method_list中找,如果找到了method,则把它存入缓存;如果没找到,则向上遍历父类,步骤也是执行1)、2)步,直到查到根部(root)NSObject。
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) {
    //循环遍历method列表,查询方法
    for (auto mlists = cls->data()->methods.beginLists(), 
              end = cls->data()->methods.endLists(); 
         mlists != end;
         ++mlists)
    {
        method_t *m = search_method_list(*mlists, sel);
        if (m) return m;
    }
    return nil;
}
  • 2)log_and_fill_cache存入缓存
static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) {
    cache_fill (cls, sel, imp, receiver);
}

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    //获取类的缓存
    cache_t *cache = getCache(cls);
    //这里可以知道cache是以方法名作为key
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    //在当前缓存大小大于缓存吃容量的3/4时,扩大缓存,在低于3/4时可以正常存储
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {//满了就扩大缓存容量
        cache->expand();
    }

    把方法的IMP存储到cache中
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

如果上面的流程走完没有找到IMP,那么还有接下来三种补救方法,动态方法解析、备用接收者、消息转发

3、动态方法解析、备用对象、消息转发

Part1、动态方法解析

当前 2 步汇编里的 CacheHit 和 C 语言里的MethodTableLookUp 都没找到 imp 时,会执行_class_resolveMethod方法,进行第一次补救,意在检查是否动态添加了方法

void _class_resolveMethod(Class cls, SEL sel, id inst)
{ 
    if (! cls->isMetaClass()) { //如果cls不是一个元类,说明调用的是一个实例方法
        //此时 cls表示的是类,而inst代表的是类的实例
        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        //此时 cls表示的是元类,而inst代表的是类
        _class_resolveClassMethod(cls, sel, inst);
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}
  • 如果 cls 不是元类,说明此次是类对象调用实例方法,此时 cls 表示的是类对象,inst 表示类对象的实例,sel 表示类的实例方法,调用_class_resolveInstanceMethod,如果本例中的 Person 类实现了resolveInstanceMethod,会再次调用 objc_msgSend
  • 如果是元类,结合 isa 指针走位图,可知 cls 是元类对象,sel 就是元类的实例方法即类方法,inst 就是类对象,如图所示
    image.png
实例方法

当类的实例对象调用实例方法时,从上图 isa 走位图可知,cls->ISA()指向的 Person 类,并不是元类,因此调用的是_class_resolveInstanceMethod方法,方法解析如下

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
     //cls->ISA() 此时指向的是元类,判断元类是否实现了resolveInstanceMethod方法
    //(相当于判断该类是否有实现 resolveInstanceMethod 的类方法)
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }
 
    //如果有实现+ (BOOL)resolveInstanceMethod:(SEL)sel,就重启消息发送流程
    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    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
    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));
        }
    }
}
 
IMP lookUpImpOrNil(Class cls, SEL sel, id inst, 
                   bool initialize, bool cache, bool resolver)
{
    IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
    if (imp == _objc_msgForward_impcache) return nil;
    else return imp;
}

if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/*initialize*/, YES/*cache*/, NO/*resolver*/))这段代码结合lookUpImpOrNil函数的几个参数可以解析如下

  • 第 1 个参数:cls->ISA(),根据上面的 isa 走位图可知,这里指向了 Person 的元类
  • 第 2 个参数:SEL_resolveInstanceMethod,表示resolveInstanceMethod方法
  • 第 3 个参数:cls,表示实例是 Person 类对象

这段代码表示 Person 的元类中是否有resolveInstanceMethod实例方法,因为类方法在元类中是以实例的形式存在的,所以这段代码可以理解为判断 Person 类是否有类方法resolveInstanceMethod
这里也可以看出resolveInstanceMethod形成了递归,流程可以结合下图理解

image.png

即循环判断了 Person 类及其父类是否实现了resolveInstanceMethod类方法,最终到 NSObject 结束了递归,NSObject 实现了resolveInstanceMethod这个方法,只是返回了NO。系统就是通过这样的方式来避免产生死递归。 看源码可以看到,如果找到了resolveInstanceMethod方法,系统就会重新执行一遍objc_msgSend的流程,去回调这个方法。

这里有个面试题
如果调用一个 LGPerson 的 run 实例方法,但 LGPerson 类只是声明,却没有实现 run 方法,而实现了resolveInstanceMethod方法,执行结果为输出了 2 次“来了 老弟”,让后崩溃,为什么?

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [[LGPerson alloc] run];
    }
    return 0;
}

@implementation LGPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"来了 老弟");
   return [super resolveInstanceMethod:sel];
}
@end

为什么会执行 2 次,我们在这里断点发现第一次调用堆栈如图所示

image.png

objc_msgSend_uncached之后执行了 1 次resolveInstanceMethod,这个堆栈可以解释为,objc_msgSend_uncached缓存查找没有命中后,执行了MethodTableLookUp,如下面汇编代码所示

    STATIC_ENTRY __objc_msgSend_uncached
    UNWIND __objc_msgSend_uncached, FrameWithNoSaves

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band x16 is the class to search
    
    MethodTableLookup
    br  x17

    END_ENTRY __objc_msgSend_uncached

MethodLookUp之后没有找到,才执行的resolveInstanceMethod方法,再看第 2 次堆栈如下

image.png

在消息转发流程中,当forwardingTargetForSelector返回nil时就会调methodSignatureForSelector方法,而他的内部触发了class_getInstanceMethod方法,如下图所示
image.png

可以发现这里重新执行了方法查找的过程,导致内部执行了第二次resolveInstanceMethod方法。

如果想动态添加方法,可以在这一步通过实现resolveInstanceMethodresolveClassMethod方法来实现,代码如下

//调用对象的实例方法时,先执行resolveInstanceMethod方法
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
        if (sel == @selector(run)) {
            SEL readBookSel = @selector(readBook);
            Method readM = class_getInstanceMethod(self, readBookSel);
            IMP readImp = class_getMethodImplementation(self, readBookSel);
            const char *type = method_getTypeEncoding(readM);
            return class_addMethod(self, sel, readImp, type);
        }
    }
    return [super resolveInstanceMethod:sel];
}

//调用类方法时,先执行resolveClassMethod方法
+ (BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(walk)) {
        SEL readBookSel = @selector(readBook);
        Method readM = class_getInstanceMethod(self, readBookSel);
        IMP readImp = class_getMethodImplementation(self, readBookSel);
        const char *type = method_getTypeEncoding(readM);
        return class_addMethod(object_getClass(self), sel, readImp, type);
    }
    
    return [super resolveClassMethod:sel];
}
class_resolveClassMethod 类方法解析

当执行的是类方法调用时([Person walk]这种方式),开始从 Person 类对象的元类里查找resolveClassMethod方法,一直递归去查找,这里如果找到了,依然会重启消息发送方法(NSObject 同样实现了resolveClassMethod这个方法,返回了NO)

static void _class_resolveClassMethod(Class cls, SEL sel, id inst)
{
//此时的cls是元类,inst是类
    assert(cls->isMetaClass());
   //开始遍历元类是否有resolveClassMethod方法
    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;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

跟实例方法不同的是:类方法重启objc_msgSend时传入的参数是_class_getNonMetaClass(cls, inst),而实例方法重启时传的参数是cls(类对象),下面我们来分析一下_class_getNonMetaClass的实现

Class _class_getNonMetaClass(Class cls, id obj)
{
    mutex_locker_t lock(runtimeLock);
    cls = getNonMetaClass(cls, obj);
    assert(cls->isRealized());
    return cls;
}
static Class getNonMetaClass(Class metacls, id inst)
{
    static int total, named, secondary, sharedcache;
    runtimeLock.assertLocked();
 
    realizeClass(metacls);
 
    total++;
 
    //metacls是元类  inst是类
    // 如果metacls不是元类了,说明此时传进来的inst类是NSObject,就直接返回元类
    if (!metacls->isMetaClass()) return metacls;
 
    // metacls really is a metaclass
 
    // special case for root metaclass
    // where inst == inst->ISA() == metacls is possible
    //如果元类的isa指向了自己,说明此时元类是根元类  ,返回它的父类 即NSObject
    if (metacls->ISA() == metacls) {
        Class cls = metacls->superclass;
        assert(cls->isRealized());
        assert(!cls->isMetaClass());
        assert(cls->ISA() == metacls);
        if (cls->ISA() == metacls) return cls;
    }
 
    //一般情况
    if (inst) {
        Class cls = (Class)inst;
        realizeClass(cls);
        // cls may be a subclass - find the real class for metacls
        while (cls  &&  cls->ISA() != metacls) {
//这里主要判断isa是否被篡改 — 异常判断
            cls = cls->superclass;
            realizeClass(cls);
        }
        if (cls) {
            assert(!cls->isMetaClass());
            assert(cls->ISA() == metacls);
//直接返回cls 类对象
            return cls;
        }
#if DEBUG
        _objc_fatal("cls is not an instance of metacls");
#else
        // release build: be forgiving and fall through to slow lookups
#endif
    }
    // 以下为 inst 为空等等异常情况,先不考虑,知道这里返回了类对象即可
    // try name lookup
    {
        Class cls = getClass(metacls->mangledName());
        if (cls->ISA() == metacls) {
            named++;
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: %d/%d (%g%%) "
                             "successful by-name metaclass lookups",
                             named, total, named*100.0/total);
            }
 
            realizeClass(cls);
            return cls;
        }
    }
 
    // try secondary table
    {
        Class cls = (Class)NXMapGet(nonMetaClasses(), metacls);
        if (cls) {
            secondary++;
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: %d/%d (%g%%) "
                             "successful secondary metaclass lookups",
                             secondary, total, secondary*100.0/total);
            }
 
            assert(cls->ISA() == metacls);            
            realizeClass(cls);
            return cls;
        }
    }
 
    // try any duplicates in the dyld shared cache
    {
        Class cls = nil;
 
        int count;
        Class *classes = copyPreoptimizedClasses(metacls->mangledName(),&count);
        if (classes) {
            for (int i = 0; i < count; i++) {
                if (classes[i]->ISA() == metacls) {
                    cls = classes[i];
                    break;
                }
            }
            free(classes);
        }
 
        if (cls) {
            sharedcache++;
            if (PrintInitializing) {
                _objc_inform("INITIALIZE: %d/%d (%g%%) "
                             "successful shared cache metaclass lookups",
                             sharedcache, total, sharedcache*100.0/total);
            }
 
            realizeClass(cls);
            return cls;
        }
    }
 
    _objc_fatal("no class for metaclass %p", (void*)metacls);
}

可以看到,_class_getNonMetaClass的正常流程还是返回了类对象,为什么要返回类对象呢,我们正常的思路是谁出问题,在谁那里修复(应该在元类里取修复),但是类方法是在元类中的,元类是一个虚拟的类,没办法进行书写,所以苹果就设计了这么一个方式,直接返回类,在类里面去实现resolveClassMethod

但是我们看类方法的_class_resolveMethod里发现,_class_resolveClassMethod执行完了如果没有找到的话,还有一步操作_class_resolveInstanceMethod(与上面实例方法步骤一致),该方法会继续从元类里寻找resolveInstanceMethod方法,直到 NSObject,这也表明了我们可以在 NSObject 扩展里实现resolveInstanceMethod方法,就可以拦截所有的未实现方法

Part2、备用接收者

如果动态方法解析依然没有结果,就会进行寻找备用接收者(id)forwardingTargetForSelector:(SEL)aSelector;
这个forwarding方法在runtime底层是通过汇编调用的,苹果并没有给出源码实现,但是我们可以通过打印 runtime 的方法调用信息(log),来查看 runtime 接下来调用了哪些方法,方式如下:

// 外部引用定义
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //使用区域,将想要查找的方法用instrumentObjcMessageSends圈起来
        //开始监听传递参数为YES,结束监听传递参数为NO
        instrumentObjcMessageSends(YES);
        [LGPerson  walk];
        instrumentObjcMessageSends(NO);
    }
    return 0;
}

这样在finder中找到/private/tmp/文件夹,如下图所示位置,找到最新的msgSends-xxxx文件,这个文件就是LGPerson调用walk方法的时候所执行的方法信息


image.png

调用信息如图所示


显而易见的,LGPerson在动态方法解析后,继续调用了NSObject的动态方法解析,在都没有imp的情况下调用了forwardingTargetForSeletor(备用接收者)方法,当备用接受者也没有实现时,会调用最后一步methodSignatureForSelector(消息转发)

举例如下:
//寻找备用接收者,让别的类来提供同名的方法

+ (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(walk)) {
        return [LGStudent new];
    }
    return [super forwardingTargetForSelector:aSelector];
}

我们可以利用forwardingTargetForSelector方法完成哪些动作?
1.找备用接收者处理当前类没有实现的方法(注意:备用接收者的方法只能是实例方法)
2.crash收集
3.防止崩溃

Part3、消息转发

如果备用接收者没有找到IMP方法,则执行消息转发,主要涉及以下两个方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

在动态方法解析和备用接收者都无法找到IMP时,系统会调用methodSignatureForSelector方法,如果方法返回nil,则系统直接调用doesNotRecognizeSelector方法使程序崩溃并打印崩溃信息;如果返回一个NSMethodSignature类型的函数签名,则系统会创建一个NSInvocation对象并调用forwardInvocation方法,我们可以在方法中指定别的方法或其他对象去执行这个的方法。
例子如下

//若返回函数签名,则继续调用forwardInvocation方法
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(run)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if (anInvocation.selector == @selector(run)) {
        //指定当前类的一个方法作为IMP
//        anInvocation.selector = @selector(readBook);
//        [anInvocation invoke];
        
        //指定其他类来执行这个IMP
        LGStudent *student = [LGStudent new];
        [anInvocation invokeWithTarget:student];
    }
}

以上就是Runtime消息传递的全部过程。

Runtime博大精深,未完待续~

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

推荐阅读更多精彩内容