通过Runtime源码,分析OC消息发送及处理

[图片上传失败...(image-f0ae06-1556960021893)]

前言

日常开发中我们得知,当我们通过对象调用一个方法时,本质是通过objc_msgSend给对象发送消息。这点我们可以通过clang编译后的代码得知。

MyPerson *p = [MyPerson new];

通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件编译得:

MyPerson *p = ((MyPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyPerson"), sel_registerName("new"));

可知接收消息的对象是:(id)objc_getClass("MyPerson")
接收的消息编号:sel_registerName("new") == @selector(new)
通过分析objc4-750源码,以objc_msgSend为入口,接下来我们开始分析整个消息发送及处理流程。

整个流程分为快速和慢速两种方式。
快速:通过汇编,在缓存(cache)的imp哈希表中寻找。这样的好处是C、C++等语言不能通过写一个函数,来直接保留未知的参数,跳转到任意的指针。而汇编通过调用寄存器,可很好的实现这一点。
慢速: 通过C、C++在方法列表中寻找。找到了会往chche中存。以上方法找不到,就会通过特殊的动态处理。

0x01 汇编缓存查找

objc4-750源码中搜索_objc_msgSend,点击查看在arm64架构中的ENTRY _objc_msgSend。代码和注释如下:

ENTRY _objc_msgSend
    UNWIND _objc_msgSend, NoFrame

//tagged pointer:特殊的数据类型,更为轻量。
    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
    ldr p13, [x0]       // p13 = isa
    GetClassFromIsa_p16 p13     // p16 = class
    
LGetIsaDone://isa处理完毕。//这里可以作为后面传参的一个参考。


//!!主要函数!!
//在缓存列表中找imp
//这里CacheLookup有三种方式:NORMAL|GETIMP|LOOKUP
//1、成功:call imp 
//2、失败:objc_msgSend_uncached
    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

通过查看CacheLookup的宏定义代码,得知缓存中寻找的三种形式:
CacheHit | CheckMiss | add
//1:找到直接返回
//2:找不到的话直接checkmiss
//3:在其它地方找到的话通过汇编直接add进缓存中。

.macro CacheLookup
    // p1 = SEL, p16 = isa
    ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#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))

    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)

查看CacheHit的定义文件即可得知找到imp后可直接返回.

.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

查看CheckMiss的定义文件即可得知找不到imp,便调用__objc_msgSend_uncached

.macro CheckMiss
    // miss if bucket->sel == 0
.if $0 == GETIMP
    cbz p9, LGetImpMiss
//因为前面声明了CacheLookup NORMAL ,所以会走下面这个判断。
.elseif $0 == NORMAL
    cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
    cbz p9, __objc_msgLookup_uncached

查看__objc_msgSend_uncached的代码中发现MethodTableLookup的调用,继续跟进,便发现了__class_lookupMethodAndLoadCache3的调用。

.macro MethodTableLookup
    
// receiver and selector already in x0 and x1
    mov x2, x16
    bl  __class_lookupMethodAndLoadCache3//这里跳转到C++中。

0x02 分析C++代码

继上:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
//第一个YES,接上文,已经完成了isa的初始化,所以为YES.
//第一个NO,接上文,通过汇编没有在cache中完成查找,所以为NO。
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward:是寻找imp的关键函数。runtime中涉及imp的获取底层都会走这个方法。比如class_getMethodImplementationclass_getInstanceMethodclass_getInstanceMethod也是通过lookUpImpOrNil,最后底层走这个方法的。

在下面的方法中大致操作为:
1、首先检测缓存,如果cache有的话直接就在缓存中查找返回imp.
2、如果类没被创建,便进行实例化操作。
3、第一次调用类的时候,执行初始化。
4、为了防止并发,再次从缓存中查找。
5、遍历当前类的父类,在父类中缓存的imp中查找
6、在父类的方法列表中,获取method_t对象。如果找到则缓存查找到的IMP
7、如果都没有找到,就尝试动态方法解析和消息转发。

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

    runtimeLock.assertUnlocked();

    // 如果cache是YES,则从缓存中查找IMP。
    if (cache) {
        // 通过cache_getImp函数查找IMP,查找到则返回IMP并结束调用
        // cache_getImp:还是通过汇编来寻找的。
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

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

    // 第一次调用当前类的话,执行initialize的代码
    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();

//再次从缓存中获取的原因:
//并发-remap(cls)
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    {
        // 如果没有从cache中查找到,则从方法列表中获取Method
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            // 如果获取到对应的Method,则加入缓存并从Method获取IMP
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    //在父类中找。
    // Try superclass caches and method lists.
    {
        unsigned attempts = unreasonableClassCount();
        // 循环遍历父类。获取这个类的缓存IMP 或 方法列表的IMP
        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
            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_t对象。如果找到则缓存查找到的IMP
            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 = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

0x03 动态方法解析

 // 如果没有找到,则尝试动态方法解析
    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;
    }

_class_resolveMethod:

void _class_resolveMethod(Class cls, SEL sel, id inst)
{
    if (! cls->isMetaClass()) {//解析实例方法。
        // try [cls resolveInstanceMethod:sel]
       // _class_resolveInstanceMethod:接收消息的是类对象。
        _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);
        }
    }
}

_class_resolveClassMethod的实现中有如下代码,表示了消息的发送。可知消息的接受者_class_getNonMetaClass(cls, inst)

BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(_class_getNonMetaClass(cls, inst), 
                        SEL_resolveClassMethod, sel);

进入class_getNonMetaClass的实现中,得知返回的依旧是类对象,这样是方便能够在同一个类中处理,方便管理,而避免了去虚拟的元类中进行改动。

static Class getNonMetaClass(Class metacls, id inst)
{
    static int total, named, secondary, sharedcache;
    runtimeLock.assertLocked();

    realizeClass(metacls);

    total++;
    
    // metacls 元类
    // metacls 类对象
    
    //判断是否是NSObject
    if (!metacls->isMetaClass()) return metacls;

    // metacls really is a metaclass

    // special case for root metaclass
    // where inst == inst->ISA() == metacls is possible
    
    // 判断是否是根元类。
    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) {
            cls = cls->superclass;
            realizeClass(cls);
        }
        // 最终返回的还是类对象
        if (cls) {
            assert(!cls->isMetaClass());
            assert(cls->ISA() == metacls);
            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
    }

在动态方法解析的过程中,都会调用lookUpImpOrNil来递归查找动态解析方法的imp,而不会发生死递归的原因是在NSObject中实现了动态方法解析,所以最终会找到它。
同时我们通过重写NSObject中的+ (BOOL)resolveInstanceMethod:(SEL)sel,在这个方法中通过给没有实现的sel添加imp方法避免崩溃,同时也可以将crash传给后台做崩溃统计等工作。

0x04 消息转发

以下的_objc_msgForward_impcache因为苹果闭源是无法看到实现的,我们可以通过定义一个instrumentObjcMessageSends,或者通过反编译函数实现的可执行文件来查看其流程。这里简单介绍一下第二种。

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

通过实现动态方法解析,未实现转发而崩溃的堆栈信息可以看出_objc_msgForward_impcache具体是在CoreFoundation.framework中实现。如图:


CoreFoundation.framework的本地地址:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation

通过hopper或者ida打开,搜索_CFInitialize,再依次进入_forwarding_prep_0___forwarding__。通过查看伪代码,会有以下发现:


 var_50 = rbx;
    if (class_respondsToSelector(object_getClass(r12), @selector(_forwardStackInvocation:)) != 0x0) {
            if (*____forwarding___.onceToken != 0xffffffffffffffff) {
                    dispatch_once(____forwarding___.onceToken, ^ { /* block implemented at ______forwarding____block_invoke */ });
            }
            r13 = [NSInvocation requiredStackSizeForSignature:r14];
            rdx = *____forwarding___.invClassSize;
            r12 = rsp - (rdx + 0xf & 0xfffffffffffffff0);
            memset(r12, 0x0, rdx);
            objc_constructInstance(*____forwarding___.invClass, r12);
            var_40 = r13;
            [r12 _initWithMethodSignature:r14 frame:var_48 buffer:r12 - (r13 + 0xf & 0xfffffffffffffff0) size:r13];
            [var_38 _forwardStackInvocation:r12];
            r15 = 0x1;
    }
    else {
            rbx = @selector(forwardInvocation:);
            if (class_respondsToSelector(object_getClass(r12), rbx) != 0x0) {
                    rdi = r12;
                    r12 = [NSInvocation _invocationWithMethodSignature:r14 frame:var_48];
                    _objc_msgSend(rdi, rbx);
            }
            else {
                    r12 = 0x0;
                    _CFLog(0x4, @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message", 0x0, object_getClassName(0x0), r8, r9, stack[2037]);
            }
            var_40 = 0x0;
            r15 = 0x0;
    }
经典走位图

如下,代码实现消息转发。

// 只有汇编调用  没有源码实现
+ (id)forwardingTargetForSelector:(SEL)aSelector{
    return [super forwardingTargetForSelector:aSelector];
}

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

+ (void)forwardInvocation:(NSInvocation *)anInvocation{
    //在此切面编程
    NSString *sto = @"这是参数";
    anInvocation.target = [LGStudent class];
    [anInvocation setArgument:&sto atIndex:2];
    NSLog(@"%@",anInvocation.methodSignature);
    anInvocation.selector = @selector(run:);
    [anInvocation invoke];
}

如果没有实现消息转发,我们再根据源码追踪一下走位。
进入消息转发的汇编部分。如下:

    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
    
    
    ENTRY _objc_msgSend_noarg
    b   _objc_msgSend
    END_ENTRY _objc_msgSend_noarg

    ENTRY _objc_msgSend_debug
    b   _objc_msgSend
    END_ENTRY _objc_msgSend_debug

    ENTRY _objc_msgSendSuper2_debug
    b   _objc_msgSendSuper2
    END_ENTRY _objc_msgSendSuper2_debug

    
    ENTRY _method_invoke
    // x1 is method triplet instead of SEL
    add p16, p1, #METHOD_IMP
    ldr p17, [x16]
    ldr p1, [x1, #METHOD_NAME]
    TailCallMethodListImp x17, x16
    END_ENTRY _method_invoke

查看__objc_forward_handler回调

void *_objc_forward_handler = (void*)objc_defaultForwardHandler;

如下可以见到我们常见的崩溃信息打印的源头了。


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

推荐阅读更多精彩内容