ios 底层原理 03--isa 的走位图&类的结构探索

720 阅读7分钟

类其实无非就是研究isa的走位和类的继承关系这两个,先从isa走位开始进入正题。

基础知识(类,元类,根)

我们创建的类基本上都继承自NSObject,而NSObject里面有一个默认参数isa;也就是说,继承自NSObject的类里面都会有一个isa

Meta Class(元类)

在我们平时开发中会用到类方法实例方法,但是在底层的实现中并没有这种区分,实际上都是通过实例方法去查找的,底层为了区分这两种方法,引入元类的概念,然后将实例方法存储在中,类方法存储在Meta Class(元类)中。isa指向元类
1、Meta Class(元类)就是类对象所属的类。一个对象所属的类叫做类对象,而一个类对象所属的类就叫做元类
2、Meta Class(元类)的定义和创建,都是由编译器自动完成。
3、所有的类方法,都存储在Meta Class(元类)中。

根类

根类:在OC中几乎所有的类都继承自NSObject类,NSObject类就是根类,根类的父类为nil

根元类

根元类:即为根类NSObject的isa指向的类

通过 NSobject 跟自定义的 LGPerson 类来查看 isa 的走位图

分析NSobject的isa 走位图

创建一个对象NSObject * obj = [NSObject alloc],通过 lldb 调试一下 看图得出结论:
1.NSObject的对象 object1isa->NSObject
2.NSObjectisa->根元类
3.根元类isa ->根元类自身

分析自定义的 LGPerson 的 isa 走位图

创建一个对象LGPerson *person = [LGPerson alloc];,通过 lldb 调试一下

截图中未显示全的代码如下
(lldb) x/4gx  0x0000000100008338
0x100008338: 0x00007fff80768fe0 0x00007fff80768fe0
0x100008348: 0x00000001005a09e0 0x0002e03500000007
(lldb) p/x 0x00007fff80768fe0 & 0x00007ffffffffff8ULL
(unsigned long long) $8 = 0x00007fff80768fe0
(lldb) po $8
NSObject

(lldb) p/x NSObject.class
(Class) $9 = 0x00007fff80769008 NSObject
(lldb) 

元类isa指向根元类,疑问:为什么不是指向NSbOject?注意NSbOject类地址和根元类类地址不一样,所以指向的是根元类
LGPerson的对象person的isa --> LGPerson
LGPerson类的isa--> LGPerson类的元类
元类isa--> 根元类

总结

对象的isa指向类对象
类对象isa指向元类
元类isa指向根元类
根元类isa指向根元类(指向自己)

类,元类,根元类的继承关系

已知LGTeacher 继承LGPerson,LGPerson 继承 NSObject

 // LGTeacher -> LGPerson -> NSObject
    // 元类也有一条继承链
    Class tMetaClass = object_getClass(LGTeacher.class);//LGTeacher的元类
    Class tsuperClass = class_getSuperclass(tMetaClass);//LGTeacher的元类的父类
    NSLog(@"teachertMetaClass是%@地址是 - %p tsuperClass是%@ -地址是 %p",tMetaClass,tMetaClass,tsuperClass,tsuperClass);
    // LGPerson元类
    Class pMetaClass = object_getClass(LGPerson.class);//LGPerson的元类
    Class psuperClass = class_getSuperclass(pMetaClass);//LGPerson的元类的父类
    NSLog(@"personpMetaClass是%@地址是 - %p psuperClass是%@--地址是%p",pMetaClass,pMetaClass,psuperClass,psuperClass);
    Class nMetaClass = object_getClass(NSObject.class);//NSObject的元类
    Class nMetaSuperClass = class_getSuperclass(nMetaClass);//NSObject的元类的父类
    NSLog(@"NSobjectMetaClass是%@地址是 - %p psuperClass是%@--地址是%p",nMetaClass,nMetaClass,nMetaSuperClass,nMetaSuperClass);

源码分析
NSObject的父类打印的结果是nilLGTeacher元类的父类LGPerson元类LGPerson的元类的地址和LGPerson类的地址不一样)。LGPerson元类的父类NSObject元类NSObject元类的父类NSObject(和NSObject类的地址一样)
LgTeacher 继承LgPersonLgPerson 继承 NSObjectNSObject的父类是nil LgTeacher元类 继承 LgPerson元类,LgPerson 继承 根元类根元类继承NSObject

类之间继承流程图

isa的走位图和继承图


苹果官网提供的走位图

补充

1.我们知道LGTeacher继承自类LGPerson,这时候LGTeacher有个对象 t,LGPerson有个对象 p,请问 pt有什么关系?答案是没有任何关系(千万不能说是继承关系)
2.子类继承自父类父类继承自根类根类继承自什么?答案是 nil(根类不是继承与自己)

