objc源码之Method消息发送

Objc源码之对象创建alloc和init
Objc源码之initialize实现
Objc源码之Load方法实现
Objc源码之NSObject和isa
Objc源码之引用计数实现
objc源码之Method消息发送

前言

   在我们进行方法调用的时候,我们的对象是如何找到我们的方法呢?这个问题大家基本都知道是通过isa找到的,实例对象通过isa找到类对象,在类对象中查找方法,类对象通过isa指针找到元类,在元类对象中查找,那么在这个过程中究竟查找过程是怎么实现的,除了查找方法,还会进行哪些操作呢?这篇文章我们通过objc的源码来看下具体的查找过程。

一、方法调用过程

TestObject *obj = [TestObject new];
[obj test];

我们以实例对象的方法调用为例,来说明一下方法的调用过程:
1.首先[obj test]会转换成objc_msgSend(self,@ selector(test))函数调用。

2.obj通过isa指针找到类对象,实例对象的方法列表存在于类对象中。

3.类对象是一个objc_class结构体,objc_class结构中存在一个cache_t类型的cache,从cache里面的bucket_t中通过@ selector(test)为key来查找方法实现IMP。

4.如果objc_class的cache中没有查找到,就通过class_data_bits_t来获取class_rw_t来获取中的methods方法列表来查找test方法。

5.如果类对象中没有查找到对应的方法,就通过objc_class结构体中的superclass来找到对象的父类对象,然后重复3、4、5这个过程,如果还没有查找到,就会到到根类NSObject,NSObject的父对象是nil的(参考下面经典的类关系图),这个时候如果还没有查找到,就开始进入消息转发了。

类关系图.png

6.进入消息转发阶段以后。

  • 首先是调用resolveInstanceMethod:或者resolveClassMethod:,这一步可以给当前类添加方法,来响应这个过程。
  • 调用forwardingTargetForSelector:,这一步是寻找一个备援接受者来响应这个而方法。
  • 调用methodSignatureForSelector和forwardInvocation,完整的消息转发,通过NSInvocation来响应这个方法。


    消息转发过程.png

7.如果上述过程都没有响应,那么则会crash,报unrecognized selector sent to instance的错误。

二、objc_msgSend

   当编译器遇到一个方法调用时,它会将方法的调用翻译成以下函数中的一个 objc_msgSend、objc_msgSend_stret、objc_msgSendSuper 和 objc_msgSendSuper_stret。发送给对象的父类的消息会使用 objc_msgSendSuper 有数据结构作为返回值的方法会使用 objc_msgSendSuper_stret 或 objc_msgSend_stret 其它的消息都是使用 objc_msgSend 发送的。

   在objc_msgSend是OC实例对象和类对象发送消息的核心引擎,用来查找方法实现,对性能要求较高,因此这一部分是通过汇编代码来编写的。下面是欧阳大哥通过汇编代码,翻译的c代码深入解构objc_msgSend函数的实现

//下面的结构体中只列出objc_msgSend函数内部访问用到的那些数据结构和成员。

/*
其实SEL类型就是一个字符串指针类型,所描述的就是方法字符串指针
*/
typedef char * SEL;

/*
IMP类型就是所有OC方法的函数原型类型。
*/
typedef id (*IMP)(id self, SEL _cmd, ...); 


/*
  方法名和方法实现桶结构体
*/
struct bucket_t  {
    SEL  key;       //方法名称
    IMP imp;       //方法的实现,imp是一个函数指针类型
};

/*
   用于加快方法执行的缓存结构体。这个结构体其实就是一个基于开地址冲突解决法的哈希桶。
*/
struct cache_t {
    struct bucket_t *buckets;    //缓存方法的哈希桶数组指针,桶的数量 = mask + 1
    int  mask;        //桶的数量 - 1
    int  occupied;   //桶中已经缓存的方法数量。
};

/*
    OC对象的类结构体描述表示,所有OC对象的第一个参数保存是的一个isa指针。
*/
struct objc_object {
  void *isa;
};

/*
   OC类信息结构体,这里只展示出了必要的数据成员。
*/
struct objc_class : objc_object {
    struct objc_class * superclass;   //基类信息结构体。
    cache_t cache;    //方法缓存哈希表
    //... 其他数据成员忽略。
};



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

