类的底层原理(上)

587 阅读8分钟

上篇文章中我们了解了alloc创建对象的过程,我们知道了对象里面含有一个叫做isa的指针,并且通过isa指针,从对象找到了他所属的类,今天我们就来探索一下关于类的秘密。

首先和NSObject一样,我们先看一下Class到底是什么,在源码的objc.h中我们找到了typedef struct objc_class *Class;,这说明Class实际上是objc_class,在objc-rumtime-new.h中我们找到了他的定义struct objc_class : objc_object,他同样也继承了objc_object,所有我们说类也是一个对象,就是我们常说的类对象。

我们可以通过不同的方式,多创建几个类,并且打印出他的地址,其结果如下

Class class1 = [WTPerson class];
Class class2 = [WTPerson alloc].class;
Class class3 = object_getClass([WTPerson alloc]);
NSLog(@"\n%p -- %p -- %p", class1, class2, class3);
0x100008960 -- 0x100008960 -- 0x100008960

我们发现所有的类打印输出的结果都是0x100008960这个地址,这就说明同一个类,在内存中只有一个地址。 那么0x100008960这个地址就是类对象的isa指针,这个isa指针又是指向哪里的呢?我们使用x/4gx来打印一下class1这个类内存地址看一下: l1.png 我们发现类对象的isa指向的内存地址使用ISA_MASK获取后同样获取到的是WTPerson,我们知道class1也是WTPerson,可是两个的地址空间是不一样的,class1的是0x00008960,isa指针指向的是0x00008938,这我们猜测类对象的isa指针指向的是类对象的元类。那元类的isa指针指向哪里呢,我们继续x/4gx: l2.png 我们发现元类的isa指针指向的是NSObject,我们打印NSObject.class,发现两个地址也是不一样的,所以我们猜测元类的isa指针指向的是NSObject的元类,同时我们打印NSObject的元类,发现他的isa指针地址和本身是一样的,即NSObject的元类的isa指针指向自己了,所以NSObject的元类也就是常说的根元类NSObject也是类对象,他的isa指针指向的就是类对象的元类,也就是指向根元类

在之前的探索中,我们知道,类在底层中实际上是objc_class这个结构,那么我们就来看看这个结构的组成

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;
    Class superclass; //指向父类的指针
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags 类存储信息的地方
    ……其他的方法……
 }
复制代码

isa指针在上面我们已经进行过探索了,我们接下来探索一下superclass指针,我们添加一个子类来,来看一下指向父类的指针,同时也探索一下元类使用是否也是相同的继承关系。

l3.png 在上图中,我们可以了解到student的superclass关系是:
student superclass -> person superclass -> NSObject superclass -> nil 元类的指针是:student元类 isa -> NSObject元类,也就是说所有的元类都是指向的根元类,元类的isa指针没有继承关系。 l5.png 那元类的父类是否有继承关系呢?我们重新梳理一下代码:

    // NSObject实例对象
    NSObject *object1 = [NSObject alloc];
    // NSObject 类对象
    Class obj_class = object_getClass(object1);
    // NSObject 元类对象
    Class meta_class = object_getClass(obj_class);
    NSLog(@"\nNSObject实例对象 - %p \n",object1);
    NSLog(@"NSObject类对象 - %p \n",obj_class);
    NSLog(@"NSObject元类对象 - %p \n",meta_class);

    // WTPerson
    Class p_meta_class = objc_getMetaClass("WTPerson");
    Class p_super_class = class_getSuperclass(p_meta_class);
    NSLog(@"\nWTPerson元类:%@ - %p\n",p_meta_class, p_meta_class);
    NSLog(@"WTPerson元类的父类:%@ - %p\n",p_super_class, p_super_class);

    // WTStudent
    Class st_meta_class = objc_getMetaClass("WTStudent");
    Class st_super_class = class_getSuperclass(st_meta_class);
    NSLog(@"\nWTStudent元类:%@ - %p\n",st_meta_class, st_meta_class);
    NSLog(@"WTStudent元类的父类:%@ - %p\n",st_super_class, st_super_class);
    
    // NSObject 的父类
    Class objc_super_class = class_getSuperclass(NSObject.class);
    NSLog(@"NSObject的父类:%@ - %p\n",objc_super_class, objc_super_class);

    // 根元类 的父类
    Class root_super_class = class_getSuperclass(meta_class);
    NSLog(@"根元类的父类:%@ - %p\n",root_super_class, root_super_class);
    
    打印结果如下:
    NSObject实例对象 - 0x10070dca0 
    NSObject类对象 - 0x10036e140 
    NSObject元类对象 - 0x10036e0f0 

    WTPerson元类:WTPerson - 0x100008938
    WTPerson元类的父类:NSObject - 0x10036e0f0

    WTStudent元类:WTStudent - 0x100008848
    WTStudent元类的父类:WTPerson - 0x100008938
    
    NSObject的父类:(null) - 0x0
    根元类的父类:NSObject - 0x10036e140

