Runtime — 消息发送

2020-05-15  本文已影响0人  Dezi

一、Runtime

1. Runtime介绍

Objective-C 是一门动态语言,而承载整个 OC 动态特性的就是 Runtime

Runtime:是一套由C、C++、汇编混合编写的为OC提供运行时功能的 api。
关于 Runtime 更多内容可进入官网文档查看。

2. 我们与 Runtime 打交道有三种方式:

二、方法的底层源码探索

1. 准备OC代码

@interface DZPerson : NSObject
- (void)sayHello;
@end

@implementation DZPerson
- (void)sayHello {
    NSLog(@"%s",__func__);
}
@end


#import <Foundation/Foundation.h>
#import "DZPerson.h"
#import <objc/runtime.h>

void run(){
    NSLog(@"%s",__func__);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZPerson *person = [DZPerson alloc];
        [person sayHello]; // OC方法
        run(); // C函数方法
    }
    return 0;
}

2. 使用clang命令查看编译期底层源码

clang -rewrite-objc main.m -o main.cpp

main.cpp 文件最下方,我们会看到代码被编译如下:

void run(){
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kx_p_4mn02529l53n53r5mggdl00000gn_T_main_63361f_mi_0,__func__);
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        DZPerson *person = ((DZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DZPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));

        run();
    }
    return 0;
}

三、OC方法的本质

1. OC方法的本质

OC方法的本质:其实就是使用 objc_msgSend 来发送消息。

参数
id: 消息接收者
SEL: 方法编号

2. 方法对照解释

DZPerson *person = [DZPerson alloc];
[person fly];

DZPerson *person = ((DZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DZPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("fly"));
  1. ((DZPerson *(*)(id, SEL))(void *)是类型强转
  2. (id)objc_getClass("DZPerson")获取FXPerson类对象
  3. sel_registerName("alloc") 等同于 @selector()
  • 可以通俗理解为 ((类型强转)objc_msgSend)(对象, 方法调用)

3. 方法发送的几种情况

DZStudent *s = [DZStudent new];
[s sayCode];
// 消息 : 消息接受者 消息编号
objc_msgSend(s, sel_registerName("sayCode"));
objc_msgSend(objc_getClass("DZStudent"), sel_registerName("sayNB"));
struct objc_super dzSuper;
dzSuper.receiver = s;
dzSuper.super_class = [DZPerson class];
objc_msgSendSuper(&dzSuper, @selector(sayHello));
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));// 元类
objc_msgSendSuper(&myClassSuper, sel_registerName("sayNB"));

四、探索消息查找机制 - 快速流程 objc_msgSend

提示:使用 objc_msgSend 函数要把校验关闭,否则编译就报错了

1. 打开Xcode汇编分析

Debug -> Debug Workflow -> Always Show Disassembly

我们会看到断点停留在 17 行,下一步就是我们为之关心的 objc_msgSend

我们可以看到,objc_msgSend是在 libobjc.A.dylib 源码中,下来我们进入libobjc.A.dylib 源码继续探索。

2. objc_msgSend 汇编源码分析

objc_msgSend 函数是通过 汇编 来实现的,并且不同架构下对应不同的版本,这里我们关注 arm64版本 的实现就可以了。

用汇编的原因:
1. C语言中不可能通过写一个函数来保留未知的参数并跳转到一个任意的函数指针,汇编可以
2. objc_msgSend用汇编可以足够快

汇编补充
如何找到汇编方法的起止:
ENTRY _objc_msgSend:ENTRY+函数名是汇编方法开始入口
END_ENTRY _objc_msgSend:END_ENTRY+函数名是汇编方法的结束

    ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame
    // 判断0号寄存器是否为空,也就是第一个参数(id:接收者) 是否为空
    cmp p0, #0          // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
    b.le    LNilOrTagged        //  (MSB tagged pointer looks negative)
#else
    b.eq    LReturnZero
