objc_msgSend做了什么?

objc_msgSend 是 OC 编译成 C 语言过程时调用的,也是调用最频繁的一个语句。这里我们分析下,苹果是怎么调用 objc_msgSend 以及调用之后的操作是怎样的。

这里 我们可以下载到苹果公开的部分源码。

1.objc_msgSend 的调用

objc_msgSend是用汇编语言实现的,所以针对不同的硬件环境采用了不同的文件,这里我们只分析arm环境下的源码。

/********************************************************************
 *
 * id objc_msgSend(id self, SEL _cmd, ...);
 * IMP objc_msgLookup(id self, SEL _cmd, ...);
 * 
 * objc_msgLookup ABI:
 * IMP returned in r12
 * Forwarding returned in Z flag
 * r9 reserved for our use but not used
 *
 ********************************************************************/
    ENTRY _objc_msgSend
    
    cbz r0, LNilReceiver_f      //判断Receiver是否存在

    ldr r9, [r0]        // r9 = self->isa
    GetClassFromIsa         // r9 = class  通过isa指针回去类对象
    CacheLookup NORMAL    //cache内查找IMP是否存在
    // cache hit, IMP in r12, eq already set for nonstret forwarding
    bx  r12         // call imp

    CacheLookup2 NORMAL
    // cache miss
    ldr r9, [r0]        // r9 = self->isa
    GetClassFromIsa         // r9 = class
    b   __objc_msgSend_uncached   //调用_objc_msgSend_uncached
LNilReceiver:
    // r0 is already zero
    mov r1, #0
    mov r2, #0
    mov r3, #0
    FP_RETURN_ZERO
    bx  lr  

    END_ENTRY _objc_msgSend


/////////////////////////////////////////////////////////////////////
//
// CacheLookup  NORMAL|STRET
// CacheLookup2 NORMAL|STRET
//
// Locate the implementation for a selector in a class's method cache.
//
// Takes: 
//    $0 = NORMAL, STRET
//    r0 or r1 (STRET) = receiver
//    r1 or r2 (STRET) = selector
//    r9 = class to search in
//
// On exit: r9 clobbered
//      (found) continues after CacheLookup, IMP in r12, eq set
//      (not found) continues after CacheLookup2
//
/////////////////////////////////////////////////////////////////////
    
.macro CacheLookup
    
    ldrh    r12, [r9, #CACHE_MASK]  // r12 = mask
    ldr r9, [r9, #CACHE]    // r9 = buckets
.if $0 == STRET
    and r12, r12, r2        // r12 = index = SEL & mask
.else
    and r12, r12, r1        // r12 = index = SEL & mask
.endif
    add r9, r9, r12, LSL #3 // r9 = bucket = buckets+index*8
    ldr r12, [r9, #CACHED_SEL]  // r12 = bucket->sel
6:
.if $0 == STRET
    teq r12, r2
.else
    teq r12, r1
.endif
    bne 8f
    ldr r12, [r9, #CACHED_IMP]  // r12 = bucket->imp

.if $0 == STRET
    tst r12, r12        // set ne for stret forwarding
.else
    // eq already set for nonstret forwarding by `teq` above
.endif

.endmacro

.macro CacheLookup2
#if CACHED_SEL != 0
#   error this code requires that SEL be at offset 0
#endif
8:  
    cmp r12, #1
    blo 8f          // if (bucket->sel == 0) cache miss
    it  eq          // if (bucket->sel == 1) cache wrap
    ldreq   r9, [r9, #CACHED_IMP]   // bucket->imp is before first bucket
    ldr r12, [r9, #8]!      // r12 = (++bucket)->sel
    b   6b
8:

.endmacro
/////////////////////////////////////////////////////////////////////
//
// GetClassFromIsa  return-type
//
// Given an Isa, return the class for the Isa.
//
// Takes:
//    r9 = class
//
// On exit: r12 clobbered
//          r9 contains the class for this Isa.
//
/////////////////////////////////////////////////////////////////////
.macro GetClassFromIsa

#if SUPPORT_INDEXED_ISA
    // Note: We are doing a little wasted work here to load values we might not
    // need.  Branching turns out to be even worse when performance was measured.
    MI_GET_ADDRESS(r12, _objc_indexed_classes)
    tst.w   r9, #ISA_INDEX_IS_NPI_MASK
    itt ne
    ubfxne  r9, r9, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS
    ldrne.w r9, [r12, r9, lsl #2]
#endif

.endmacro


/********************************************************************
 * IMP cache_getImp(Class cls, SEL sel)
 *
 * On entry:    r0 = class whose cache is to be searched
 *              r1 = selector to search for
 *
 * If found, returns method implementation.
 * If not found, returns NULL.
 ********************************************************************/

    STATIC_ENTRY _cache_getImp

    mov r9, r0
    CacheLookup NORMAL
    // cache hit, IMP in r12
    mov r0, r12
    bx  lr          // return imp
    
    CacheLookup2 GETIMP
    // cache miss, return nil
    mov r0, #0
    bx  lr

    END_ENTRY _cache_getImp

    STATIC_ENTRY __objc_msgSend_uncached

    // THIS IS NOT A CALLABLE C FUNCTION
    // Out-of-band r9 is the class to search
    
    MethodTableLookup NORMAL    // returns IMP in r12 调用MethodTableLookup
    bx  r12

    END_ENTRY __objc_msgSend_uncached

/////////////////////////////////////////////////////////////////////
//
// MethodTableLookup    NORMAL|STRET
//
// Locate the implementation for a selector in a class's method lists.
//
// Takes: 
//    $0 = NORMAL, STRET
//    r0 or r1 (STRET) = receiver
//    r1 or r2 (STRET) = selector
//    r9 = class to search in
//
// On exit: IMP in r12, eq/ne set for forwarding
//
/////////////////////////////////////////////////////////////////////
    
.macro MethodTableLookup
    
    stmfd   sp!, {r0-r3,r7,lr}
    add r7, sp, #16
    sub sp, #8          // align stack
    FP_SAVE

.if $0 == NORMAL
    // receiver already in r0
    // selector already in r1
.else
    mov     r0, r1          // receiver
    mov     r1, r2          // selector
.endif
    mov r2, r9          // class to search

    blx __class_lookupMethodAndLoadCache3
    mov r12, r0         // r12 = IMP
    
.if $0 == NORMAL
    cmp r12, r12        // set eq for nonstret forwarding
.else
    tst r12, r12        // set ne for stret forwarding
.endif

    FP_RESTORE
    add sp, #8          // align stack
    ldmfd   sp!, {r0-r3,r7,lr}

.endmacro

以上汇编大家看起来费劲。这里转成 C 语言的伪代码:


/*
objc_msgSend的C语言版本伪代码实现.
receiver: 是调用方法的对象
op: 是要调用的方法名称字符串
*/
id  objc_msgSend(id receiver, SEL op, ...)
{

    //1............................ 对象空值判断。
    //如果传入的对象是nil则直接返回nil
    if (receiver == nil)
        return nil;
    
   //2............................ 获取或者构造对象的isa数据。
    void *isa = NULL;
    //如果对象的地址最高位为0则表明是普通的OC对象,否则就是Tagged Pointer类型的对象
    if ((receiver & 0x8000000000000000) == 0) {
        struct objc_object  *ocobj = (struct objc_object*) receiver;
        isa = ocobj->isa;
    }
    else { //Tagged Pointer类型的对象中没有直接保存isa数据,所以需要特殊处理来查找对应的isa数据。
        
        //如果对象地址的最高4位为0xF, 那么表示是一个用户自定义扩展的Tagged Pointer类型对象
        if (((NSUInteger) receiver) >= 0xf000000000000000) {
            
            //自定义扩展的Tagged Pointer类型对象中的52-59位保存的是一个全局扩展Tagged Pointer类数组的索引值。
            int  classidx = (receiver & 0xFF0000000000000) >> 52
            isa =  objc_debug_taggedpointer_ext_classes[classidx];
        }
        else {
            
            //系统自带的Tagged Pointer类型对象中的60-63位保存的是一个全局Tagged Pointer类数组的索引值。
            int classidx = ((NSUInteger) receiver) >> 60;
            isa  =  objc_debug_taggedpointer_classes[classidx];
        }
    }
    
   //因为内存地址对齐的原因和虚拟内存空间的约束原因,
   //以及isa定义的原因需要将isa与上0xffffffff8才能得到对象所属的Class对象。
    struct objc_class  *cls = (struct objc_class *)(isa & 0xffffffff8);
    
   //3............................ 遍历缓存哈希桶并查找缓存中的方法实现。
    IMP  imp = NULL;
    //cmd与cache中的mask进行与计算得到哈希桶中的索引,来查找方法是否已经放入缓存cache哈希桶中。
    int index =  cls->cache.mask & op;
    while (true) {
        
        //如果缓存哈希桶中命中了对应的方法实现,则保存到imp中并退出循环。
        if (cls->cache.buckets[index].key == op) {
              imp = cls->cache.buckets[index].imp;
              break;
        }
        
        //方法实现并没有被缓存,并且对应的桶的数据是空的就退出循环
        if (cls->cache.buckets[index].key == NULL) {
             break;
        }
        
        //如果哈希桶中对应的项已经被占用但是又不是要执行的方法,则通过开地址法来继续寻找缓存该方法的桶。
        if (index == 0) {
            index = cls->cache.mask;  //从尾部寻找
        }
        else {
            index--;   //索引减1继续寻找。
        }
    } /*end while*/

   //4............................ 执行方法实现或方法未命中缓存处理函数
    if (imp != NULL)
         return imp(receiver, op,  ...); //这里的... 是指传递给objc_msgSend的OC方法中的参数。
    else
         return objc_msgSend_uncached(receiver, op, cls, ...);
}

/*
  方法未命中缓存处理函数:objc_msgSend_uncached的C语言版本伪代码实现,这个函数也是用汇编语言编写。
*/
id objc_msgSend_uncached(id receiver, SEL op, struct objc_class *cls)
{
   //这个函数很简单就是直接调用了_class_lookupMethodAndLoadCache3 来查找方法并缓存到struct objc_class中的cache中,最后再返回IMP类型。
  IMP  imp =   _class_lookupMethodAndLoadCache3(receiver, op, cls);
  return imp(receiver, op, ....);
}

通过以上分析。我们发现还要调用 __class_lookupMethodAndLoadCache3 方法。
继续分析__class_lookupMethodAndLoadCache3内做了些什么操作。
最总我们发现这个方法是在 objc-runtime.mm 内实现的。
实现代码如下:

/***********************************************************************
* _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*/);
}

/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup. 
* initialize==NO tries to avoid +initialize (but sometimes fails)
* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use initialize==YES and cache==YES.
* inst is an instance of cls or a subclass thereof, or nil if none is known. 
*   If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use 
*   must be converted to _objc_msgForward or _objc_msgForward_stret.
*   If you don't want forwarding at all, use lookUpImpOrNil() instead.
**********************************************************************/
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); //1........ cache查找 汇编实现
        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()) { //2........类对象是否初始化
        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.

    imp = cache_getImp(cls, sel); //3.1........cache内是否查到?查到返回IMP
    if (imp) goto done;

    // Try this class's method lists.     3.2........... 类对象内查找
    {
        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. 3.3 ............父类对象cache和method list内查找
    {
        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.
// 4........动态加载  做标记 只加载一次
    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;
}


通过以上分析 发现一个问题,这里只有 resolveMethod 没有消息转发呀!莫非我哪里漏掉了什么?再想想看

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

推荐阅读更多精彩内容