类的结构分析

我们在上一节isa的结构分析分析了isa的结构,我们在创建一个类的时候打印其地址得到的第一个地址就是它的isa地址,我们知道所有的类都有一个isa指向它的本质,那么除了isa,类里面的结构还有什么内容呢?我们这一节就来分析类的结构

实例对象、类、元类分析

在探究类的结构之前我们先来分析一下实例对象,类和元类的关系
首先我们创建一个类如下:

#import <Foundation/Foundation.h>
#import "SYPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...

        SYPerson *person = [[SYPerson alloc]init];
        
        NSLog(@"%p",person);
    }
    return 0;
}

x/4gx: 以16进制形式打印地址内容,读取4个16字节内容
p/x: 打印变量的十六进制格式信
po: 打印变量的 description 方法
通过isa & ISA_MSAK可以查看 isa 指向的类信息
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
...
# elif __x86_64__
#   define ISA_MASK        0x00007ffffffffff8ULL

我们通过打印分析isa走位

isa指向打印

分析:

  • 实例对象person的isa指向了SYPerson
  • SYPerson的isa指向了SYPerson元类
  • 元类SYPerson的isa指向了NSObject
  • NSObject的isa指向了本身
    那么得到的isa走位符合下图
    isa流程图

查看类结构源码

要分析类SYPerson我们知道它继承来自父类NSObject,所以我们通过查看NSObject的源码找到了:

@protocol NSObject

- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;

@property (readonly) Class superclass;

这里我们可以看到NSObject有一个Class类型superclass,再点进去发现typedef struct objc_class *Class; 说明Class是一个名为objc_class的结构体,也就是说类的本质就是一个结构,那么我们想分析类结构实质上就是分析这个结构体,我们可以看到结构体objc_class的源码发现有以下成员:

struct objc_class : objc_object {
    // 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,为什么这里没有呢?那是因为objc_class继承来自objc_object,而objc_object里面已经包含了isa

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

这里也说明所有以objc_object为模板创建的类都有isa

通过地址平移分析objc_class结构

  • 地址平移

什么是地址平移呢?我们可以拿到类的地址后通过类的地址平移拿到里面的所有信息。通过下面例子我们可以了解一下地址平移

        int a[4] = {1,2,3,4};
        int *b = a;
        NSLog(@"%p--%p--%p",&a,&a[0],&a[1]);
        NSLog(@"%p--%p--%p",b,b+1,b+2);
        
        for (int i = 0; i < 4; i++) {
            int value = *(b + i);
            NSLog(@"%d",value);
        }
//打印结果
2020-09-13 15:54:53.066933+0800 KCObjc[61247:1111510] 0x7ffeefbff510--0x7ffeefbff510--0x7ffeefbff514
2020-09-13 15:54:53.067769+0800 KCObjc[61247:1111510] 0x7ffeefbff510--0x7ffeefbff514--0x7ffeefbff518
2020-09-13 15:54:53.067892+0800 KCObjc[61247:1111510] 1
2020-09-13 15:54:53.067976+0800 KCObjc[61247:1111510] 2
2020-09-13 15:54:53.068055+0800 KCObjc[61247:1111510] 3
2020-09-13 15:54:53.068130+0800 KCObjc[61247:1111510] 4

数组a的第一个元素的地址就是数组的地址,通过对数组的地址进行便宜依次得到了数组后续的值。

在知道什么是地址偏移后我们再通过地址偏移来分析objc_class的结构。前面我们知道了结构体中的成员包含了:

  • Class ISA继承自父类,占8字节
  • Class superclass是一个指针,占用8个字节。
  • cache_t cache以前缓存指针和虚函数表,占16字节
  • class_data_bits_t bits结构体
    那么我们类的信息是否存在于这里bits呢?我们通过查看结构体源码发现
    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

查看结构体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 *>()->methods;
        } else {
            return method_array_t{v.get<const class_ro_t *>()->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 *>()->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>()->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 *>()->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
        }
    }

这些代码是否看着很熟悉呢?有methodspropertiesprotocols,我们可以进行验证这是否可以得到们类的信息。给我们的person添加一个方法- (void)helloClass;
前面我们分析类地址偏移可以得到类里面的所有信息,想查看bits的信息我们需要将地址偏移8+8+18=32字节.

(lldb) x/4gx SYPerson.class
0x1000021f0: 0x00000001000021c8 0x0000000100334140
0x100002200: 0x0000000101134640 0x0002801000000003
//0x1000021f0地址偏移32得到0x100002210
(lldb) p (class_data_bits_t *)0x100002210
(class_data_bits_t *) $1 = 0x0000000100002210
//调用data()
(lldb) p $1->data()
(class_rw_t *) $2 = 0x00000001011345e0
//获取data()方法得到的数据class_rw_t类型的$3
(lldb) p *$2
(class_rw_t) $3 = {
  flags = 2148007936
  witness = 1
  ro_or_rw_ext = {
    std::__1::atomic<unsigned long> = 4294975744
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
//调用前面查看到方法如methods()
(lldb) p $3.methods()
(const method_array_t) $5 = {
  list_array_tt<method_t, method_list_t> = {
     = {
      list = 0x0000000100002148
      arrayAndFlag = 4294975816
    }
  }
}
//查看list里面数据
(lldb) p $5.list
(method_list_t *const) $6 = 0x0000000100002148
//获取到$6的内容,得到了我们申明的方法helloClass
(lldb) p *$6
(method_list_t) $7 = {
  entsize_list_tt<method_t, method_list_t, 3> = {
    entsizeAndFlags = 26
    count = 1
    first = {
      name = "helloClass"
      types = 0x0000000100000f9e "v16@0:8"
      imp = 0x0000000100000f10 (KCObjc`-[SYPerson helloClass])
    }
  }
}
//我们添加属性后同理也可获得属性名称

总结

我们首先通过分析isa走位分析了实例对象、类、元类的关系,然后通过查看源码找到了了类的结构体objc_class,明确了类的底层结构就是结构体,进而对结构体里面的成员进行了分析,发现类的属性方法都存储在class_data_bits_t bits的结构体中,通过地址偏移也验证了这个结论。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。