Runtime消息机制

2019-06-02  本文已影响0人  分流替躺欧阳克

1. OC消息机制三个阶段

2. 源码跟读

我们OC的方法调用会转成runtime的API ,objc_msgSend("消息接收者","方法名即SEL");这个形式。

OC消息机制三个阶段

  1. 消息发送。
  2. 动态解析方法。
  3. 消息转发。

消息发送

objc_msgSend给消息接收者发送一条消息,接收者先从自己的cache_t里查找方法有没有缓存(这一阶段在源码里是汇编代码)。若是缓存列表里找不到方法,则去自己的methods方法列表里查找,若仍然没有找到。则通过superClass去找到他的父类的缓存列表里找,若是找了方法,则把方法缓存到自己的方法缓存列表。若是父类的缓存列表找不到方法,则去父类的方法列表里找,若是找到了,则缓存到自己的缓存列表(注意这是直接缓存到自己的缓存列表,没有缓存到父类的缓存列表)若是仍然没找到,再继续找父类的父类缓存列表,这样递归下去,直到找到,缓存到自己的方法列表。若找完基类仍是没有找到方法,则进入第二阶段,动态解析方法。

动态解析方法

系统会回调我们在类里实现的+ (BOOL)resolveInstanceMethod:(SEL)sel方法。在这个方法里我们可以把别的方法的实现交给这个找不到的sel,添加到methods里面,runtime会重新去找一次方法列表methods。

- (void)other
{
    NSLog(@"%s",__func__);
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(test)) {
        Method  method = class_getInstanceMethod(self, @selector(other));
        class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
       return YES;
    }
   return [super resolveInstanceMethod:sel];
}

若是这个resolveInstanceMethod:方法没有实现,或者没有给方法列表添加方法,runtime第二次去方法列表里查找也没找到方法,就会进入第三阶段,消息转发阶段。

消息转发

在消息发送阶段没有找到方法,并且动态解析方法也没有找到方法,当这个类已经没能力处理这个方法时,runtime就会进入消息转发阶段,我们在自己的调用方法的类里实现以下方法

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        return [[Dog alloc]init];
        //objc_msgSend("Dog",aSelector)把消息发给d返回的类处理
    }
    return [super forwardingTargetForSelector:aSelector];
}

把我们找不到的方法转交给Dog类,就相当于objc_msgSend("Dog",aSelector)这么做。如果Dog类里实现了找不到的方法,那么Dog就会调用这个方法。但是如果在这个类里没有实现forwardingTargetForSelector:(SEL)aSelector方法,或者这个方法里没有return 任何东西,或者return了,但是Dog里并没有实现这个方法,就会进入以下阶段,需要我们实现以下两个方法:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) {
        //如果这里return nil 则不会调用forwardInvocation:
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}
如果上面的方法实现了,并返回了签名,则会runtime会调用下面的方法,方法执行至此,已经不会崩溃,可以在里面做任何事。
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    anInvocation.target = [[MJCat alloc]init];
    [anInvocation invoke];//把方法给MJCat执行 
}

系统回调这两个方法,需要我们传回方法签名,这个签名就是Method_t里的 type成员
格式:v代表返回值void, 16代表参数和返回值总共占16字节,@代表id类型 ,0代表id类型内存从0个字节开始,:代表SEL类型,8代表SEL类型参数内存从第八个字节开始。可以使用@Encoding()方法获取对应类型的符号。

注意点 消息转发阶段的类方法,输入是没有提示的,需要我们先打出成员方法,再把减号改成加号。

源码跟读

第一阶段 消息发送

objc_msgSend("recevier","SEL");源码实现是在objc-msg-arm64文件里,是使用汇编代码实现的,源码如下

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    MESSENGER_START

    cmp x0, #0          // nil check and tagged pointer check
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
    ldr x13, [x0]       // x13 = isa
    and x16, x13, #ISA_MASK // x16 = class  
LGetIsaDone:
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    mov x10, #0xf000000000000000
    cmp x0, x10
    b.hs    LExtTag
    adrp    x10, _objc_debug_taggedpointer_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
    ubfx    x11, x0, #60, #4
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone

