OC 底层原理(7)- 类原理(类的属性存储,类的方法存储)(随记)

629 阅读9分钟

一、类的属性存储

这部我们将分析类的属性存储,那么就先添加两个属性

@interface LGPerson : NSObject {
    NSString *hobby;
}
@property (nonatomic, strong) NSString *name;
@end

定义好之后在运行是,打印一下类的内存结构

经过上篇的分析,我们知道了,倒数第一和第二段分别为 cache 和 bits,根据字面翻译cache 为缓存(缓存了些先不去想),bits 为数据,为了方便查看再把 class 类的结构前4个成员定义源码贴出来

现在我们需要找到hobby 和 name 存储的位置,差不多可以猜测这两个属性最有可能存储的位置在bits内,但目前看来 bits全是0 没法看,所以在这过程中就需要用到指针偏移来获取我们想要的数据。bits 的地址为:isa(8字节) + superclass(8字节) + cache_t(因为cache_t不是结构体指针,所以所占空间为结构体里面成员实际所占内存的总和) 这三个成员变量所占空间的总和就是bits的起始位置。

isa 和 superclass 所占空间都知道了,现在不确定的 cache_t,现将这个结构体先贴出来

cache_t 所占空间就是这三个成员实际所占空间的总和,也就是:_buckets(8字节) + _mask(4字节) + _occupied(4 字节) = 16字节。因为_mask 和 _occupied 都是 mask_t 类型,在定义里 mask_t 属于 uint32_t 类型,uint32_t 占 4 字节。

typedef uint32_t mask_t; 

所以 bits 所在位置就是 isa(8字节) + superclass(8字节) + cache_t(16字节)= 32字节,所以如果要得到 bits 首地址,就要偏移32个字节,因为地址都是16进制的,需要需要32 转为16进制就是 0x20,上面有打印有显示指向 isa 的首地址为 0x1000014b8 ,加上 0x20 的话就是 0x1000014d8 .这里不知道怎么来的就用计算器16进制进行相加一下。然后我们开始读取 bits 内的数据。

因为 bit 为 class_data_bits_t 类型,读取的时候强转一下,再有我们是要从地址里读取到bits的信息所以,强转的时候就需要带一个 * ,最后就是 (class_data_bits_t *),见下图

现在我们得到了bits 的指针,然后我们要去得到指针对应的值,也就是一个 class_rw_t 见下图
就可以通过data()来获取bits里面的data,见下图
然后看到是一个 (class_rw_t *) 类型的数据,此时我们再看一下 class_rw_t 结构里面的结构

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif

    void setFlags(uint32_t set) 
    {
        OSAtomicOr32Barrier(set, &flags);
    }

    void clearFlags(uint32_t clear) 
    {
        OSAtomicXor32Barrier(clear, &flags);
    }

    // set and clear must not overlap
    void changeFlags(uint32_t set, uint32_t clear) 
    {
        assert((set & clear) == 0);

        uint32_t oldf, newf;
        do {
            oldf = flags;
            newf = (oldf | set) & ~clear;
        } while (!OSAtomicCompareAndSwap32Barrier(oldf, newf, (volatile int32_t *)&flags));
    }
};

查看源码代码发现方法,协议,属性都存在这里面,那就将刚才的 (class_rw_t *) 类型的数据打印出来,看看目前为止到底存了些什么东西

从上面打印的结果可以看到方法列表,属性列表和协议方法列表,然后我们就继续找我们定义的属性 hobby 和 name
读取了属性列表之后就出现一个二维数组的东西,再往里面看,有个list,然后就继续打印这个 list 看看里面是啥
看到是一个 property_list_t 指针类型的数据,然后全局搜索一下它定义的地方。

有继承 entsize_list_tt 这个东西,然后再全局搜一下这个结构体,看看能不能找到,打印出刚才list里面的数据

终于找到一个跟元素相关的了,那就来试一下
终于找到一个跟我们定义的属性名一样的名称 ‘name’,然后我们在读成员变量

这里可能会有疑问,为什么我要用 get 去拿取数据呢,因为这个git 方法也是 entsize_list_tt 结构体里面的函数,刚才只是我没有将 entsize_list_tt 结构体里面的东西全部展示出来。回过来继续说属性的事,正如上图所显示的,我并有在找到任何 hobby的影子了,那么就有可能成员变量hobby并没有存在 (class_rw_t) 的 properties 里,那我们继续回答打印bits.data()的地方

既然 properties 里面没有,然后又注意到还有个ro,那这个ro 里装的又是什么呢?继续打印一下
打印了ro 发现这里存的东西还不少呢,并且经历这里面也有提到 方法列表,协议列表,还有属性。然后我们就打印一下属性列表

在竟然这里也发现一个属性名为 ‘name’ 的属性,这是巧合还是必然呢,那我就将 ‘name’ 这个属性就换一个名字,再走一遍这个过程
可以看到最后的结果,我们读到了我们修改属性名之后的名称 nickName,但还有问题是,还有成员变量 hobby 还没找到,那存在哪呢?我们再继续在找到属性的nickName 的 ro 中找找发现里面有个 ivars ,那再继续打印一下这个试试

果然在 ivars 里面找到了成员变量 hobby 。

总结:类的属性并没在我们以为的 method 里,而是在 ro 里,至于为什么,先一步步往后看。

二、类的方法存储

第一部分的内容里有分析到类的属性存储在 baseProperties 里,再仔细一看然后在 ro 里有看到类似属性列表的字段 baseMethodList ,于是就把 baseMethodList 读出来,看是否是方法存储的地方

