Objective-C基础之五(Runtime之Class结构解析)

983 阅读37分钟

isa

在之前的学习中,我们了解到isa指针在runtime机制中起到了非常大的作用,通过实例对象的isa指针,我们可以找到类对象,通过类对象的isa指针我们可以找到元类对象,在通过查看objc4的源码,我们可以看到isa指针是一个union isa_t类型的共用体。

其实在arm64之前,isa只是单纯的一个指针,里面存放了类对象(class)、元类对象(mata-class)的地址值。但是在arm64之后,isa指针被优化为共用体的结构,并且使用位域的技术来使得isa中可以存储更多的信息。

此处objc4的源码版本我选择的是objc4-756.2

位运算

在深入了解isa内部结构之前,我们先来简单了解一下什么是位运算。计算机内存中存储的数据都是以二进制的形式存储的,也就是0或者1,而位运算就是直接对内存中的二进制位进行操作,所以它的运算效率非常高。常用位运算有以下几种,此处以C语言为例:

运算符 用法
按位与(&) a & b
按位或(|) a | b
按位异或(^) a ^ b
按位取反(~) ~a
左移(<<) a << b
右移(>>) a >> b

按位与(&)

运算规则:相同位的两个数字都为1,则为1,若其中有一个不为1,则为1

举例:有两个数12和20进行按位与运算,20的二进制为0b00001011,12的二进制格式为0b00010100

 //12和22进行按位与运算
   0000 1011  (20)
 & 0001 1100  (12)
 ------------
   0000 1000

按位或(|)

运算规则:相同位的两个数字只要有一个为1,则为1

举例:有两个数12和20进行按位或运算,20的二进制为0b00001011,12的二进制格式为0b00010100

 //12和22进行按位或运算
   0000 1011  (20)
 | 0001 1100  (12)
 ------------
   0001 1111

按位异或(^)

运算规则:相同位置的两个数字相同则为0,不同则为1

举例:有两个数12和20进行按位异或运算,20的二进制为0b00001011,12的二进制格式为0b00010100

 //12和22进行按位异或运算
   0000 1011  (20)
 ^ 0001 1100  (12)
 ------------
   0001 0111

按位取反(~)

运算规则:将二进制的每一位变成相反的数,1->0或者0->1

举例:对0b0100 1110进行取反操作

  ~0100 1110
 ------------
   1011 0001

左移(<<)

运算规则:将一个运算对象的各个二进制位全部左移若干位(注意:最左侧的二进制位丢弃,右侧二进制位补0)

举例:(0b0100 1110)<<1、(0b1100 1110)<<1

   0100 1110<<1    1100 1110<<1
 ---------------  --------------
   1001 1100       1001 1100

如果左侧最高位不为1,那么左移相当于将原有数乘以2,此处0b0100 1110的十进制数为78,左移一位得到的十进制数为156

右移(<<)

运算规则:将一个运算对象的各个二进制位全部右移若干位(注意:正数左侧补0,负数左侧补1,右侧二进制位丢弃)

举例:(0b1100 1110)>>1

   1100 1110>>1
 ---------------
   0110 0111

操作的数每右移一位相当于改数除以2,此处0b1100 1110的十进制数为206,右移一位得到十进制数为103

共用体

在C语言中,共用体其实就是将不同类型的变量存放到同一段内存单元中,使用覆盖技术,几个变量共同占用同一段内存结构,相互覆盖。

//创建一个共用体size
union {
    int height; //宽度
    int width;  //高度
}size;

//对共用体进行操作
size.height = 10;
//如果将height的值修改为10,这时去打印width的值结果显示为10
NSLog(@"%d",size.width);

//如果将width的值修改为20,这时去打印height的值结果显示为20
size.width = 20;
NSLog(@"%d",size.height);

我们定义一个共用体size,其中有两个int类型的成员变量height和width,各占用4个字节,但是在共用体中,这两个成员变量共用4个字节的内存空间,一旦修改其中一个成员变量的值,另一个成员变量的值也会跟着修改。

位域

位域(又叫做位短)其实是一种数据结构,它可以将数据以二进制位的形式来存储,并且允许对此结构的位进行运算。有些信息在存储的时候,并不需要占用一个完整字节,有时候只需要占用一个或几个二进制位,比如存放一个BOOL类型的变量时,只需要保存0或1两种状态,此时只需要1个二进制位就能存储。因此,位域就是运用在这种场景下的一种数据结构,使用位域可以有效的节省存储空间。

位域可以把一个字节中的二进制位划分为几个不同的区域,并且制定每个区域占用的位数,每个域可以设置一个域名,可以根据域名对指定的位进行操作。

但是位域也有明显的缺点,就是它的内存分配和内存对齐的方式依赖于具体的机器和操作系统,不同的平台可能会有不同的结果。

位域的结构和结构体类似,它的形式为

struct 位域结构名称{
    类型说明符 位域名 : 位域长度;
    类型说明符 位域名 : 位域长度;
    类型说明符 位域名 : 位域长度;
    ......
}

//具体事例
struct size{
    unsigned int width  : 4;
    unsigned int height : 4;
    unsigned int area   : 8;
};

此处需要注意的是:位域成员必须声明为int、unsigned int或signed int类型(short char long)

通过sizeof(struct size)可以得到位域所占用内存大小为4个字节,其实,如果不使用位域的话,整个size结构体占用的内存大小为12个字节(int占用4个字节),但是使用位域之后,size总共只占用了4个字节,因为其中的width占用一个字节中的4位,height占4位,area占8位,共占16位,共2个字节,但是由于内存对齐的原则,这个size共占用4个字节的内存。因此,通过位域就可以大量节省内存消耗。如果想了解更多关于位域和内存对齐的知识可以自行查询资料,此处只是简单的做一下介绍。