#endif
    // 读取 x0 赋值到 p13,因为第一个参数是isa所以x0其实就是 isa
    // isa 是在类或者元类中查找方法缓存或方法列表所必需的
    ldr p13, [x0]       // p13 = isa
    // 通过isa 获取class 存入p16
    GetClassFromIsa_p16 p13     // p16 = class
// 查找isa结束
LGetIsaDone:
    // CacheLookup 先查找类中方法缓存
    CacheLookup NORMAL      // calls imp or objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
    b.eq    LReturnZero     // nil check

    // tagged
    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]
    adrp    x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
    add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
    cmp x10, x16
    b.ne    LGetIsaDone

    // 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
// SUPPORT_TAGGED_POINTERS
#endif

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

    END_ENTRY _objc_msgSend

其实 objc_msgSend 主要就是通过 isa 获取 class 存入 p16,然后调用 CacheLookup 方法,查找方法缓存。

3. CacheLookup

CacheLookup NORMAL|GETIMP|LOOKUP 
#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2
.macro CacheLookup
    // p1 = SEL, p16 = isa 
    // [x16, #CACHE]意思是平移16个字节得到cache结构体,cache里包含p10(buckets) 和 p11(occupied|mask)
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
    // 这段其实就是_cmd & mask操作拿到散列表的下标key,然后获取buckets
#if !__LP64__
    and w11, w11, 0xffff    // p11 = mask
#endif
    and w12, w1, w11        // x12 = _cmd & mask
    add p12, p10, p12, LSL #(1+PTRSHIFT)
                     // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
   
 /* 这里获取到buckets里的 imp 和 sel,
然后进行判断bucket->sel是否等于传入的SEL _cmd,
b.ne是b.noEqual,如果不等于,2f 跳转 "2:" 流程
如果等于,即在缓存里边找到了,直接返回imp
*/
    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

3:  // wrap: p12 = first bucket, w11 = mask
    add p12, p12, w11, UXTW #(1+PTRSHIFT)
                                // p12 = buckets + (mask << 1+PTRSHIFT)

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

    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

3:  // double wrap
    JumpMiss $0
    
.endmacro

4. CheckMiss

然后判断 bucket->sel 是否等于
因为在流程 "1:" 中,如果传入的 SEL _cmdcache 缓存中的 bucket->sel 没有匹配到,也就是没匹配到方法,那么就跳转 "2:",然后我们就看到了 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
.else
.abort oops
.endif
.endmacro

我们应该记得,进入 CacheLookup 的参数是 NORMAL,那么很明显,我们进入 CheckMiss 后的下一步是 __objc_msgSend_uncached

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

.macro MethodTableLookup
    
    // push frame
    SignLR
    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
    AuthenticateLR

.endmacro

五、探索消息查找机制 - 慢速流程 方法查找

在缓存中通过 objc_msgSend 进行快速查找,如果没有找到就会走 __class_lookupMethodAndLoadCache3 函数,也就是慢速查找,具体源码如下,我们来进行具体分析。

1. _class_lookupMethodAndLoadCache3