由此可知,元类的父类就是父类的元类,根元类的父类就是NSObject,NSObject的父类是nil。 l4.png 由此superclass属性我们了解了,接下来我们来瞧一瞧class_data_bits_t bits这个类存储信息的地方, 首先查看其内存结构:

struct class_data_bits_t { 
    friend objc_class;
    ......省略部分......
    class_rw_t* data() const { return (class_rw_t *)(bits & FAST_DATA_MASK); } 
    ......省略部分......
}

friend关键字是c++中的友元函数,其可以把一些函数(包括全局函数和其他类的成员函数)声明为“友元”,这样那些函数就成为该类的友元函数,在友元函数内部就可以访问该类对象的私有成员了。大概了解下面我们主要研究最主要的方法class_rw_t* data(),其返回的是我们比较熟悉的class_rw_t*,继续看其内存结构:

struct class_rw_t {
    ......省略部分......
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods()};
        }
    }
    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }
    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }
};

在这里可以看到methodspropertiesprotocols,这说明对象的方法、属性和协议我们都可以通过这里去获取到,method_array_t、property_array_t、protocol_array_t都是继承list_array_tt。 接下来我们开始获取class_data_bits_t bits,我们能够获取到类的地址,也就是类的首地址,接下来就是确定class_data_bits_t相对于首地址需要偏移多少才能找到他,我们再来看下类的四个属性 isa 8字节,superclass8字节,cache_t cache占有多少呢?

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {                                          // 联合体取最大字节和内部字节的整数倍,所以8字节
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags; //2
#endif
            uint16_t                   _occupied; //2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; //8
    };

所以cache_t cache占有16字节,所以最终我们需要偏移8 + 8 + 16 = 32字节的位置,就能找到我们的class_data_bits_t

@interface WTPerson : NSObject
{
    @public
    short sex;
    NSString *nickName;
}
@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
+ (void)classMethod;
- (void)testMethod;
@end

l6.png 上图我们根据地址偏移32位获取到了bits,然后调用bits->data()一步步获取到了person存储的方法列表,我们可以看到person中有5个方法。我们再来看看内部是否真正存的是我们设置的方法吗?这里我们发现$7中返回的是entsize_list_tt,entsize_list_tt其实是c++的模版,在源码中查找到该结构体我们查到其中有Element& get(uint32_t i)方法,说明我们可以使用这个方法获取我们的methods。我们继续打印,确发现虽然确实得到了可是打印出来的不是我们想要的,不过打印出来了method_t,那我们就继续查看method_t的内部结构,我们都清楚method中存在的重要变量就是SELIMP,查询源码时我们发现struct big中存在这两个变量,我们可以继续欢乐的打印了: l7.png 贫穷的我使用的是inter的电脑,M1电脑查询过程会有一些小差异,inter使用的是大端存储,M1使用的是小端存储,所以M1电脑无法使用big()取值,而是应该用small()
大端:高位字节存放内存的低地址段,低位字节存放内存的高地址段
小端:高位字节存放内存的高地址段,低位字节存放内存的低地址段
如:0x01050814 ,两位16进制数据代表一个字节,需要4个字节进行存储 1111 代表16进制的15
大端:0x100001存储0x01 0x10002存储0x05 0x10003存储0x08 0x10004存储0x14
小端:0x100001存储0x14 0x10002存储0x08 0x10003存储0x05 0x10004存储0x01
两着的存储方式正好相反。另一种不用big,small的通用打印方法是method_t中的getDescription()l8.png 同样,我们也可以把所有属性都打印出来: l9.png 这里我们也了解到了,属性列表中没有包含成员变量nickName,方法列表中也没有类方法+ (void)classMethod;

总结

综上所述我们可以得到:
实例对象 isa -> 类对象 isa -> 元类对象 isa -> 根元类 isa -> 根元类本身
元类对象的isa指针不会收到继承影响,使用指向根元类,而元类的父类就是父类的元类,根元类的父类就是NSObject,NSObject的父类是nil。
我们可以拿到类的地址进行偏移32位,获取到类存储信息的地方,然后一步步得到我们想要的属性和对象方法。