类的结构分析

通过上篇文章ios 底层原理 02-对象的本质&isa关联类我们知道 isaClass 类型的。Classobjc_class *,objc_class是一个结构体

通过源码查看

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;
//这是用在非 OBJC2 上面的
/* Use `Class` instead of `struct objc_class *` */

现在用到的如图所示。 之前源码分析过 objc_class 继承自 objc_object 在搜索 objc_object的时候,需要注意源码里面有 2 个定义 objc_object的地方,这里我们使用的是objc.h里面的(通过 main.cpp 得出的)还有一个在objc-private.h里面。

objc_classobjc_object的关系

1.结构体 objc_class 继承自 objc_object
2. objc_object有一个isa属性,所以 objc_class 也有一个 isa 属性(PS:在代码里面有个注释 // Class ISA;可以验证)
3.NSObject也有isa。因为 nsobject在底层其实就是个 objc_object

类中信息的探索

我们看到上面的图片里还有其他三个成员变量.既然我们知道类的首地址,通过之前说的首地址+偏移量可以获取里面的成员变量的地址,通过地址来取值。但是偏移量需要知道当前变量之前的所有成员变量的大小。

  • isa结构体指针为 8 字节
  • Class superclass也是结构体指针为 8 字节;
  • cache_t cache 是结构体类型的大小,由结构体内成员变量决定
  • class_data_bits_t bits根据前面三个成员变量大小,可以得到bits的地址。
Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

所以,需要知道cache_t cache的大小

struct cache_t {
private:
   explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //大小为 8
   union {
       struct {
           explicit_atomic<mask_t>    _maybeMask;//大小为 4
#if __LP64__
           uint16_t                   _flags;//大小为 2
#endif
           uint16_t                   _occupied;//大小为 2
       };
       explicit_atomic<preopt_cache_t *> _originalPreoptCache;
   };
。。。。。。。。。。。。。。。省略一些源码

cache_t分析

cache_t是结构体类型,有 2 个成员变量_bucketsAndMaybeMask一个union联合体

  • _bucketsAndMaybeMaskuintptr_t无符号长整形大小为8字节
  • union联合体里面有两个成员变量 _originalPreoptCachestruct结构体,(之前的知识:联合体的内存大小由成员变量中的最大变量类型决定)
  • _originalPreoptCache是结构体指针大小为8字节
  • struct结构体里面有三个参数_maybeMask;//大小为 4_flags;//大小为 2_occupied;//大小为 2 ,所以结构体是 8 字节
  • cache_t的内存大小为 8+8 或者是 8+4+2+2 都是 16 字节

结论

  • isa内存地址为首地址
  • superclass内存地址为首地址+0x8
  • cache 内存地址为首地址+0x10
  • bits内存地址为首地址+0x20

bits分析

如图所示,bits.data()应该存储数据,data()的类型是 class_rw_t.查看 class_rw_t的源码就是上面圈出来的.
可以看出,class_rw_t是结构体类型,里面提供了获取方法列表属性列表协议列表的方法.
下面继续探究属性,方法,成员变量等等

bits.data()分析

类的属性

先看下类里面的属性,我们在代码里面定义了2 个属性

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *hobby;

根据上面分析的,寻找这 2 个属性,流程如图

类的方法

流程如下:NSObject.class->metaClass->class_data_bits_t->class_rw_t->method_arrat_t->method_list_t->method_t->big

  • 类方法存储在元类中的方法列表里面
  • 实例方法
  • 编译器自动生成元类,目的是为了存放类方法

成员变量

流程如下:class_rw_t -> ro() 获取 成员变量列表容器 class_ro_t -> ivars 获取成员变量列表 从成员变量列表中获取成员变量
NSObject.class->metaClass->class_data_bits_t->class_rw_t->class_ro_t->ivar_list_t->ivar_t

协议方法

流程如下:从 class_rw_t -> protocols 获取 协议列表容器 取出 list 取出 ptr 取出 ptr 中的数据 protocol_list_t 这里可以发现,里面的list中存了一个指针,并不是完整的一个数据结构。我们找到了这样的定义: typedef uintptr_t protocol_ref_t; // protocol_t *, but unremapped 获取 protocol_t * 读取数据

实例方法

class_rw_t -> methods 获取 方法列表容器 取出 list 取出 ptr 取出 ptr 中的数据 method_list_t 获取方法数量 通过c++函数get()big()单个获取类的实例方法 这里除了我们自己定义的一些实例方法,还看到了一些列属性自动生成的set、get方法,以及析构函数

遇到的问题