LExtTag:
    // ext tagged
    adrp    x10, _objc_debug_taggedpointer_ext_classes@PAGE
    add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
    ubfx    x11, x0, #52, #8
    ldr x16, [x10, x11, LSL #3]
    b   LGetIsaDone

LReturnZero:
    // x0 is already zero
    mov x1, #0
    movi    d0, #0
    movi    d1, #0
    movi    d2, #0
    movi    d3, #0
    MESSENGER_END_NIL
    ret

    END_ENTRY _objc_msgSend
  1. cmp x0, #0 判断x0(消息接收者,即第一个参数)是否是0
  2. b .le LNilOrTagged 如果小于0则跳转到 LNilOrTagged LNilOrTagged里是 b.eq LReturnZero返回0退出函数
  3. CacheLookup NORMAL 查找方法缓存(CacheLookup是个宏,根据注释可以看出是去cache_t里查找缓存)
.macro CacheLookup
    // x1 = SEL, x16 = isa
    ldp x10, x11, [x16, #CACHE] // x10 = buckets, x11 = occupied|mask
    and w12, w1, w11        // x12 = _cmd & mask
    add x12, x10, x12, LSL #4   // x12 = buckets + ((_cmd & mask)<<4)

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          // loop

3:  // wrap: x12 = first bucket, w11 = mask
    add x12, x12, w11, UXTW #4  // x12 = buckets+(mask<<4)

    // Clone scanning loop to miss instead of hang when cache is corrupt.
    // The slow path may detect any corruption and halt later.

    ldp x9, x17, [x12]      // {x9, x17} = *bucket
1:  cmp x9, x1          // if (bucket->sel != _cmd)
    b.ne    2f          //     scan more
    CacheHit $0         // call or return imp
    
2:  // not hit: x12 = not-hit bucket
    CheckMiss $0            // miss if bucket->sel == 0
    cmp x12, x10        // wrap if bucket == buckets
    b.eq    3f
    ldp x9, x17, [x12, #-16]!   // {x9, x17} = *--bucket
    b   1b          // loop

3:  // double wrap
    JumpMiss $0
    
.endmacro
  1. 如果缓存里没有找到方法应该是执行CheckMiss $0这段代码
.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz x9, LGetImpMiss
.elseif $0 == NORMAL
    cbz x9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz x9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
  1. 因为我们在CacheLookup NORMAL步骤里传的是NORMAL所以执行的是 __objc_msgSend_uncached这句代码。
    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
  1. 这句代码执行的方法MethodTableLookup是从方法缓存列表去找
.macro MethodTableLookup
    
    // push frame
    stp fp, lr, [sp, #-16]!
    mov fp, sp

    // save parameter registers: x0..x8, q0..q7
    sub sp, sp, #(10*8 + 8*16)
    stp q0, q1, [sp, #(0*16)]
    stp q2, q3, [sp, #(2*16)]
    stp q4, q5, [sp, #(4*16)]
    stp q6, q7, [sp, #(6*16)]
    stp x0, x1, [sp, #(8*16+0*8)]
    stp x2, x3, [sp, #(8*16+2*8)]
    stp x4, x5, [sp, #(8*16+4*8)]
    stp x6, x7, [sp, #(8*16+6*8)]
    str x8,     [sp, #(8*16+8*8)]

    // receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3
    // imp in x0
    mov x17, x0
    
    // restore registers and return
    ldp q0, q1, [sp, #(0*16)]
    ldp q2, q3, [sp, #(2*16)]
    ldp q4, q5, [sp, #(4*16)]
    ldp q6, q7, [sp, #(6*16)]
    ldp x0, x1, [sp, #(8*16+0*8)]
    ldp x2, x3, [sp, #(8*16+2*8)]
    ldp x4, x5, [sp, #(8*16+4*8)]
    ldp x6, x7, [sp, #(8*16+6*8)]
    ldr x8,     [sp, #(8*16+8*8)]

    mov sp, fp
    ldp fp, lr, [sp], #16
.endmacro
  1. 我们根据注释先忽略那些对寄存器操作的代码,可以看到bl _class_lookupMethodAndLoadCache3这句代码,因为汇编的代码回对runtime 的api前面加 下划线,我们搜索_class_lookupMethodAndLoadCache3,去掉了第一个下划线,全工程搜索,可以在objc-runtime-new.mm文件看到查找方法的具体代码。
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();
//因为在汇编代码里找过一次缓存方法列表,这里过掉从缓存方法列表里找
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }
    runtimeLock.read();
    //判断有没有对类重新组织过的意思,与本篇逻辑无关
    if (!cls->isRealized()) {
        runtimeLock.unlockRead();
        runtimeLock.write();

        realizeClass(cls);

        runtimeLock.unlockWrite();
        runtimeLock.read();
    }
//类有没有接收到消息初始化过
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlockRead();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.read();
    }
 
 retry:    
    runtimeLock.assertReading();

    imp = cache_getImp(cls, sel);//又从方法缓存里找了一次,因为怕上面代码执行的时候,有调用过方法,把方法加到了缓存列表。
    if (imp) goto done;//如果找到方法,则结束返回IMP,返回到汇编代码里的
  bl  __class_lookupMethodAndLoadCache3 bl跳转的意思,即通过bl指令调用方法

    {
        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)
        {
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    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;
            }
        }
    }
    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        triedResolver = YES;
        goto retry;
    }
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

上面代码会先判断类有没有经过runtime重新规划合并类,初始化判断。
然后imp = cache_getImp(cls, sel);在从缓存里找一遍,因为上面重新规划合并类初始化的代码可能会把方法加到缓存列表。
if (imp) goto done;
如果找到方法则跳到done处执行代码runtimeLock.unlockRead();return imp;
结束返回IMP,返回到汇编代码里的
bl __class_lookupMethodAndLoadCache3
bl跳转的意思,即通过bl指令调用方法。

如果缓存里没有找到方法,则继续执行代码,去这个类里面的方法列表methods找方法,找到了则跳到done,返回imp到汇编代码执行。如果找到了就缓存到自己的缓存列表。

 {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

这段代码是具体查找方法,cls->data()->methods拿到类里的class_rw_t(包含了类的各种信息的结构体)

static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
    runtimeLock.assertLocked();

    assert(cls->isRealized());

    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;
}

如果自己的类方法列表里找不到,则去父类的缓存方法列表查找,父类缓存列表找不到,就去父类方法列表里查找,再去父类的父类找,一直递归到基类为止,这个过程中一旦找到方法就会把方法缓存到自己的缓存列表(不是任何一个父类)并且返回IMP到汇编通过bl执行方法。

{
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.");
            }
            
            //去父类缓存列表查找.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // 父类缓存列表里有方法,则缓存到自己的缓存列表
                    log_and_fill_cache(cls, imp, sel, inst, curClass);
                    goto done;
                }
                else {
                    break;
                }
            }
            
            // 如果父类缓存列表没有方法,则去父类的方法列表里去查找
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

以上为runtime机制第一阶段,消息发送阶段源码。

第二阶段 消息转发

    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;
    }

先判断有没有过动态解析的过程,由与第一次进方法,triedResolver == NO,所以会进这个判断, _class_resolveMethod(cls, sel, inst);这句话会调用我们在类里实现的resolveInstanceMethod:然后goto retry 重新查找,若是我们在resolveInstanceMethod:方法里实现了把方法加到方法列表,则会在类方法列表里找到方法,返回IMP到汇编通过bl 执行我们添加的方法。若是我们没有实现resolveInstanceMethod:或者没有往类里加方法,则还是会走到 if (resolver && !triedResolver)这一步判断,但是由于triedResolver在上次进来对其赋值YES,所以不会执行里面的代码。

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    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;
    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));
        }
    }
}

上面的代码
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
这两句相当与 objc_msgSend(cls, SEL_resolveInstanceMethod, sel)调用我们的类里的resolveInstanceMethod:方法。
至此,第二阶段消息动态解析阶段结束。

第三阶段 消息转发

 imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

这段找不到源码,但是有前辈整理了这里的原理。这里就是runtime调用我们类里实现的方法

上一篇 下一篇

猜你喜欢

热点阅读