深入解构objc_msgSend函数的实现

96
欧阳大哥2013 595a1b60 08f6 4beb 998f 2bf55e230555
0.6 2018.08.13 09:09* 字数 5658

阅读本文后你将会进一步了解Runtime的实现,享元设计模式的实践,内存数据存储优化,编译内存屏障,多线程无锁读写实现,垃圾回收等相关的技术点。

objc_class(Class对象)结构简介

熟悉OC语言的Runtime(运行时)机制以及对象方法调用机制的开发者都知道,所有OC方法调用在编译时都会转化为对C函数objc_msgSend的调用。

/*下面的例子是在arm64体系下的函数调用实现,本文中如果没有特殊说明都是指在arm64体系下的结论*/
   // [view1 addSubview:view2];
  objc_msgSend(view1, "addSubview:", view2);
      
   // CGSize size = [view1 sizeThatFits:CGSizeZero];
   CGSize size = objc_msgSend(view1, "sizeThatFits:", CGSizeZero);

   //  CGFloat alpha = view1.alpha; 
   CGFloat alpha = objc_msgSend(view1, "alpha");

系统的Runtime库通过函数objc_msgSend以及OC对象中隐藏的isa数据成员来实现多态和运行时方法查找以及执行。每个对象的isa中保存着这个对象的类对象指针,类对象是一个Class类型的数据,而Class则是一个objc_class结构体指针类型的别名,它被定义如下:

   typedef struct objc_class * Class;

虽然在对外公开暴露的头文件#import <objc/runtime.h>中可以看到关于struct objc_class的定义,但可惜的是那只是objc1.0版本的定义,而目前所运行的objc2.0版本运行时库并没有暴露出struct objc_class所定义的详细内容。

你可以在https://opensource.apple.com/source/objc4/objc4-723/中下载和查看开源的最新版本的Runtime库源代码。Runtime库的源代码是用汇编和C++混合实现的,你可以在头文件objc-runtime-new.h中看到关于struct objc_class结构的详细定义。objc_class结构体用来描述一个OC类的类信息:包括类的名字、所继承的基类、类中定义的方法列表描述、属性列表描述、实现的协议描述、定义的成员变量描述等等信息。在OC中类信息也是一个对象,所以又称类信息为Class对象。 下面是一张objc_class结构体定义的静态类图:

objc_class结构

图片最左边显示的内容有一个编辑错误,不应该是NSObject而应该是objc_class。

objc_class结构体中的数据成员非常的多也非常的复杂,这里并不打算深入的去介绍它,本文主要介绍的是objc_msgSend函数内部的实现,因此在下面的代码中将会隐藏大部分数据成员的定义,并在不改变真实结构体定义的基础上只列出objc_msgSend方法内部会访问和使用到的数据成员。

objc_msgSend函数的内部实现