位域和共用体结合,配合位运算给XLPerson增加BOOL属性

理解了位运算,位域和共用体的知识,我们现在就通过具体的实例来加深理解,首先创建一个XLPerson类,如果我们要给XLPerson类增加属性,可以使用以下方式

@interface XLPerson : NSObject

@property(nonatomic, assign)int height;

@end

但是通过@property这种方式创建的属性,内部会自动生成_height成员变量,因此,我们需要自己来实现setter和getter方法,XLPerson.h如下

@interface XLPerson : NSObject

- (void)setHappy:(BOOL)happy;
- (void)setSad:(BOOL)sad;
- (void)setAlone:(BOOL)alone;

- (BOOL)happy;
- (BOOL)sad;
- (BOOL)alone;

@end

XLPerson.m完整代码如下

#import "XLPerson.h"

#define XLPersonHappyMask (1 << 0)
#define XLPersonSadMask (1 << 1)
#define XLPersonAloneMask (1 << 2)

/** 使用一个字节来存储多个BOOL属性 */
@implementation XLPerson{
    union {
        char bits; //共用一个字节 0b0000 0000
        struct {
            char happy  : 1;    //happy占一位
            char sad    : 1;    //sad占一位
            char alone  : 1;    //alone占一位
        };
    } _emotion;
    
}

- (void)setHappy:(BOOL)happy{
    if (happy) {
        //将0b0000 0000的最后一位设置为1
        _emotion.bits |= XLPersonHappyMask;
    }else{
        _emotion.bits &= ~XLPersonHappyMask;
    }
}

- (void)setSad:(BOOL)sad{
    if (sad) {
        //将0b0000 0000的倒数第二位设置为1
        _emotion.bits |= XLPersonSadMask;
    }else{
        _emotion.bits &= ~XLPersonSadMask;
    }
}

- (void)setAlone:(BOOL)alone{
    if (alone) {
        //将0b0000 0000的倒数第三位设置为1
        _emotion.bits |= XLPersonAloneMask;
    }else{
        _emotion.bits &= ~XLPersonAloneMask;
    }
}

- (BOOL)happy{
    return !!(_emotion.bits & XLPersonHappyMask);
}

- (BOOL)sad{
    return !!(_emotion.bits & XLPersonSadMask);
}

- (BOOL)alone{
    return !!(_emotion.bits & XLPersonAloneMask);
}


@end

  • 首先,通过位域和共用体结合,我们创建出了_emotion这个共用体,内部有一个char类型的成员变量bits,占用1个字节,共用体内部有一个结构体,有三个成员变量,分别为happy、sad和alone,各占一位,共用bits这一个字节的存储空间,将bits用二进制位来表示就是0b0000 0000,从低位到高位(从右往左)3位依次代表happy、sad和alone
union {
    char bits; //共用一个字节 0b0000 0000
    struct {
        char happy  : 1;    //happy占一位
        char sad    : 1;    //sad占一位
        char alone  : 1;    //alone占一位
    };
} _emotion;
  • 以happy为例,它占用0b0000 0000的最后一位,因此要想改变happy的值,只要修改0b0000 0000最后一位为1或者0即可。要想将0b0000 0000的最后一位设置为1,只需要将0b0000 0000和0b0000 0001进行或运算。同理,要想将0b0000 0000的最后一位设置为0,只需要将0b0000 0000和0b1111 1110进行按位与运算。
//将最后一位设置为1
  0000 0000
 |0000 0001
 -----------
  0000 0001

//将最后一位设置为0
  0000 0000
 &1111 1110
 -----------
  0000 0000

所以,在代码中,我们单独为happy属性设置一个掩码,为XLPersonHappyMask,它的值为1 << 0,转换成二进制位就是0b0000 0001,然后通过此掩码来进行位运算,如下

- (void)setHappy:(BOOL)happy{
    if (happy) {
        //将0b0000 0000的最后一位设置为1
        _emotion.bits |= XLPersonHappyMask;
    }else{
        _emotion.bits &= ~XLPersonHappyMask;
    }
}
  • 外部在获取happy值的时候,需要拿到最后一位,这时可以通过bits和XLPersonHappyMask进行按位与操作,这个时候获取到的值最后一位一定是1,其它位一定为0,然后再对结果进行两次取反操作,就能拿到happy的值。
- (BOOL)happy{
    return !!(_emotion.bits & XLPersonHappyMask);
}

注意:此处进行按位与操作所得到的值可能为任何数,但是有一点不变,就是获取到的值要么为0,要么为任意数,因此,我们只要对按位与的结果进行两次取反,就能将最后的结果转换成0或者1.

  • sad、alone属性和happy属性实现方式相同,自此我们就使用共同体加位域实现了为XLPerson添加BOOL属性的功能。

isa_t类型详解

在最新版本的runtime源码中,NSObject类型最终会转化成object_class类型的结构体,而object_class继承自objc_object,在结构体objc_object中就含有isa_t类型的成员isa

struct objc_object {
private:
    isa_t isa;
public:
    ......
}

查看isa_t的源码,其中有除了两个构造函数外,有一个cls指针,还有一个uintptr_t类型的成员bits以及一个结构体:

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

查看结构体的源码可以发现,在结构体中使用位域来存储了很多信息,此处只展示arm64架构下的源码信息