/***********************************************************************
* _class_lookupMethodAndLoadCache.
* Method lookup for dispatchers ONLY. OTHER CODE SHOULD USE lookUpImp().
* This lookup avoids optimistic cache scan because the dispatcher 
* already tried that.
**********************************************************************/
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
//  initialize = YES , cache = NO , resolver = YES
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // 缓存查找,cache为NO直接跳过
    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.

    // lock是为了防止多线程操作; 类是否被编译
    runtimeLock.lock();
    checkIsKnownClass(cls);

    /* 为查找方法做准备条件,如果类没有初始化时,初始化类和父类、元类等,
    保证当对象方法或者类方法没有找到,能够在父类和元类中继续递归查找 
    */
    if (!cls->isRealized()) {
        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.
    // 从缓存里面查找一遍,若有直接goto done
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // Try this class's method lists.
    // 当前类的方法列表
    // 形成局部作用域,避免局部变量命名重复
    {
        // 在类的方法列表中查找方法,若有直接cache_fill
        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.
            // 在父类缓存中查找,若有直接cache_fill
            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.
            // getMethodNoSuper_nolock在父类的方法列表中查找方法,若有直接log_and_fill_cache (内部是cache_fill)
            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.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;
        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;
}

慢速流程:

  1. 首先会初始化类和父类、元类,保证当对象方法或者类方法没有找到,能够在父类和元类中继续递归查找
  2. 在缓存中查找方法
  3. 在类的方法列表中查找方法,若有,log_and_fill_cache -> cache_fill -> cache_fill_nolock
  4. 在父类的方法列表中查找方法,若有,log_and_fill_cache -> cache_fill -> cache_fill_nolock
  5. 都没有找到,则进行动态方法解析 _class_resolveMethod,系统会提供一次容错的机会可以针对 sel 来操作一下
  6. 如果没做任何处理,就会进入消息的转发 _objc_msgForward_impcache,此时又进入到了汇编中 __objc_msgForward

下面针对关键部分函数进行详细解析:

2. getMethodNoSuper_nolock

methods列表 进行遍历,找到当前 sel 对应的 method,找不到返回 nil。
search_method_list 内部是一个二分查找。

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

    assert(cls->isRealized());
    // fixme nil cls? 
    // fixme nil sel?

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

3. log_and_fill_cache

如果找到方法,会进入 log_and_fill_cache,调用 cache_fill 进入缓存流程,具体参考 方法缓存cache_t

static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
    if (objcMsgLogEnabled) {
        bool cacheIt = logMessageSend(implementer->isMetaClass(), 
                                      cls->nameForLogging(),
                                      implementer->nameForLogging(), 
                                      sel);
        if (!cacheIt) return;
    }
#endif
    cache_fill (cls, sel, imp, receiver);
}

4. _class_resolveMethod

动态方法解析 _class_resolveMethod,找不到 impRuntime 会给一次容错机会:cls 是元类的话调用类方法的_class_resolveInstanceMethod,实例方法调用_class_resolveInstanceMethod
两个方法逻辑相似,主要是 objc_msgSend 函数发送 SEL_resolveInstanceMethod 消息,再查找一下 sel 方法

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {
        // try [cls resolveInstanceMethod:sel]

        _class_resolveInstanceMethod(cls, sel, inst);
    } 
    else {
        // try [nonMetaClass resolveClassMethod:sel]
        // and [cls resolveInstanceMethod:sel]
        _class_resolveClassMethod(cls, sel, inst); // 已经处理
        if (!lookUpImpOrNil(cls, sel, inst, 
                            NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
        {
            // 对象方法 决议
            _class_resolveInstanceMethod(cls, sel, inst);
        }
    }
}

5. _objc_msgForward_impcache

如果没有任何处理,我们就会进入 消息的转发,其内部调用了 __objc_msgForward 方法,这里我们看到 __objc_msgForward 又调用了 __objc_forward_handler 方法。

STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b   __objc_msgForward
END_ENTRY __objc_msgForward_impcache

ENTRY __objc_msgForward
adrp    x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward

6. __objc_forward_handler

通过对 __objc_forward_handler 的探索,我们看到如果没有找到方法,这里有一段经典的报错信息:unrecognized selector sent to instance xxx

// Default forward handler halts the process.
__attribute__((noreturn)) 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);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

六、总结

  1. 方法的本质是消息发送,先是通过 objc_msgSend 在缓存中进行 快速查找,如果未找到缓存方法则进行慢速查找,也就是通过对类及父类的方法列表进行二分法常规查找。
  2. 如果类、父类、元类都没有实现,则进入动态方法决议,之后进入消息的转发,如果消息的转发也处理失败就会crash,报出经典错误信息 unrecognized selector sent to instance xxx
  3. 下一篇我们进行消息转发流程的详细探索,此处仅探索到消息发送。
上一篇 下一篇

猜你喜欢

热点阅读