上面的代码,总结一下:
1.对象空值判断,这个就是在OC中为什么给空对象发送消息,不crash的原因。
2. 获取或者构造对象的isa数据,通过isa查找类或者元类
3. 遍历缓存哈希桶并查找缓存中的方法实现,通过cache查找是否命中缓存
4. 执行方法实现或方法未命中缓存处理函数objc_msgSend_uncached

未命中缓存

三、lookUpImpOrForward

lookUpImpOrForward是方法调用过程的核心类,方法的查找、类的初始化、initialize都可能在这里面调用。

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

    runtimeLock.assertUnlocked();

    //1. 缓存查找
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    runtimeLock.lock();
    checkIsKnownClass(cls);
     //2. 类是否实现
    if (!cls->isRealized()) {
        realizeClass(cls);
    }
     //3. 类是否初始化
    if (initialize  &&  !cls->isInitialized()) {
        runtimeLock.unlock();
        _class_initialize (_class_getNonMetaClass(cls, inst));
        runtimeLock.lock();
    }

    
 retry:    
    runtimeLock.assertLocked();

   
    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 4.方法列表查找,查找到以后,进行缓存。
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 5.父类方法列表查找,查找到进行缓存。
    {
        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 {
                    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;
            }
        }
    }

    // 6.如果还没有查找到。进入消息转发resolveMethod方法

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

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

 done:
    runtimeLock.unlock();

    return imp;
}

lookUpImpOrForward方法有如下过程:
1. 缓存中查找方法
2. 类是否实现
3.类是否初始化
4.方法列表查找,查找到以后,进行缓存。
5.父类方法列表查找,查找到进行缓存。
6.如果还没有查找到。进入消息转发resolveMethod方法
这里的方法查找过程,我在第一部分的方法调用过程中都有描述过,我重点说一下2和3,这两部分是做什么。

  • 类是否实现,这一部分主要是判断类是否是第一次调用,第一次调用的时候,class_rw_t可能还没有创建好,因为方法是存在这里面的,所以要保证类已经实现。
  • 类是否初始化,这一部分主要是初始化类的一些参数,包括isa指针,同时我们熟悉的Initialize方法也是在这里调用的。

四、消息转发

消息转发是在运行时进行的,大致分为三个阶段:
第一阶段是先检查接收者,看是否能通过runtime动态添加一个方法,来处理这个方法;
第二阶段就是备援接收者,看看有没有对象可以响应这个方法。
第二阶段就是把该消息的全部信息封装到NSInvocation对象中,看哪个对象能否处理,如果还无法处理,则报错unrecognized selector sent to instance。

1.动态方法解析

// 类方法专用
+ (BOOL)resolveClassMethod:(SEL)sel
// 对象方法专用
+ (BOOL)resolveInstanceMethod:(SEL)sel

2.备援接收者

- (id)forwardingTargetForSelector:(SEL)aSelector

3.完整消息转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

在方法签名的过程中,注意签名符号:

*          代表  char * 
char BOOL  代表  c
:          代表  SEL 
^type      代表  type *
@          代表  NSObject * 或 id
^@         代表  NSError ** 
#          代表  NSObject 
v          代表  void
消息转发过程.png

五、总结

方法的调用过程:
1.缓存查找
2.查找当前类的缓存及方法。
3.查找父类的缓存及方法
4.消息转发

参考:
objc4-750源码
从源代码看 ObjC 中消息的发送.md
深入解构objc_msgSend函数的实现
iOS消息转发机制实例
iOS的消息转发机制详解

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

推荐阅读更多精彩内容

  • 关于OC中的消息发送的实现,在去年也看过一次,当时有点不太理解,但是今年再看却很容易理解。 我想这跟知识体系的构建...
    咖啡绿茶1991阅读 934评论 0 1
  • 消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 ...
    lylaut阅读 1,821评论 2 3
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 2019年1月3日,星期四,阴。 昨天忙活一天,晚上喝多了点。 雇主是上次帝都那小伙。事不繁重,安装两...
    距离负人阅读 179评论 0 5
  • 那天我做了一个梦。 那个小小的家里,爷爷还坐在那张他专属的板凳上,奶奶像平时一样笨拙的忙进忙出。 我似乎是一个局外...
    相佯阅读 188评论 0 0