#define ISA_MASK        0x0000000ffffffff8ULL
#define ISA_MAGIC_MASK  0x000003f000000001ULL
#define ISA_MAGIC_VALUE 0x000001a000000001ULL
struct {
    uintptr_t nonpointer        : 1;                                       
    uintptr_t has_assoc         : 1;                                       
    uintptr_t has_cxx_dtor      : 1;                                       
    uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ 
    uintptr_t magic             : 6;                                       
    uintptr_t weakly_referenced : 1;                                       
    uintptr_t deallocating      : 1;                                       
    uintptr_t has_sidetable_rc  : 1;                                       
    uintptr_t extra_rc          : 19
};

因此,我们可以将共用体isa_t的结构简化为以下形式:

//uintptr_t其实就是unsigned long类型,占8个字节
typedef unsigned long uintptr_t;
union isa_t {
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;                                       
        uintptr_t has_assoc         : 1;                                       
        uintptr_t has_cxx_dtor      : 1;                                       
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ 
        uintptr_t magic             : 6;                                       
        uintptr_t weakly_referenced : 1;                                       
        uintptr_t deallocating      : 1;                                       
        uintptr_t has_sidetable_rc  : 1;                                       
        uintptr_t extra_rc          : 19
    };
}

isa_t位域存放信息类型

isa_t作为共用体,内部使用8个字节的内存空间,共64位二进制位,存放了以下信息

  • nonpointer代表是否是优化过的isa指针,占用1位。
    • 1:表示新版本isa指针,使用位域来存储信息
    • 0:旧版本普通的isa指针,直接存储Class和Mata-Class的内存地址
  • has_assoc代表是否有关联对象,占用1位,一旦设置过关联对象,则会置为1。如果添加过关联对象,在释放时会检测是否有关联对象,所以释放会更慢。
  • has_cxx_dtor代表是否实现了C++的析构函数(.cxx_destruct),如果没有,释放时的速度会更快。占用1位
  • shiftcls中存放着类或者元类的内存地址,占用33位。
  • magic是调试时用来判断对象是否初始化文采,占用6位
  • weakly_referenced代表是否被弱引用指向过,占用1位,如果为0,则释放时速度会更快
  • deallocating用来表示对象是否正在释放,占用1位
  • extra_rc用来存储引用计数的值,占用19位,此处需要注意的时,它存储的是引用计数的值-1。如果对象的引用计数为1,则extra_rc中存储的值为0
  • has_sidetable_rc用来表示是否将引用计数存储在SiteTable中,引用计数的值过大,在extra_rc无法存储,则会将引用计数存放到SiteTable当中。

在函数objc_destructInstance中,我们可以区分出在什么情况下对象释放会更快

//释放一个实例对象
void *objc_destructInstance(id obj) {
    if (obj) {
        //判断是否有.cxx_destruct析构函数
        bool cxx = obj->hasCxxDtor();
        //判断是否有关联对象
        bool assoc = obj->hasAssociatedObjects();

        //g如果有.cxx_destruct析构函数,则调用此析构函数,占用部分时间
        if (cxx) object_cxxDestruct(obj);
        //如果有关联对象,则移除关联对象,占用部分时间
        if (assoc) _object_remove_assocations(obj);
        //释放对象
        obj->clearDeallocating();
    }
    return obj;
}

整个isa_t的内存结构图如下

通过Demo查看isa的具体内存结构

  • 创建Demo,创建NSObject的实例对象
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSObject *obj = [[NSObject alloc] init];
    __weak typeof(obj) weakObj = obj;
    objc_setAssociatedObject(obj, @"person", @"Jack", OBJC_ASSOCIATION_COPY_NONATOMIC);
}
  • 添加断点,通过LLDB的p指令,查看obj的isa指针的内存地址,此处需要使用真机来调试,因为我们主要研究arm64架构下的isa指针

  • 将obj的isa指针的内存地址0x000005a1ce6a7eb3转换成二进制可以得到如下结构

  • 因为当前是新版的isa指针类型,所以nonpointer的值为1。同时设置了关联对象,所以has_assoc值为1。obj对象有弱指针引用,所以weakly_referenced值为1。

Class底层结构分析

在Objective-C基础之一(深入理解OC对象)中,我们了解到,Class其实是一个objc_class类型的结构体,并且它继承自结构体objc_object,在结构体objc_object的内部则有一个isa_t类型的指针isa,用来存放类对象和元类对象内存地址等一系列信息,上文中有明确说明。接下来我们再次通过阅读源码来深入理解Class的底层结构。

  • objc_class由于是继承自结构体objc_object,所以它的结构我们可以简化,如下:
struct objc_class{
    Class ISA;                  //isa指针,通过位域存放多个信息
    Class superclass;           //supperClass
    cache_t cache;             // 方法缓存
    class_data_bits_t bits;    // 用来获取类的具体信息
}
  • objc_class中除了有isa指针外,还保存了父类的class,方法缓存以及当前类的一些基本信息。继续查看class_data_bits_t的源码如下,此处只展示主要方法