objc_msgSend函数是所有OC方法调用的核心引擎,它负责查找真实的类或者对象方法的实现,并去执行这些方法函数。因调用频率是如此之高,所以要求其内部实现近可能达到最高的性能。这个函数的内部代码实现是用汇编语言来编写的,并且其中并没有涉及任何需要线程同步和锁相关的代码。你可以在上面说到的开源URL链接中的Messengers文件夹下查看各种体系架构下的汇编语言的实现。

     ;这里列出的是在arm64位真机模式下的汇编代码实现。
    0x18378c420 <+0>:   cmp    x0, #0x0                  ; =0x0 
    0x18378c424 <+4>:   b.le   0x18378c48c               ; <+108>
    0x18378c428 <+8>:   ldr    x13, [x0]
    0x18378c42c <+12>:  and    x16, x13, #0xffffffff8
    0x18378c430 <+16>:  ldp    x10, x11, [x16, #0x10]
    0x18378c434 <+20>:  and    w12, w1, w11
    0x18378c438 <+24>:  add    x12, x10, x12, lsl #4
    0x18378c43c <+28>:  ldp    x9, x17, [x12]
    0x18378c440 <+32>:  cmp    x9, x1
    0x18378c444 <+36>:  b.ne   0x18378c44c               ; <+44>
    0x18378c448 <+40>:  br     x17
    0x18378c44c <+44>:  cbz    x9, 0x18378c720           ; _objc_msgSend_uncached
    0x18378c450 <+48>:  cmp    x12, x10
    0x18378c454 <+52>:  b.eq   0x18378c460               ; <+64>
    0x18378c458 <+56>:  ldp    x9, x17, [x12, #-0x10]!
    0x18378c45c <+60>:  b      0x18378c440               ; <+32>
    0x18378c460 <+64>:  add    x12, x12, w11, uxtw #4
    0x18378c464 <+68>:  ldp    x9, x17, [x12]
    0x18378c468 <+72>:  cmp    x9, x1
    0x18378c46c <+76>:  b.ne   0x18378c474               ; <+84>
    0x18378c470 <+80>:  br     x17
    0x18378c474 <+84>:  cbz    x9, 0x18378c720           ; _objc_msgSend_uncached
    0x18378c478 <+88>:  cmp    x12, x10
    0x18378c47c <+92>:  b.eq   0x18378c488               ; <+104>
    0x18378c480 <+96>:  ldp    x9, x17, [x12, #-0x10]!
    0x18378c484 <+100>: b      0x18378c468               ; <+72>
    0x18378c488 <+104>: b      0x18378c720               ; _objc_msgSend_uncached
    0x18378c48c <+108>: b.eq   0x18378c4c4               ; <+164>
    0x18378c490 <+112>: mov    x10, #-0x1000000000000000
    0x18378c494 <+116>: cmp    x0, x10
    0x18378c498 <+120>: b.hs   0x18378c4b0               ; <+144>
    0x18378c49c <+124>: adrp   x10, 202775
    0x18378c4a0 <+128>: add    x10, x10, #0x220          ; =0x220 
    0x18378c4a4 <+132>: lsr    x11, x0, #60
    0x18378c4a8 <+136>: ldr    x16, [x10, x11, lsl #3]
    0x18378c4ac <+140>: b      0x18378c430               ; <+16>
    0x18378c4b0 <+144>: adrp   x10, 202775
    0x18378c4b4 <+148>: add    x10, x10, #0x2a0          ; =0x2a0 
    0x18378c4b8 <+152>: ubfx   x11, x0, #52, #8
    0x18378c4bc <+156>: ldr    x16, [x10, x11, lsl #3]
    0x18378c4c0 <+160>: b      0x18378c430               ; <+16>
    0x18378c4c4 <+164>: mov    x1, #0x0
    0x18378c4c8 <+168>: movi   d0, #0000000000000000
    0x18378c4cc <+172>: movi   d1, #0000000000000000
    0x18378c4d0 <+176>: movi   d2, #0000000000000000
    0x18378c4d4 <+180>: movi   d3, #0000000000000000
    0x18378c4d8 <+184>: ret    
    0x18378c4dc <+188>: nop    

毕竟汇编语言代码比较晦涩难懂,因此这里将函数的实现反汇编成C语言的伪代码:

//下面的结构体中只列出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, ....);
}

可以看出objc_msgSend函数的实现逻辑主要分为4个部分:

1. 对象空值判断

首先对传进来的方法接收者receiver进行是否为空判断,如果是nil则函数直接返回,这也就说明了当对一个nil对象调用方法时,不会产生崩溃,也不会进入到对应的方法实现中去,整个过程其实什么也不会发生而是直接返回nil。

2. 获取或者构造对象的isa数据

通常情况下每个OC对象的最开始处都有一个隐藏的数据成员isa,isa保存有类的描述信息,所以在执行方法前就需要从对象处获取到这个指针值。为了减少内存资源的浪费,苹果提出了Tagged Pointer类型对象的概念。比如一些NSString和NSNumber类型的实例对象就会被定义为Tagged Pointer类型的对象。Tagged Pointer类型的对象采用一个跟机器字长一样长度的整数来表示一个OC对象,而为了跟普通OC对象区分开来,每个Tagged Pointer类型对象的最高位为1而普通的OC对象的最高位为0。因此上面的代码中如果对象receiver地址的最高位为1则会将对象当做Tagged Pointer对象来处理。从代码实现中还可以看出系统中存在两种类型的Tagged Pointer对象:如果是高四位全为1则是用户自定义扩展的Tagged Pointer对象,否则就是系统内置的Tagged Pointer对象。因为Tagged Pointer对象中是不可能保存一个isa的信息的,而是用Tagged Pointer类型的对象中的某些bit位来保存所属的类信息的索引值。系统分别定义了两个全局数组变量:

   extern "C" { 
    extern Class objc_debug_taggedpointer_classes[16*2];
    extern Class objc_debug_taggedpointer_ext_classes[256];
}

来保存所有的Tagged Pointer类型的类信息。对于内置Tagged Pointer类型的对象来说,其中的高四位保存的是一个索引值,通过这个索引值可以在objc_debug_taggedpointer_classes数组中查找到对象所属的Class对象;对于自定义扩展Tagged Pointer类型的对象来说,其中的高52位到59位这8位bit保存的是一个索引值,通过这个索引值可以在objc_debug_taggedpointer_ext_classes数组中查找到对象所属的Class对象。

思考和实践: Tagged Pointer类型的对象中获取isa数据的方式采用的是享元设计模式,这种设计模式在一定程度上还可以缩小一个对象占用的内存尺寸。还有比如256色的位图中每个像素位置中保存的是颜色索引值而非颜色的RGB值,从而减少了低色彩位图的文件存储空间。保存一个对象引用可能需要占用8个字节,而保存一个索引值时可能只需要占用1个字节。

在第二步中不管是普通的OC对象还是Tagged Pointer类型的对象都需要找到对象所属的isa信息,并进一步找到所属的类对象,只有找到了类对象才能查找到对应的方法的实现。

isa的内部结构

上面的代码实现中,在将isa转化为struct objc_class 时发现还进行一次和0xffffffff8的与操作。虽然isa是一个长度为8字节的指针值, 但是它保存的值并不一定是一个struct objc_class 对象的指针。在arm64位体系架构下的用户进程最大可访问的虚拟内存地址范围是0x0000000000 - 0x1000000000,也就是每个用户进程的可用虚拟内存空间是64GB。同时因为一个指针类型的变量存在着内存地址对齐的因素所以指针变量的最低3位一定是0。所以将isa中保存的内容和0xffffffff8进行与操作得到的值才是真正的对象的Class对象指针。 arm64体系架构对isa中的内容进行了优化设计,它除了保存着Class对象的指针外,还保存着诸如OC对象自身的引用计数值,对象是否被弱引用标志,对象是否建立了关联对象标志,对象是否正在销毁中等等信息。如果要想更加详细的了解isa的内部结构请参考文章:https://blog.csdn.net/u012581760/article/details/81230721 中的介绍。

思考和实践:对于所有指针类型的数据,我们也可以利用其中的特性来使用0-2以及36-63这两个区段的bit位进行一些特定数据的存储和设置,从而减少一些内存的浪费和开销。

3. 遍历缓存哈希桶并查找缓存中的方法实现

一个Class对象的数据成员中有一个方法列表数组保存着这个类的所有方法的描述和实现的函数地址入口。如果每次方法调用时都要进行一次这样的查找,而且当调用基类方法时,还需要遍历基类进行方法查找,这样势必会对性能造成非常大的损耗。为了解决这个问题系统为每个类建立了一个哈希表进行方法缓存(objc_class 中的数据成员cache是一个cache_t类型的对象)。这个哈希表缓存由哈希桶来实现,每次当执行一个方法调用时,总是优先从这个缓存中进行方法查找,如果找到则执行缓存中保存的方法函数,如果不在缓存中才到Class对象中的方法列表数组或者基类的方法列表数组中去查找,当找到后将方法名和方法函数地址保存到缓存中以便下次加速执行。所以objc_msgSend函数第3部分的内容主要实现的就是在Class对象的缓存哈希表中进行对应方法的查找:

☛ 3.1 函数首先将方法名op与cache中的mask进行与操作。这个mask的值是缓存中桶的数量减1,一个类初始缓存中的桶的数量是4,每次桶数量扩容时都乘2。也就是说mask的值的二进制的所有bit位数全都是1,这样当op和mask进行与操作时也就是取op中的低mask位数来命中哈希桶中的元素。因此这个哈希算法所得到的index索引值一定是小于缓存中桶的数量而不会出现越界的情况。

☛3.2 当通过哈希算法得到对应的索引值后,接下来便判断对应的桶中的key值是否和op相等。每个桶是一个struct bucket_t 结构,里面保存这方法的名称(key)和方法的实现地址(imp)。一旦key值和op值相等则表明缓存命中,然后将其中的imp值进行保存并结束查找跳出循环;而一旦key值为NULL时则表明此方法尚未被缓存,需要跳出循环进行方法未命中缓存处理;而当key为非NULL但是又不等于op时则表明出现冲突了,这里解决冲突的机制是采用开地址法将索引值减1来继续循环来查找缓存。

当你读完第3部分代码时是否会产生如下几个问题的思考:
问题一: 缓存中哈希桶的数量会随着方法访问的数量增加而动态增加,那么它又是如何增加的?

问题二: 缓存循环查找是否会出现死循环的情况?

问题三: 当桶数量增加后mask的值也会跟着变化,那么就会存在着前后两次计算index的值不一致的情况,这又如何解决?

问题四: 既然哈希桶的数量会在运行时动态添加那么在多线程访问环境下又是如何做同步和安全处理的?

这四个问题都会在第4步中的objc_msgSend_uncached函数内部实现中找到答案。

4. 执行方法实现或方法未命中缓存处理函数

当方法在哈希桶中被命中并且存在对应的方法函数实现时就会调用对应的方法实现并且函数返回,整个函数执行完成。而当方法没有被缓存时则会调用objc_msgSend_uncached函数,这个函数的实现也是用汇编语言编写的,它的函数内部做了两件事情:一是调用_class_lookupMethodAndLoadCache3函数在Class对象中查找方法的实现体函数并返回;二是调用返回的实现体函数来执行对应的方法。可以从_class_lookupMethodAndLoadCache3函数名中看出它的功能实现就是先查找后缓存,而这个函数则是用C语言实现的,因此可以很清晰的去阅读它的源代码实现。_class_lookupMethodAndLoadCache3函数的源代码实现主要就是先从Class对象的方法列表或者基类的方法列表中查找对应的方法和实现,并且更新到Class对象的缓存cache中。如果你仔细阅读里面的源代码就可以很容易回答在第3步所提出的四个问题:

💡问题一: 缓存中哈希桶的数量会随着方法访问的数量增加而动态增加,那么它又是如何增加的?
🔑: 每个Class类对象初始化时会为缓存分配4个桶,并且cache中有一个数据成员occupied来保存缓存中已经使用的桶的数量,这样每当将一个方法的缓存信息保存到桶中时occupied的数量加1,如果数量到达桶容量的3/4时,系统就会将桶的容量增大2倍变,并按照这个规则依次继续扩展下去。

💡问题二: 缓存循环查找是否会出现死循环的情况?
🔑:不会,因为系统总是会将空桶的数量保证有1/4的空闲,因此当循环遍历时一定会出现命中缓存或者会出现key == NULL的情况而退出循环。

💡问题三: 当桶数量增加后mask的值也会跟着变化,那么就会存在着前后两次计算index的值不一致的情况,这又如何解决?
🔑: 每次哈希桶的数量扩容后,系统会为缓存分配一批新的空桶,并且不会维护原来老的缓存中的桶的信息。这样就相当于当对桶数量扩充后每个方法都是需要进行重新缓存,所有缓存的信息都清0并重新开始。因此不会出现两次index计算不一致的问题。

💡问题四: 既然哈希桶的数量会在运行时动态添加那么在多线程访问环境下又是如何做同步和安全处理的?
🔑:在整个objc_msgSend函数中对方法缓存的读取操作并没有增加任何的锁和同步信息,这样目的是为了达到最佳的性能。在多线程环境下为了保证对数据的安全和同步访问,需要在写写和读写两种场景下进行安全和同步处理:
☞首先来考察多线程同时写cache缓存的处理方法。假如两个线程都检测到方法并未在缓存中而需要扩充缓存或者写桶数据时,在扩充缓存和写桶数据之前使用了一个全局的互斥锁来保证写入的同步处理,而且在锁住的范围内部还做了一次查缓存的处理,这样即使在两个线程调用相同的方法时也不会出现写两次缓存的情况。因此多线程同时写入的解决方法只需要简单的引入一个互斥锁即可解决问题。

☞再来考察多线程同时读写cache缓存的处理方法。上面有提到当对缓存中的哈希桶进行扩充时,系统采用的解决方法是完全丢弃掉老缓存的内存数据,而重新开辟一块新的哈希桶内存并更新Class对象cache中的所有数据成员。因此如果处理不当就会在objc_msgSend函数的第3步中访问cache中的数据成员时发生异常。为了解决这个问题在objc_msgSend函数的第四条指令中采用了一种非常巧妙的方法:

 0x18378c430 <+16>:  ldp    x10, x11, [x16, #0x10]

这条指令中会把cache中的哈希桶buckets和mask|occupied整个结构体数据成员分别读取到x10和x11两个寄存器中去。因为CPU能保证单条指令执行的原子性,而且在整个后续的汇编代码中函数并没有再次去读取cache中的buckets和mask数据成员,而是一直使用x10和x11两个寄存器中的值来进行哈希表的查找。所以即使其他写线程扩充了cache中的哈希桶的数量和重新分配了内存也不会影响当前读线程的数据访问。在写入线程扩充哈希桶数量时会更新cache中的buckets和mask两个数据成员的值。这部分的实现代码如下:

//设置更新缓存的哈希桶内存和mask值。
  void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    // objc_msgSend uses mask and buckets with no locks.
    // It is safe for objc_msgSend to see new buckets but old mask.
    // (It will get a cache miss but not overrun the buckets' bounds).
    // It is unsafe for objc_msgSend to see old buckets and new mask.
    // Therefore we write new buckets, wait a lot, then write new mask.
    // objc_msgSend reads mask first, then buckets.

    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    buckets = newBuckets;
    
    // ensure other threads see new buckets before new mask
    mega_barrier();
    
    mask = newMask;
    occupied = 0;
}

这段代码是用C++编写实现的。代码中先修改哈希桶数据成员buckets再修改mask中的值。为了保证赋值的顺序不被编译器优化这里添加了mega_baerrier()来实现编译内存屏障(Compiler Memory Barrier)。假如不添加编译内存屏障的话,编译器有可能会优化代码让mask先赋值而buckets后赋值,这样会造成什么后果呢?当写线程先执行完mask赋值并在执行buckets赋值前读线程执行ldp x10, x11, [x16, #0x10]指令时就有可能读取到新的mask值和老的buckets值,而新的mask值要比老的mask值大,这样就会出现内存数组越界的情况而产生崩溃。而如果添加了编译内存屏障,就会保证先执行buckets赋值而后执行mask赋值,这样即使在写线程执行完buckets赋值后而在执行mask赋值前,读线程执行ldp x10, x11, [x16, #0x10]时得到新的buckets值和老的mask值是也不会出现异常。 可见可以在一定的程度上借助编译内存屏障相关的技巧来实现无锁读写同步技术。当然假如这段代码不用高级语言而用汇编语言来编写则可以不用编译内存屏障技术而是用stp指令来写入新的buckets和mask值也能保证顺序的写入。

思考和实践:如果你想了解编译屏障相关的知识请参考文章https://blog.csdn.net/world_hello_100/article/details/50131497的介绍

对于多线程读写的情况还有一个问题需要解决,就是因为写线程对缓存进行了扩充而分配了新的哈希桶内存,同时会销毁老的哈希桶内存,而此时如果读线程中正在访问的是老缓存时,就有可能会因为处理不当时会发生读内存异常而系统崩溃。为了解决这个问题系统将所有会访问到Class对象中的cache数据的6个API函数的开始地址和结束地址保存到了两个全局的数组中:

 uintptr_t objc_entryPoints[] = {cache_getImp, objc_msgSend, objc_msgSendSuper, objc_msgSendSuper2, objc_msgLookup, objc_msgLookupSuper2};
//LExit开头的表示的是函数的结束地址。
 uintptr_t objc_exitPoints[] = {LExit_cache_getImp,LExit_objc_msgSend, LExit_objc_msgSendSuper, LExit_objc_msgSendSuper2, LExit_objc_msgLookup,LExit_objc_msgLookupSuper2};

当某个写线程对Class对象cache中的哈希桶进行扩充时,会先将已经分配的老的需要销毁的哈希桶内存块地址,保存到一个全局的垃圾回收数组变量garbage_refs中,然后再遍历当前进程中的所有线程,并查看线程状态中的当前PC寄存器中的值是否在objc_entryPoints和objc_exitPoints这个范围内。也就是说查看是否有线程正在执行objc_entryPoints列表中的函数,如果没有则表明此时没有任何函数会访问Class对象中的cache数据,这时候就可以放心的将全局垃圾回收数组变量garbage_refs中的所有待销毁的哈希桶内存块执行真正的销毁操作;而如果有任何一个线程正在执行objc_entryPoints列表中的函数则不做处理,而等待下次再检查并在适当的时候进行销毁。这样也就保证了读线程在访问Class对象中的cache中的buckets时不会产生内存访问异常。

思考和实践:上面描述的技术解决方案其实就是一种垃圾回收技术的实现。垃圾回收时不立即将内存进行释放,而是暂时将内存放到某处进行统一管理,当满足特定条件时才将所有分配的内存进行统一销毁释放处理。

objc2.0的runtime巧妙的利用了ldp指令、编译内存屏障技术、内存垃圾回收技术等多种手段来解决多线程数据读写的无锁处理方案,提升了系统的性能,你是否get到这些技能了呢?

小结

上面就是objc_msgSend函数内部实现的所有要说的东西,您是否在这篇文章中又收获了新的知识?是否对Runtime又有了进一步的认识?在介绍这些东西时,还顺便介绍了享元模式的相关概念,以及对指针类型数据的内存使用优化,还介绍了多线程下的无锁读写相关的实现技巧等等。如果你喜欢这篇文章就记得为我点一个赞👍吧,


欢迎大家访问我的github地址简书地址

iOS
Web note ad 1