iOS Runtime

objc源码之Method消息发送

2019-09-28  本文已影响0人  繁星mind

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.进入消息转发阶段以后。

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,这两部分是做什么。

四、消息转发

消息转发是在运行时进行的,大致分为三个阶段:
第一阶段是先检查接收者,看是否能通过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的消息转发机制详解

上一篇下一篇

猜你喜欢

热点阅读