struct class_data_bits_t {
    uintptr_t bits;
    //通过data函数可以获取当前结构体中class_rw_t类型的结构体成员
    class_rw_t* data() {
        //通过按位与来获取到class_rw_t的内存地址
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    
    //通过safe_ro函数获取到class_ro_t类型结构体
    const class_ro_t *safe_ro() {
        //首先通过data函数获取到class_rw_t
        class_rw_t *maybe_rw = data();
        //使用class_rw_t中的flags进行按位与运算,判断当前data返回的是否是被实现的class_rw_t
        if (maybe_rw->flags & RW_REALIZED) {
            //当前是class_rw_t
            return maybe_rw->ro;
        } else {
            //当前是class_ro_t
            return (class_ro_t *)maybe_rw;
        }
    }
}

在class_data_bits_t中通过bits & FAST_DATA_MASK来获取到class_rw_t的内存地址,但是通过safe_ro函数可以看出,其实一开始在在class_data_bits_t中是不存在class_rw_t的,而是存放的class_ro_t,class_rw_t则是在之后进行创建的,具体会在下文中说明。

查看了class_rw_t和class_ro_t发现两者结构并不相同,但是因为在class_rw_t和class_ro_t中都有flags,并且都是第一个成员变量,因此不管是class_rw_t和class_ro_t它们的内存地址其实就是flags的内存地址,所以两者可以通过强制转换来拿到结构体中的flags。

class_rw_t

查看class_rw_t的源码,发现在class_rw_t存在一个成员变量class_ro_t,以及方法列表,属性列表和协议列表

struct class_rw_t {
    uint32_t flags;             //用来存放类的一些基本信息
    uint32_t version;           //版本号

    const class_ro_t *ro;       //class_ro_t类型指针

    method_array_t methods;     //方法列表
    property_array_t properties;//属性列表
    protocol_array_t protocols; //协议列表
}

class_rw_t中的方法列表、属性列表和协议列表其实都是二维数组,以method_array_t结构为例,可以发现在方法列表中其实存放的是method_list_t,而在method_list_t中存放的则是method_t,method_t中则存放了我们所需要的方法的基本信息。

class_rw_t结构图如下

class_ro_t

  • 查看class_ro_t的源码,发现在class_ro_t中也有方法列表、属性列表和协议列表
struct class_ro_t {
    uint32_t flags;                 //存放类的一些基本信息
    uint32_t instanceStart;
    uint32_t instanceSize;          //实例对象占用内存
    const char * name;              //当前类名
    method_list_t * baseMethodList; //方法列表
    protocol_list_t * baseProtocols;//协议列表
    const ivar_list_t * ivars;      //成员变量列表
    property_list_t *baseProperties;//属性列表
}

在class_ro_t中,方法列表、属性列表和协议列表都是一维数组,分别是method_list_t、property_list_t和protocol_list_t。

class_ro_t结构图如下:

class_rw_t和class_ro_t的区别

根据上文中class_rw_t和class_ro_t的结构,我们可以得到class_rw_t的完整结构图如下

  • 首先,在class_rw_t中的二维数组methods、properties、protocols是可读可写的,它包含了类的初始内容,分类的内容。
  • class_ro_t中的一维数组baseMethodList、baseProtocols、ivars、baseProperties是只读的,它包含了类初始的内容,并且在编译完成之后就决定了,在运行时是无法进行修改的。
  • class_rw_t中的二维数组,包含了class_ro_t中一维数组的内容,以methons为例,methods作为二维数组,内部存放了很多的methon_list_t,而在methon_list_t中,则存放了具体的方法信息methon_t。但是不管methods中有多少methon_list_t,它的最后一个元素永远保存的是class_ro_t中的baseMethodList,这一点会通过阅读源码来进行验证。这也是为什么class_rw_t可读可写的原因。而且在methods中,同样也保存了所有的Category所包含的方法。每一个Category都对应一个methon_list_t,而且Category的方法列表存放在数组的最前面。这一点在Objective-C基础之三(深入理解Category)中也有详细说明。

源码解析

上文提到,在类初始化的时候其实class中保存的是class_ro_t而不是class_rw_t,这一点可以通过objc-runtime-new.mm中的realizeClassWithoutSwift函数可以看出

static Class realizeClassWithoutSwift(Class cls){
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;
    
    if (!cls) return nil;
    //如果class已经初始化,则直接返回当前class
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));
    //首先通过class的data()函数取到class中bits中存放的class_ro_t
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        //如果当前的cls是future class,并且rw已经被创建,则直接拿到rw和rw中的ro
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        //如果是普通的class,创建rw
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        //将ro赋值给rw中的ro
        rw->ro = ro;
        //设置rw的flags
        rw->flags = RW_REALIZED|RW_REALIZING;
        //将rw设置到cls中的bits中去
        cls->setData(rw);
    }
    
    ......
    
    //递归初始化父类
    supercls = realizeClassWithoutSwift(remapClass(cls->superclass));
    //递归初始化元类,通过isa指针来获取到cls的元类
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()));
    
    ......
    
    //修改rw中的方法列表,属性列表和协议列表,并且将分类中的方法列表,属性列表和协议列表附加到rw中去
    methodizeClass(cls);
}

在类初始化时,cls通过data()函数获取到的其实是class_ro_t,内部存放了类初始的方法列表、属性列表和协议列表。如果当前的cls是普通的class,则通过calloc函数创建rw(class_rw_t),然后将rw中的ro指针指向原始的ro(class_ro_t),之后重置rw中的flags,并且将rw的内存地址保存到cls中的bits中去。并且,realizeClassWithoutSwift中首先是通过递归来初始化当前父类以及元类。最后才初始化当前类的。

创建完rw(class_rw_t)之后,则会重新整理cls中的方法列表、属性列表和协议列表。具体methodizeClass函数源码如下:

static void methodizeClass(Class cls){
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;
    // 从ro中拿到baseMethodList
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        //将baseMethodList附加到rw的methods中去
        rw->methods.attachLists(&list, 1);
    }
    // 从ro中拿到baseProperties
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        //将baseProperties附加到rw的properties中去
        rw->properties.attachLists(&proplist, 1);
    }
    //从ro中拿到baseProtocols
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        //将baseProtocols附加到rw的protocols中去
        rw->protocols.attachLists(&protolist, 1);
    }

    //最后将所有Category的方法列表、属性列表和协议列表附加到cls
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
    
}