可以看到方法是由name,type,imp 三部分组成,我们可以下这个结构的定义 method_t

struct method_t {
    SEL name;
    const char *types;
    MethodListIMP imp;

    struct SortBySELAddress :
        public std::binary_function<const method_t&,
                                    const method_t&, bool>
    {
        bool operator() (const method_t& lhs,
                         const method_t& rhs)
        { return lhs.name < rhs.name; }
    };
};

再回过来看刚才打印的方法列表里面的内容,里面有个字段 count = 3,方法列表是一个列表,说明count在这里代表的是方法的个数为3个呢?

实际上这个 LGPerson 里并没有定义任何方法,但是又一个属性nickName,属性在底层编译会自动生 get 和 set 方法,所以就是 3 个,刚才我们打印出来了一个C++默认添加的方法,接下来打印另外两个方法

看到没有,就这样打印出来 set / get 方法。

打印到这里方法列表都是系统自动添加的方法,那如果是我们自己写的方法,会在这里面吗?验证一下

先添加两个方法,然后在按照上面的步骤走一次

如图所示,类的实例方法 goodMorning 方法在这里找到了,然后注意到这里的count = 4,按照之前的理解,就是methodList 的个数,然后将4个方法都打印出来没有看到另个类的类方法 goodAfternoon 去哪了呢,有点茫然不知在哪了,这里就要试着用另一种方法来查找了---- 通过 API 来获取,就分别在类和元类里通过 sel 在实例化方法里查找看是否能找到

1)在类和元类的获取实例化方法里查找方法的代码

void findInstanceMethod_classtoMetaClass(Class pClass) {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getInstanceMethod(pClass, @selector(goodMorning));
    Method method2 = class_getInstanceMethod(metaClass, @selector(goodMorning));
    
    Method method3 = class_getInstanceMethod(pClass, @selector(goodAfternoon));
    Method method4 = class_getInstanceMethod(metaClass, @selector(goodAfternoon));

    NSLog(@"%p -- %p -- %p -- %p", method1, method2, method3, method4);
    NSLog(@"%s\n", __func__);
}

2)在类和元类的获取类方法里查找方法的代码

void findClassMethod_classToMetaClass(Class pClass) {
    const char *className = class_getName(pClass);
    Class metaClass = objc_getMetaClass(className);
    
    Method method1 = class_getClassMethod(pClass, @selector(goodMorning));
    Method method2 = class_getClassMethod(metaClass, @selector(goodMorning));
    
    Method method3 = class_getClassMethod(pClass, @selector(goodAfternoon));
    Method method4 = class_getClassMethod(metaClass, @selector(goodAfternoon));

    NSLog(@"%p -- %p -- %p -- %p", method1, method2, method3, method4);
    NSLog(@"%s\n", __func__);
}

然后在main函数里调用并查看结果

通过打印结果可以看到:

第一个打印结果:在类的获取实例化方法里面找到了实例化方法,在元类的获取实例化方法里面找到类方法。也就是实例化方法是类的实例化方法,类方法是元类的实例化方法

第二个打印结果:在类里和元类的获取类方法里都找到类方法,这里在获取类方法里面就找不到实例化方法了。但是结果就有点感觉怪怪的了,为什么在元类的元类里还能找到类方法呢。我们先看看这个获取类方法的接口实现源码

这个方法主要实现的是通过传进来的类查找它的元类,再通过查找到的元类获取元类的实例化方法。然后在看一下获取元类的方法

这个方法主要实现的是,如果已经是元类就返回当前元类自己,要是不是元类,在通过ISA进行查找元类返回

所以上面的第二个打印结果其实并不是真的在元类的元类也就是根元类里找到的,而是在元类里找到的。返回的还是在元类里的实例方法地址。

接下来我们就通过打印来验证一下,在元类找类方法

嗯,果然在元类里确实找到了。

总结: 1)成员变量存储在ivar; 2)属性存储在property 和 ivar; 3)对象方法存储在类里面; 3)类方法存储在元类里面。

拓展: 1)获取类里面属性和成员变量列表

void testObjc_copyIvar_copyProperies(Class pClass) {
    unsigned int count = 0;
    Ivar *ivars = class_copyIvarList(pClass, &count);
    for (unsigned int i = 0; i < count; i++) {
        Ivar const ivar = ivars[i];
        //获取实例变量名称
        const char *cName = ivar_getName(ivar);
        NSString *ivarName = [NSString stringWithUTF8String:cName];
        NSLog(@"class_copyIvarList:%@",ivarName);
    }
    free(ivars);
    
    unsigned int pCount = 0;
    objc_property_t *properties = class_copyPropertyList(pClass, &pCount);
    for (unsigned int i = 0; i < pCount; ++i) {
        objc_property_t const property = properties[i];
        //获取属性名
        NSString *propertyName = [NSString stringWithUTF8String:property_getName(property)];
        //获取属性值
        NSLog(@"class_copyProperiesList:%@",propertyName);
    }
    free(properties);
}

验证:

2)获取方法列表

void findObjc_copyMethodList(Class pClass) {
    unsigned int count = 0;
    Method *methods = class_copyMethodList(pClass, &count);
    for (unsigned int i = 0; i < count; i++) {
        Method const method = methods[i];
        //获取方法名
        NSString *methodName = [NSString stringWithUTF8String:method_getName(method)];
        NSLog(@"findObjc_copyMethList:%@", methodName);
    }
}

验证: