objc库源码分析(3)-方法调用-消息发送

objc_msgSend()

前面说过了,我们在写代码时候的会调用objc_msgSend系列的方法,然后再调用lookUpImpOrForward()方法,其实这只是一个笼统的说法。
当我们调用方法时候,编译起会根据实际情况调用以下方法中的一种

  • objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
  • objc_msgSend_stret(id _Nullable self, SEL _Nonnull op, ...)
  • objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
  • objc_msgSendSuper_stret(id _Nullable self, SEL _Nonnull op, ...)

这些方法的定义在message.h文件中可以找到。这些方法的功能其实都是去寻找对象的方法,并去执行相应的方法。

其中objc_msgSend()和objc_msgSendSuper()的接收者不一样。objc_msgSend()第一个参数是一个id类型的,而objc_msgSendSuper()第一个参数是objc_super结构体。定义如下

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

可以看到objc_super结构体中包含了两个对象,一个是id类型的方法接受者receiver,一个是receiver的父类。

当我们调用对象的方法的时候,如[objectA test]的时候,会调用会objc_msgSend()方法,把 objectA 这个对象传给objc_msgSend()中的第一个参数self。

当我们调用父类的方法的时候,如[super test]的时候(关于super的定义,在另外一篇文章中有提及到),会调用会objc_msgSendSuper()方法,把super对象传给objc_msgSendSuper()方法。

objc_msgSend()和objc_msgSendSuper()区别在于,objc_msgSend()是从子类开始查找方法,而objc_msgSendSuper()是从父类开始查找方法。

另外,在有如下定义

#if !OBJC_OLD_DISPATCH_PROTOTYPES
objc_msgSend(void /* id self, SEL op, ... */ )
   OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#else
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
   OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif

其中OBJC_OLD_DISPATCH_PROTOTYPES的意思是在调用objc_msgSend()时,是否需要转换成合适的方法指针。如OBJC_OLD_DISPATCH_PROTOTYPES为YES,我们在调用objc_msgSend()就需要声明方法的返回值类型和参数类型,如

((int (*)(id, SEL, NSString *, BOOL)) objc_msgSend)((id)obj,@selector(viewWillAppear:),YES);

如果OBJC_OLD_DISPATCH_PROTOTYPES为NO,在调用时,我们可以用如下的方式去调用

objc_msgSend((id)obj,@selector(viewWillAppear:),YES);

OBJC_OLD_DISPATCH_PROTOTYPES的值可以通过Build Setting中去设置在 Enable Strict Checking of objc_msgSend Calls 设置。


在objc库中,objc_msgSend()的实现是不开源的,但是可以通过反编译去找到伪代码的实现。在网上找了一份(来源https://www.jianshu.com/p/df6629ec9a25

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, ....);
}

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

可以看到,objc_msgSend()方法的执行流程是

1 判断对象是否为空
2 处理tagget pointer,找到相应的类,
3 判断类的缓存列表里面是否找到方法的实现,如果找到,则执行
4 如果缓存列表里面没找到方法实现,则调用lookUpImpOrForward查找方法


方法缓存

待补充


方法查找lookUpImpOrForward()

lookUpImpOrForward方法的实现如下

IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
    const IMP forward_imp = (IMP)_objc_msgForward_impcache;
    IMP imp = nil;
    Class curClass;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup 去方法的缓存列表找方法
    if (fastpath(behavior & LOOKUP_CACHE)) {
        imp = cache_getImp(cls, sel);
        if (imp) goto done_nolock;//如果找到方法,则跳转到最下方的done_nolock
    }

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

    // We don't want people to be able to craft a binary blob that looks like
    // a class but really isn't one and do a CFI attack.
    //  CFI attack 破坏机器码执行来控制程序行为的攻击
    // To make these harder we want to make sure this is a class that was
    // either built into the binary or legitimately registered through
    // objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
    //
    // TODO: this check is quite costly during process startup.
    checkIsKnownClass(cls);//检查类的来源

    if (slowpath(!cls->isRealized())) {//如果类没有标志为已实现,则需要判断锁是否释放掉了
        
        cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);//这个主要是判断是否解锁,如果锁释放掉,则重新枷锁
        // runtimeLock may have been dropped but is now locked again
    }

    //如果类没有初始化,则初始化类
    if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
        //#define fastpath(x) (__builtin_expect(bool(x), 1))
        //#define slowpath(x) (__builtin_expect(bool(x), 0))
        //__builtin_expect作用是"允许程序员将最有可能执行的分支告诉编译器"。这个指令的写法为:__builtin_expect(EXP, N)。意思是:EXP==N的概率很大。

        cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
        // runtimeLock may have been dropped but is now locked again

        // 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
        //如果当前调用的方法是initialize方法,而且是由messenger(一般是我们自己调用)调用,会调用两次,但是第一次调用initialize就会把类标记为initialized
    }

    runtimeLock.assertLocked();
    curClass = cls;

    // The code used to lookpu the class's cache again right after
    // we take the lock but for the vast majority of the cases
    // evidence shows this is a miss most of the time, hence a time loss.
    //
    // The only codepath calling into this without having performed some
    // kind of cache lookup is class_getInstanceMethod().

    //重新查找方法缓存
    for (unsigned attempts = unreasonableClassCount();;) {
        // curClass method list.
        //查找当前类的方法列表
        Method meth = getMethodNoSuper_nolock(curClass, sel);
        if (meth) {
            imp = meth->imp;
            goto done;
        }

        if (slowpath((curClass = curClass->superclass) == nil)) {
            // No implementation found, and method resolver didn't help.
            // Use forwarding.
            //如果找不到在类中,找不到方法。而且该类的父类为空,则进入消息转发流程
            imp = forward_imp;
            break;
        }

        // Halt if there is a cycle in the superclass chain.
        if (slowpath(--attempts == 0)) {
            _objc_fatal("Memory corruption in class list.");
        }

        // Superclass cache.查找父类的缓存,如果还是没有找到方法,则返回forward_imp的实现
        imp = cache_getImp(curClass, sel);
        if (slowpath(imp == forward_imp)) {
            // Found a forward:: entry in a superclass.
            // Stop searching, but don't cache yet; call method
            // resolver for this class first.
            break;
        }
        if (fastpath(imp)) {
            // Found the method in a superclass. Cache it in this class.
            goto done;
        }
    }

    // No implementation found. Try method resolver once.
    //如果没有找到方法实现,则尝试动态方法解析
    if (slowpath(behavior & LOOKUP_RESOLVER)) {
        behavior ^= LOOKUP_RESOLVER;
        return resolveMethod_locked(inst, sel, cls, behavior);
    }

 done:
    log_and_fill_cache(cls, imp, sel, inst, curClass);//缓存方法调用
    runtimeLock.unlock();
 done_nolock:
    if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
        return nil;
    }
    return imp;
}

这段代码的执行流程如图

lookUpImpOrForward执行流程.jpg

lookUpImpOrForward()执行完以后,把imp返回给objc_msgSend_uncached()方法。objc_msgSend_uncached()方法会去直接执行imp。

由于lookUpImpOrForward()中有可能返回的是方法的具体实现,有可能返回的是消息转发forward_imp。如果返回的是forward_imp,那么将进入消息转发流程。
消息转发流程发在下一篇文章讲述。

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

推荐阅读更多精彩内容