methodizeClass函数中首先会拿到ro中的方法列表,属性列表和协议列表,然后将拿到的方法列表,属性列表和协议列表通过对应的attachLists函数附加到rw中的二维数组中去。

void attachLists(List* const * addedLists, uint32_t addedCount) {
    if (addedCount == 0) return;
    //这里以方法列表为例
    //array()->lists表示原来类中的方法列表
    //addedLists表示所有Category中的方法列表
    if (hasArray()) {
        //获取原来类中方法列表的长度
        uint32_t oldCount = array()->count;
        //得到方法合并之后的新的数组长度
        uint32_t newCount = oldCount + addedCount;
        //给array重新分配长度为newCount的内存空间
        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
        array()->count = newCount;
        //将原来array()->lists中的数据移动到数组中oldCount的位置
        //也就是相当于将array()->lists的数据在内存中往后移动了addedCount个位置
        memmove(array()->lists + addedCount, array()->lists,
                oldCount * sizeof(array()->lists[0]));
        //将Category中的方法列表copy到array()->lists中
        //并且是从数组的起始地址开始存放
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }
    else if (!list  &&  addedCount == 1) {
        // 0 lists -> 1 list
        list = addedLists[0];
    }
    else {
        // 1 list -> many lists
        List* oldList = list;
        uint32_t oldCount = oldList ? 1 : 0;
        uint32_t newCount = oldCount + addedCount;
        setArray((array_t *)malloc(array_t::byteSize(newCount)));
        array()->count = newCount;
        if (oldList) array()->lists[addedCount] = oldList;
        memcpy(array()->lists, addedLists,
               addedCount * sizeof(array()->lists[0]));
    }

安装完类本身实现的方法、属性和协议之后,会继续通过attachCategories函数拿到class的所有Category中的方法、属性和协议列表,然后调用attachLists函数附加到rw中的二维数组中去

//将方法列表、属性列表、协议列表附加到类中去
//假设cats中的所有的类别都是按顺序进行加载和排序的,最早装载进内存的类别是第一个
static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);
    //用来判断是否是元类
    bool isMeta = cls->isMetaClass();

    //申请连续内存空间,创建一个二维数组,里面存放着所有的method_list_t
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    //申请连续内存空间,创建一个二维数组,里面存放着所有的property_list_t
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    //申请连续内存空间,创建一个二维数组,里面存放着所有的protocol_list_t
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    //获取到category_list之后,通过逆序遍历来取出Category内部的方法、属性和协议列表
    while (i--) {
        auto& entry = cats->list[i];
        //遍历cls所有的category_t,将category_t中的method_list_t取出,存放到二维数组mlists中
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }
        // 将category_t中的property_list_t取出,存放到二维数组proplists中
        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }
        //将category_t中的protocol_list_t取出,存放到二维数组protolists中
        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }
    //拿到类对象cls的class_rw_t类型的成员data,它是可读可写的
    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    //将方法列表合并到rw的方法列表中去,并且插入到表头位置
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);
    //将属性列表合并到rw的属性列表中去,并且插入到表头位置
    rw->properties.attachLists(proplists, propcount);
    free(proplists);
    //将协议列表合并到rw的协议列表中去,并且插入到表头位置
    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}

具体的Category的附加操作在Objective-C基础之三(深入理解Category)中有详细的说明。

因为是先附加的类本身实现的方法、属性和协议,之后才附加的Category的方法、属性和协议,并且attachLists操作从数组的头部开始进行附加,所以先执行附加操作的方法、属性和协议会放在数组的后面,因此上文中类本身实现的方法、属性和协议肯定存放在rw二维数组中的最后一个元素。

objc_class中方法缓存cache的作用

method_t

在了解方法缓存作用之前,先要了解方法底层是如何进行存储的。OC中方法都是以method_t的形式存储

//IMP其实就是函数的具体实现
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
using MethodListIMP = IMP;

struct method_t {
    SEL name;           //方法名称
    const char *types;  //方法的返回值和参数类型
    MethodListIMP imp;  //函数地址(指向函数的指针)
};

SEL

SEL代表着方法的名称,也叫作方法选择器,和c语言的char *结构类似,具体的定义如下

typedef struct objc_selector *SEL;

可以通过@selector()函数和sel_registerName()函数获取到对应的SEL

SEL res = sel_registerName("test:");
SEL res1 = @selector(test:);

可以通过sel_getName()函数和NSStringFromSelector()方法来讲SEL转换成对应的字符串

SEL res = sel_registerName("test:");
SEL res1 = @selector(test:);
NSLog(@"%s  %@", sel_getName(res), NSStringFromSelector(res));

其实不同类中如果定义了相同的方法,那么通过@selector()函数和sel_registerName()获取到的方法选择器是同一个,在内存中只存在一份。

SEL res = sel_registerName("test:");
SEL res1 = @selector(test:);
//获取方法选择器的内存地址
NSLog(@"%p   %p", res, res1);

type Encoding

types表示方法的返回值类型和参数类型,也是一个char *类型的字符串,这里创建XLStudent,在XLStudent创建test方法如下:

@interface XLStudent : NSObject

- (int)test:(int)age height:(int)height;

@end

在OC中,每个方法其实都有两个默认的参数,id类型的self和SEL类型的_cmd,所以test方法本质上是以下的结构

int test(id self, SEL _cmd, int age, int height);

然后通过runtime的函数可以获取当前test方法的Encoding

XLStudent *student = [[XLStudent alloc] init];
Method method = class_getInstanceMethod([student class], @selector(test:height:));
NSLog(@"%s", method_getTypeEncoding(method));

最后得到对应的types就是i24@0:8i16i20。其中每一位的含义如下

code meaning
i 代表返回值类型为int
24 代表所有参数所占内存大小为24个字节
@ 代表方法的第一个参数是id类型
0 代表第一个参数地址从0开始
: 代表第二个参数是一个方法选择器(SEL)
8 代表第二个参数地址从8开始,占8个字节
i 代表第三个参数是int类型
16 代表第三个参数地址从16开始,占4个字节
i 代表第四个参数是int类型
20 代表第四个参数地址从20开始,占4个字节

在iOS中提供了一个@encode指令来获取具体的类型所对应的字符串编码

NSLog(@"%s", @encode(int));//运行结果为 i
NSLog(@"%s", @encode(char));//运行结果为 c
NSLog(@"%s", @encode(id));//运行结果为 @

完整的Type Encoding列表如下

code meaning
c char
i int
s short
l long
q long long
c unsigned char
I unsigned int
S unsigned short
L unsigned long
Q unsigned long long
f float
d double
B C++ bool or C99 _Bool
v void
* A character string(char *)
@ An object(whether statically typed or typed id)
# class object(Class)
: method selecter(SEL)
[array type] An Array
{name=type...} A structure
{name=type...} A union
bnum A bit field of num bits
^type A pointer to type
? An unknown type

方法缓存

cache_t

上文中提到,在Class内部有个方法缓存cache_t,它的内部结构如下

struct cache_t {
    struct bucket_t *_buckets;  //散列表
    mask_t _mask;               //散列表的长度 - 1
    mask_t _occupied;           //已经缓存的方法数量
public:
    mask_t mask();              //获取当前_mask的值
    mask_t occupied();          //获取_occupied的值
    mask_t capacity();          //获取当前散列表的容量,也就是_mask + 1
    struct bucket_t * find(SEL sel, id receiver);   //以sel为key到散列表中查找对应的bucket_t
}

cache_t内部主要有3个成员变量。

  • _buckets是一个散列表(哈希表),内部存储了多个bucket_t,bucket_t的内部结构如下
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    uintptr_t _imp; //存放了函数的内存地址  (在最新版本的源码中,_imp不是直接存放函数内存地址)
    SEL _sel;       //方法选择器SEL的地址作为key
#else
    SEL _sel;
    uintptr_t _imp;
#endif
}
  • _mask是散列表的长度-1
  • _occupied则是代表已经缓存的方法数量,也就是哈希表中已经存放的方法个数

散列表(哈希表)

上文中所说的散列表其实就是类似一个数组,在散列表中存放着对应的bucket_t,具体的散列表的结构如下图

有了散列表,接下来就是如何计算出索引,然后向指定的位置添加bucket_t,在cache_t中有个find函数,就是用来根据索引查找到指定的bucket_t的,如下

bucket_t * cache_t::find(SEL s, id receiver)
{
    assert(s != 0);
    //拿到整个散列表
    bucket_t *b = buckets();
    //拿到散列表的_mask
    mask_t m = mask();
    //根据SEL计算出开始的索引地址
    mask_t begin = cache_hash(s, m);
    mask_t i = begin;
    do {
        //如果通过begin索引找到的bucket_t中的SEL和参数中的SEL相等,则直接返回bucket_t的地址
        if (b[i].sel() == 0  ||  b[i].sel() == s) {
            return &b[i];
        }
        //如果begin位置的bucket_t不是我们要找的,则将begin+1,继续查找下一个索引地址,直到找到为止
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)s, cls);
}
  • 首先,会调用cache_hash函数,通过sel和_mask进行按位与运算得到初始的索引begin
static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    return (mask_t)(uintptr_t)sel & mask;
}
  • 然后拿到begin位置的bucket_t,通过sel()函数获取到SEL,与参数中的sel进行比较,如果相同,则直接返回bucket_t的内存地址,如果不同,则将begin+1,继续通过按位与算出下一个索引继续进行比较,直到找到对应的bucket_t为止。
static inline mask_t cache_next(mask_t i, mask_t mask) {
    return (i+1) & mask;
}
  • 插入操作其实和查询操作相同,也是先计算出起始的索引地址begin,如果当前索引地址有值,则将begin+1,再次计算出下一个索引地址,然后继续进行判断,直到找到可以插入的位置。散列表会有一个初始的长度,如果整个散列表元素大于散列表总长度的3/4的话,会自动进行扩容操作,扩容为原来的2倍,于此同时会将整个散列表清空,然后修改_mask和_occupied的值。

有一点要注意的是:cache_t中的_mask为什么要存放散列表长度-1?是因为通过SEL & _mask运算得到的值永远会小于等于_mask,也就是说(SEL & _mask) <= _mask。因此只有_mask的值为散列表的长度-1才能保证不会产生数组越界。

方法查找流程

模拟散列表

首先,结合方法缓存,再来梳理一下iOS的方法调用流程

  1. 调用对象方法时,首先通过实例对象的isa指针找到对应的类对象,然后在类对象的散列表cache中根据SEL查找对应的方法,如果找到方法,则执行。如果未找到,则执行第二步。
  2. 在类对象的方法列表中查找方法,如果找到,则执行该方法,然后将该方法保存到当前类对象的方法缓存中,以便下次调用同一方法时能从缓存中调用。如果未找到,则执行第三步。
  3. 通过类对象的superClass指针找到父类对象,然后到父类对象的方法缓存中去查找,如果找到,则执行该方法,并且将该方法存放到当前类对象的方法缓存中去(注意,此处是当前类而不是它的父类对象),如果未找到则执行第四步
  4. 在父类对象的方法列表中去查找方法,如果找到,则执行该方法,然后将该方法保存到当前类对象的方法缓存中(注意,此处是当前类而不是它的父类对象)。如果未找到则重复执行第三步,直到superClass为nil。

