《跟我学》之OC类的结构分析

我们之前的篇幅介绍了对象,也知道对象是一个实例。那么它的结构又是怎么样的。为了更直接的观察。我们做好充足的前戏提前定义好了两个类。
Person继承NSObjectDeveloper继承Person ,代码如下。

准备工作

Person.h文件

@interface Person : NSObject
{
    ///成员变量
     NSString *_variables;
}
///一个属性
@property (nonatomic, strong) NSString *attributes;
///类方法
+ (void)classMethod;
///实例方法
- (void)instanceMethod;
@end

Developer.h文件

@interface Developer : Person
{
    ///成员变量
     NSString *_subVariables;
}
///一个属性
@property (nonatomic, strong) NSString *subAttributes;
///类方法
+ (void)subClassMethod;
///实例方法
- (void)subInstanceMethod;
@end

main.m文件

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //ISA_MASK  0x00007ffffffffff8ULL
        Person *person = [Person alloc];
        Developer *developer = [Developer alloc];
        NSLog(@"person %@", person);
        NSLog(@"developer %@", developer);
    }
    return 0;
}

要用到的lldb指令

指令 作用
p expr - 的缩写。它的工作是把接收到的参数在当前环境下进行编译,然后打印出对应的值。
po expr -o-。它所做的操作与p相同。如果接收到的参数是一个指针,那么它会调用对象的 description 方法并打印。如果接收到的参数是一个 core foundation 对象,那么它会调用 CFShow 方法并打印。如果这两个方法都调用失败,那么 po 打印出和 p 相同的内容。总的来说,po 相对于 p 会打印出更多内容。一般在工作中,用 p 即可,因为 p 操作较少,效率更高。
p/x 16进制读取对象的地址或者值
x/4gx 16进制形式读取48位的内存空间里面存储的值
(lldb) x/4gx person
0x1039b9230: 0x001d80010000231d 0x0000000000000000
0x1039b9240: 0x0000000000000000 0x0000000000000000
(lldb) p/x 0x001d80010000231d & 0x00007ffffffffff8ULL
(unsigned long long) $4 = 0x0000000100002318
(lldb) po 0x0000000100002318
Person

(lldb) x/4gx Person.class
0x100002318: 0x00000001000022f0 0x00007fff91427118
0x100002328: 0x0000000100504630 0x000580240000000f
(lldb) p/x 0x00000001000022f0 & 0x00007ffffffffff8ULL
(unsigned long long) $7 = 0x00000001000022f0
(lldb) po 0x00000001000022f0
Person

(lldb) x/4gx 0x00000001000022f0
0x1000022f0: 0x00007fff914270f0 0x00007fff914270f0
0x100002300: 0x0000000100604270 0x0004e03500000007
(lldb) p/x 0x00007fff914270f0 & 0x00007ffffffffff8ULL
(unsigned long long) $9 = 0x00007fff914270f0
(lldb) po 0x00007fff914270f0
NSObject

(lldb) x/4gx 0x00007fff914270f0
0x7fff914270f0: 0x00007fff914270f0 0x00007fff91427118
0x7fff91427100: 0x00000001039b9880 0x0004e03100000007
(lldb) p/x 0x00007fff914270f0 & 0x00007ffffffffff8ULL
(unsigned long long) $11 = 0x00007fff914270f0
(lldb) po 0x00007fff914270f0
NSObject

我们打个断点通过lldb来观察一下person对象发现它的isa指针指向Person类,Person类的isa指针指向的还是Person类,
这里面有个细节,就是尽管都是Person类,但是他们并不是一个,仔细观察我们发现personisa指针地址为0x0000000100002318,而Person.classisa指针地址为0x00000001000022f0。他们并不是同一个类
我们把后面这个看起来像是它自己的称之为元类

随后元类isa指针指向为NSObject的地址为0x00007fff914270f0。即便我们一直这样观察下去也发现,循环指向0x00007fff914270f0NSObject
再暴力点直接x/4gx NSObject.class发现它的isa也是0x00007fff914270f0

结论