具体流程图如下

此处流程图未标明superClass为nil的情况,因为涉及到消息转发,会在后续文章中进行详细说明。

以上就是方法缓存的完整流程,下面我们就通过Demo来验证我们的结论。要想查看Class的内部结构,就需要对我们创建的对象进行强制转换,转换成对应的结构体,需要用到的转换工具类如下。创建XLClass.h,然后将以下代码复制到XLClass.h中去。

//XLClass.h

#ifndef XLClass_h
#define XLClass_h

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif
typedef uintptr_t SEL;

struct xl_class_data_bits_t {
    // Values are the FAST_ flags above.
    uintptr_t bits;
};

struct xl_bucket_t {
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif
};

struct xl_cache_t {
    struct xl_bucket_t *_buckets;
    mask_t _mask;
    mask_t _occupied;
};

/* OC对象 */
struct xl_objc_object {
    void *isa;
};

/* 类对象 */
struct xl_objc_class : mj_objc_object {
    Class superclass;
    xl_cache_t cache;
    xl_class_data_bits_t bits;
};


#endif /* XLClass_h */

上述代码其实是将源码中的部分函数和结构体的定义拿出来,重新封装一下。之后通过强制转换就能够查看对象的内部结构。此处还需要注意将main.m的后缀改成main.mm,以便整个项目支持C++编译。

之后创建XLPerson类,在类中增加如下方法

@interface XLPerson : NSObject

- (void)personMethond1;

@end

@implementation XLPerson

- (void)personMethond1{
    NSLog(@"%s", __func__);
}

@end

然后创建XLPerson类的子类XLTeacher,在XLTeacher中增加以下方法

@interface XLTeacher : XLPerson

- (void)teacherMethond1;
- (void)teacherMethond2;
- (void)teacherMethond3;
- (void)teacherMethond4;
- (void)teacherMethond5;
- (void)teacherMethond6;
- (void)teacherMethond7;
- (void)teacherMethond8;

@end

@implementation XLTeacher

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%s", __func__);
    }
    return self;
}

- (void)teacherMethond1{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond2{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond3{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond4{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond5{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond6{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond7{
    NSLog(@"%s", __func__);
}
- (void)teacherMethond8{
    NSLog(@"%s", __func__);
}

@end

在main函数中创建XLTeacher对象,然后转换成对应的mj_objc_class结构体

int main(int argc, const char * argv[]) {
    @autoreleasepool {
  
        XLTeacher *teacher = [XLTeacher alloc];
        [teacher teacherMethond1];
//        [teacher personMethond1];
//        [teacher teacherMethond2];
//        [teacher teacherMethond3];
//        [teacher teacherMethond1];
//        [teacher teacherMethond1];
//        [teacher teacherMethond4];
//        [teacher teacherMethond5];
//        [teacher teacherMethond6];
//        [teacher teacherMethond7];
//        [teacher teacherMethond8];
        
        NSLog(@"-------------散列表------------");
        //将XLTeacher转换成mj_objc_class
        xl_objc_class *teacherClass = (__bridge xl_objc_class *)[teacher class];
        //获取缓存cache_t
        xl_cache_t cache = teacherClass->cache;
        //拿到缓存中的散列表
        xl_bucket_t *buckets = cache._buckets;
        //打印散列表的内容
        for (int i = 0; i < cache._mask + 1; i++) {
            xl_bucket_t bt = buckets[i];
            NSLog(@"index:%d --- sel:%p --- imp:%lu", i,bt._sel, bt._imp);
        }
        
        NSLog(@"111");
    }
    return 0;
}
  • 首先只执行方法teacherMethond1,散列表的内容如下
2020-01-08 14:28:25.583428+0800 Test[652:4619102] -[XLTeacher teacherMethond1]
2020-01-08 14:28:25.583919+0800 Test[652:4619102] -------------散列表------------
2020-01-08 14:28:25.583989+0800 Test[652:4619102] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:28:25.584024+0800 Test[652:4619102] index:1 --- sel:0x100001e49 --- imp:0
2020-01-08 14:28:25.584049+0800 Test[652:4619102] index:2 --- sel:0x0 --- imp:11928
2020-01-08 14:28:25.584068+0800 Test[652:4619102] index:3 --- sel:0x1 --- imp:4301329584

其中0x100001e49表示@selector(teacherMethond1)选择器的内存地址,通过以下方式打印出@selector(teacherMethond1)的内存地址,发现也是0x100001e49,因此就可以断定teacherMethond1方法被存放到了散列表索引为1的位置。

SEL method1 = @selector(teacherMethond1);
NSLog(@"%p",method1);
  • 修改代码,同时执行teacherMethond1和teacherMethond2,再次查看散列表,发现teacherMethond2方法被缓存到了散列表索引为1的位置,@selector(teacherMethond2)地址为0x100001e59
2020-01-08 14:54:23.713899+0800 Test[1383:4643955] -[XLTeacher teacherMethond1]
2020-01-08 14:54:23.714313+0800 Test[1383:4643955] -[XLTeacher teacherMethond2]
2020-01-08 14:54:23.714427+0800 Test[1383:4643955] -------------散列表------------
2020-01-08 14:54:23.714484+0800 Test[1383:4643955] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:54:23.714525+0800 Test[1383:4643955] index:1 --- sel:0x100001e49 --- imp:11976
2020-01-08 14:54:23.714557+0800 Test[1383:4643955] index:2 --- sel:0x100001e59 --- imp:12024
2020-01-08 14:54:23.714587+0800 Test[1383:4643955] index:3 --- sel:0x1 --- imp:4345415488
  • 继续修改代码,同时执行方法teacherMethond1、teacherMethond2和teacherMethond3,这个时候发现,整个散列表进行了扩容,长度从4扩容到了8,并且散列表中只剩下了teacherMethond3,之前的teacherMethond1和teacherMethond2都被清空了
2020-01-08 14:54:53.400791+0800 Test[1399:4644780] -[XLTeacher teacherMethond1]
2020-01-08 14:54:53.401206+0800 Test[1399:4644780] -[XLTeacher teacherMethond2]
2020-01-08 14:54:53.401433+0800 Test[1399:4644780] -[XLTeacher teacherMethond3]
2020-01-08 14:54:53.401527+0800 Test[1399:4644780] -------------散列表------------
2020-01-08 14:54:53.401579+0800 Test[1399:4644780] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401613+0800 Test[1399:4644780] index:1 --- sel:0x100001e69 --- imp:11952
2020-01-08 14:54:53.401679+0800 Test[1399:4644780] index:2 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401705+0800 Test[1399:4644780] index:3 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401736+0800 Test[1399:4644780] index:4 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401765+0800 Test[1399:4644780] index:5 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401792+0800 Test[1399:4644780] index:6 --- sel:0x0 --- imp:0
2020-01-08 14:54:53.401818+0800 Test[1399:4644780] index:7 --- sel:0x1 --- imp:4301259200
  • 继续执行完teacherMethond1到teacherMethond8等8个方法,发现当执行teacherMethond8时,散列表又进行了一次扩容,长度从8扩容到了16,并且散列表中只剩下了方法teacherMethond8,之前的方法都被清空,因此可以得出结论:当散列表的容量超过3/4时,散列表会进行一次扩容,并且会清空整个散列表。这一点其实在cache_fill_nolock函数中也能找到对应的源码
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    if (!cls->isInitialized()) return;
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 如果散列表中,当前已缓存的方法数量+1小于等于总长度的3/4,则继续使用当前散列表
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // 如果散列表中,当前已缓存的方法数量+1大于总长度的3/4,则对当前散列表进行扩容
        cache->expand();
    }

    //散列表的最小长度为4
    bucket_t *bucket = cache->find(sel, receiver);
    if (bucket->sel() == 0) cache->incrementOccupied();
    bucket->set<Atomic>(sel, imp);
}

散列表的最小长度为4,如果散列表中已缓存的方法数量+1大于散列表长度的3/4,则调用expand函数对散列表进行扩容,容量扩大为原来容量的2倍

void cache_t::expand()
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    //将新的容量扩充为原来容量的2倍
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
    
    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        newCapacity = oldCapacity;
    }
    //重新分配内存
    reallocate(oldCapacity, newCapacity);
}
  • 修改代码,只调用teacherMethond1和父类方法personMethond1,发现父类的方法也在散列表中,由此就证明了之前的结论:如果当子类中没有找到对应方法,会到父类中查找,如果找到,会将父类的方法缓存到子类的cache中去。
2020-01-08 14:53:04.754352+0800 Test[1353:4642077] -[XLTeacher teacherMethond1]
2020-01-08 14:53:04.754734+0800 Test[1353:4642077] -[XLPerson personMethond1]
2020-01-08 14:53:04.754823+0800 Test[1353:4642077] -------------散列表------------
2020-01-08 14:53:04.754885+0800 Test[1353:4642077] index:0 --- sel:0x0 --- imp:0
2020-01-08 14:53:04.754921+0800 Test[1353:4642077] index:1 --- sel:0x100001e49 --- imp:11976
2020-01-08 14:53:04.754951+0800 Test[1353:4642077] index:2 --- sel:0x100001e31 --- imp:11384
2020-01-08 14:53:04.754979+0800 Test[1353:4642077] index:3 --- sel:0x1 --- imp:4301540624
  • bucket_t的最新核心源码如下,_sel是方法选择器的地址,用来进行散列表的索引值的计算,而_imp则存放了方法的具体内存地址,但是直接拿到_imp的值是无法拿到具体的方法地址的,还需要调用trauth_auth_and_resign对_imp指针进行身份验证,并且重新分配它,最终才能得到真实的方法内存地址。这一点和旧版的Api有所区别。
struct bucket_t {
private:
#if __arm64__
    uintptr_t _imp;
    SEL _sel;
#else
    SEL _sel;
    uintptr_t _imp;
#endif

public:
    inline SEL sel() const { return _sel; }

    inline IMP imp() const {
        if (!_imp) return nil;
        return (IMP)
            ptrauth_auth_and_resign((const void *)_imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(_sel),
                                    ptrauth_key_function_pointer, 0);
    }

未解决的问题

  • 在上述Demo中,方法缓存cache_t中有一个成员_occupied,从源码上看,_occupied存放的是散列表中已缓存方法的数量。但是在Demo中,_occupied的值打印出来一直和散列表已缓存方法数量不匹配。
  • 在散列表中,最后一个索引的元素一直存放的是当前bucket_t *_buckets的内存地址,至于为什么这样做还有待考证。

以上两个问题还没有找到具体的解释,如果有知道的同学,欢迎不吝赐教。

结束语

以上内容纯属个人理解,如果有什么不对的地方欢迎留言指正。

一起学习,一起进步~~~