1、对象isa 指向 (也可称为类对象
person -> isa 他所属的类 Person
2、isa 指向 元类
Person类 -> isa 他的Person元类 (虽然看起来是他自己,但真的不是它自己。因为isa地址不一样)
3、元类isa 指向 根元类,即NSObject
Person元类 -> isa = 他的根元类NSObject
4、根元类isa 指向 它自己 也是NSObject
NSObject根源类 -> isa自己 NSObject(这个的内存地址始终唯一

我们来解释一下什么是元类,应该就可以明白刚才所说的看起来是它自己。但是又不是它自己
我们都知道 对象isa 是指向的其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类
1、元类是系统给的,其定义和创建都是由编译器完成,在这个过程中,的归属来自于元类
2、元类类对象,每个都有一个独一无二元类用来存储 类方法的相关信息。
3、元类本身是没有名称的,由于与相关联,所以使用了同类名一样的名称
如果简单的理解话。你可以把它理解成一个副本类。有这一样的名字

类是否唯一?

Class developerClass1 = [Developer class];
Class developerClass2 = [Developer alloc].class;
Class developerClass3 = object_getClass([Developer alloc]);
NSLog(@"developerClass1=%p", developerClass1);
NSLog(@"developerClass2=%p", developerClass2);
NSLog(@"developerClass3=%p", developerClass3);
/// developerClass1=0x100003380 developerClass2=0x100003380 developerClass3=0x100003380

Class personClass1 = [Person class];
Class personClass2 = [Person alloc].class;
Class personClass3 = object_getClass([Person alloc]);
NSLog(@"personClass1=%p", personClass1);
NSLog(@"personClass2=%p", personClass2);
NSLog(@"personClass3=%p", personClass3);
/// personClass1=0x100003330 personClass2=0x100003330 personClass3=0x100003330

Class objectClass1 = [NSObject class];
Class objectClass2 = [NSObject alloc].class;
Class objectClass3 = object_getClass([NSObject alloc]);
NSLog(@"personClass1=%p", objectClass1);
NSLog(@"personClass2=%p", objectClass2);
NSLog(@"personClass3=%p", objectClass3);
/// developerClass1=personClass1 personClass2=0x7fff91427118 personClass3=0x7fff91427118

我们又通过这一坨很无聊的代码得出另外一个结论,任何内存只存在一份

核心isa走位 与 类的继承关系图

这个图第一次我看很懵逼。现在看依然懵逼。
但是。学以致用,把他套入到我们自己的类用。就容易理解很多


手残党画的不好。但是确实套用我们刚才示例代码的进来。清晰了不少。
我们梳理一下这幅图中的俩条线索

isa走位链路

1、实例对象 isa 指向他所属的类
2、类对象isa指向它的元类(其实也是它,但是内存地址不是同一个)
3、元类的isa指向它的根元类
4、根元类的isa指向它,无限循环,形成闭环,所有的根元类就是NSObject

supclass走位链路

我们都知道类与类之间可以存在继承关系
1、子类继承父类
2、父类继承根类,此时的根类是指NSObject
3、根类继承nil,所以万物皆NSObject

元类也有继承关系
1、子元类继承父元类
2、父元类继承根元类
3、根元类继承根类,此时根类NSObject

容易混淆的一个点是。类与类之间是有继承关系,但是实例对象实例对象之间没有继承关系
DeveloperPersonNSObject三个的关系是Developer继承PersonPerson继承NSObject
它们的实例对戏那个为developer, person, object。你不们理解为他们的类是继承关系。所以他们三个实例对象也存在继承关系。这是错误的。

objc_class & objc_object

isa走位我们理清楚了,又来了一个新的问题:为什么 对象都有isa属性呢?
不提到两个结构体类型:objc_class & objc_object
我们之前提及NSObject的底层编译是NSObject_IMPL结构体

struct NSObject_IMPL {
    Class isa;
};
typedef struct objc_class *Class;

objc4源码中搜索objc_class的定义,源码中对其的定义有两个版本

  • 旧版 位于 runtime.h中,已经被废除,代码如下
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
  • 新版 位于objc-runtime-new.h,这个是objc4-781最新优化的。我们就来研究以这个版本为准,由于代码比较多。只展示核心部分代码
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

    class_rw_t *data() const {
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}

从新版的定义中,可以看到 objc_class 结构体类型是继承自 objc_object的.objc_object定义又如下代码

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

我们来探索一下类里面都哪些信息

isa属性:继承自objc_objectisa,占 8字节
superclass 属性:Class类型,Class是由objc_object定义的,是一个指针,占8字节
cache属性:简单从类型class_data_bits_t目前无法得知,而class_data_bits_t是一个结构体类型,结构体的内存大小需要根据内部的属性来确定,而结构体指针才是8字节
bits属性:只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits

计算 cache 类的内存大小

进入cachecache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),代码如下

struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
    explicit_atomic _buckets;
    explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
    explicit_atomic _maskAndBuckets;
    mask_t _mask_unused;

计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是12字节

  • 【情况一】if流程
buckets 类型是struct bucket_t *,是结构体指针类型,占8字节
mask 是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节
  • 【情况二】elseif流程
_maskAndBuckets 是uintptr_t类型,它是一个指针,占8字节
_mask_unused 是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节

_flagsuint16_t类型,uint16_tunsigned short 的别名,占 2个字节
_occupieduint16_t类型,uint16_tunsigned short 的别名,占 2个字节

总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16字节

接下来就是如何获取bits了

要获取bits的中的内容,只需通过类的首地址平移32字节即可
我们刚才发现。其中的data()获取数据,我们先利用lldb再次打印看能否观察出有价值的信息


x/6gx Person.class 打印出类的首地址
p (class_data_bits_t *)0x100002568 打印出平移了32位的bits信息
最后我们打印发现了一个 class_rw_t.我们查看源代码发现

const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get()->methods;
        } else {
            return method_array_t{v.get()->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get()->properties;
        } else {
            return property_array_t{v.get()->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get()->protocols;
        } else {
            return protocol_array_t{v.get()->baseProtocols};
        }
    }

这三个是不是对应的方法属性协议呢?lldb调试日志如下

p $3.methods()打印出方法数组
p $4.list获取元类方法数组的方法列表
p *$5 获取元类方法列表的第一个方法


我们通过 p $6.get(0)p $6.get(1)p $6.get(2)p $6.get(3)依次打印出来instanceMethodcxx_destruct方法和两个属性方法attributessetAttributes

通过上述内容最终得出

类的实例方法存储在类的bits属性中,例如Person类的实例方法instanceMethod 就存储在 Person类的bits属性中

你可能感兴趣的:(《跟我学》之OC类的结构分析)