本文的主要目的是分析 类 & 类的结构,整篇都是围绕一个类
展开的一些探索
类 的分析
类的分析 主要是分析 isa
的走向 以及 继承
关系
准备工作
定义两个类
- 继承自
NSObject
的类CJLPerson
,
@interface CJLPerson : NSObject { NSString *hobby; } @property (nonatomic, copy) NSString *cjl_name; - (void)sayHello; + (void)sayBye; @end @implementation CJLPerson - (void)sayHello {} + (void)sayBye {} @end
- 继承自
CJLPerson
的类CJLTeacher
@interface CJLTeacher : CJLPerson @end @implementation CJLTeacher @end
- 在main中分别用两个定义两个对象:
person & teacher
int main(int argc, const char * argv[]) { @autoreleasepool { //ISA_MASK 0x00007ffffffffff8ULL CJLPerson *person = [CJLPerson alloc]; CJLTeacher *teacher = [CJLTeacher alloc]; NSLog(@"Hello, World! %@ - %@",person,teacher); } return 0; }
元类
首先,我们先通过一个案例的lldb调试先引入元类
- 在main中CJLTeacher部分加一个断点,运行程序
- 开启lldb调试,调试的过程如下图所示
lldb调试过程
根据调试过程,我们产生了一个疑问
:为什么图中的p/x 0x001d8001000022dd & 0x00007ffffffffff8ULL
与 p/x 0x00000001000022b0 & 0x00007ffffffffff8ULL
中的类信息打印出来都是CJLPerson
?
0x001d8001000022dd
是person
对象的isa指针地址
,其&
后得到的结果
是创建person
的类CJLPerson
0x00000001000022b0
是isa中获取的类信息所指的类的isa
的指针地址,即CJLPerson类的类
的isa
指针地址,在Apple中,我们简称CJLPerson类的类
为元类
- 所以,两个打印都是
CJLPerson
的根本原因就是因为元类
导致的
元类的说明
下面来解释什么是元类
,主要有以下几点说明:
- 我们都知道
对象
的isa
是指向类
,类
的其实也是一个对象
,可以称为类对象
,其isa
的位域指向苹果定义的元类
元类
是系统
给的,其定义
和创建
都是由编译器
完成,在这个过程中,类
的归属
来自于元类
元类
是类对象
的类
,每个类
都有一个独一无二的元类
用来存储类方法的相关信息
。元类
本身是没有名称的
,由于与类
相关联
,所以使用了同类名一样的名称
下面通过lldb
命令来探索元类的走向
,也就是isa
的走位
,如下图所示,可以得出一个关系链:对象 --> 类 --> 元类 --> NSobject, NSObject 指向自身
isa走位
总结
从图中可以看出
对象
的isa
指向类
(也可称为类对象
)类
的isa
指向元类
元类
的isa
指向根元类
,即NSObject
根元类
的isa
指向 它自己
NSObject到底有几个?
从图中可以看出,最后的根元类
是NSObject
,这个NSObject
与我们日开开发中所知道的NSObject
是同一个吗?
有以下两种验证方式
- 【方式一】
lldb
命令验证 - 【方式二】
代码
验证
【方式一】lldb命令验证
我们也通过lldb调试,来验证这两个NSObject是否是同一个,如下图所示
从图中可以看出,最后NSObject
类的元类
也是NSObject
,与上面的CJLPerson
中的根元类
(NSObject)的元类
,是同一个,所以可以得出一个结论:内存中只存在存在一份根元类NSObject,根元类的元类是指向它自己
【方式二】代码验证
通过三种不同的方式获取类,看他们打印的地址是否相同
//MARK:--- 分析类对象内存 存在个数 void testClassNum(){ Class class1 = [CJLPerson class]; Class class2 = [CJLPerson alloc].class; Class class3 = object_getClass([CJLPerson alloc]); NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3); }
以下是代码运行的结果
从结果中可以看出,打印的地址都是同一个
,所以NSObject只有一份
,即NSObject(根元类)
在内存中永远只存在一份
[面试题]:类存在几份?
由于类的信息在内存中永远只存在一份,所以 类对象只有一份
著名的 isa走位 & 继承关系 图
根据上面的探索以及各种验证,对象、类、元类、根元类
的关系如下图所示
关系图示
isa走位
isa的走向有以下几点说明:
实例对象(Instance of Subclass)
的isa
指向类(class)
类对象(class)
isa
指向元类(Meta class)
元类(Meta class)
的isa
指向根元类(Root metal class)
根元类(Root metal class)
的isa
指向它自己
本身,形成闭环
,这里的根元类
就是NSObject
superclass走位
superclass(即继承关系)的走向也有以下几点说明:
类
之间 的继承
关系:
类(subClass)
继承自父类(superClass)
父类(superClass)
继承自根类(RootClass)
,此时的根类是指NSObject
根类
继承自nil
,所以根类
即NSObject
可以理解为万物起源
,即无中生有
元类
也存在继承
,元类之间的继承关系如下:
子类的元类(metal SubClass)
继承自父类的元类(metal SuperClass)
父类的元类(metal SuperClass)
继承自根元类(Root metal Class
根元类(Root metal Class)
继承于根类(Root class)
,此时的根类是指NSObject
- 【注意】
实例对象
之间没有继承关系
,类
之间有继承关系
举例说明
以前文提及的的CJLTeacher及对象teacher
、CJLPerson及对象person
举例说明,如下图所示
isa走位 & 继承 举例说明
- isa 走位链(两条)
- teacher的isa走位链:
teacher(子类对象) --> CJLTeacher (子类)--> CJLTeacher(子元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
- person的isa走位图:
person(父类对象) --> CJLPerson (父类)--> CJLPerson(父元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
- superclass走位链(两条)
- 类的继承关系链:
CJLTeacher(子类) --> CJLPerson(父类) --> NSObject(根类)--> nil
- 元类的继承关系链:
CJLTeacher(子元类) --> CJLPerson(父元类) --> NSObject(根元类)--> NSObject(根类)--> nil
objc_class & objc_object
isa走位我们理清楚了,又来了一个新的问题:为什么 对象
和 类
都有isa属性
呢?这里就不得不提到两个结构体类型:objc_class
& objc_object
下面在这两个结构体的基础上,对上述问题进行探索。
在上一篇文章iOS-底层原理 07:isa与类关联的原理中,使用clang
编译过main.m文件,从编译后的c++文件中可以看到如下c++源码
NSObject
的底层编译是NSObject_IMPL
结构体,
- 其中
Class
是isa
指针的类型,是由objc_class
定义的类型, - 而
objc_class
是一个结构体。在iOS中,所有的Class
都是以objc_class
为模板创建的`
struct NSObject_IMPL { Class isa; }; typedef struct objc_class *Class;
在objc4源码中搜索objc_class
的定义,源码中对其的定义有两个版本
旧版
位于runtime.h
中,已经被废除
旧版objc_class定义
- 新版 位于
objc-runtime-new.h
,这个是objc4-781
最新优化的,我们后面的类的结构分析也是基于新版来分析的。
新版objc_class定义
- 从新版的定义中,可以看到
objc_class
结构体类型是继承自objc_object
的,
- 在objc4源码中搜索
objc_object (或者 objc_object {
,这个类型也有两个版本
- 一个位于
objc.h
,没有被废除,从编译的main.cpp
中可以看到,使用的这个版本的objc_object
- 位于
objc-privat.
以下是编译后的main.cpp
中的objc_object
的定义
struct objc_object { Class _Nonnull isa __attribute__((deprecated)); };
【问题】objc_class 与 objc_object 有什么关系?
通过上述的源码查找以及main.cpp中底层编译源码,有以下几点说明:
- 结构体类型
objc_class
继承自objc_object
类型,其中objc_object
也是一个结构体,且有一个isa
属性,所以objc_class
也拥有了isa
属性 - mian.cpp底层编译文件中,
NSObject
中的isa
在底层是由Class
定义的,其中class
的底层编码来自objc_class
类型,所以NSObject
也拥有了isa
属性 NSObject
是一个类,用它初始化一个实例对象objc
,objc 满足objc_object
的特性(即有isa属性),主要是因为isa
是由NSObject
从objc_class
继承过来的,而objc_class
继承自objc_object
,objc_object
有isa
属性。所以对象
都有一个isa
,isa表示指向
,来自于当前的objc_object
objc_object(结构体)
是 当前的根对象
,所有的对象
都有这样一个特性objc_object
,即拥有isa属性
【百度面试题】objc_object 与 对象的关系
- 所有的
对象
都是以objc_object
为模板继承
过来的 - 所有的对象 是 来自
NSObject
(OC) ,但是真正到底层的 是一个objc_object(C/C++)
的结构体类型
【总结】 objc_object
与 对象
的关系
是 继承
关系
总结
- 所有的
对象
+类
+元类
都有isa
属性 - 所有的
对象
都是由objc_object
继承来的 - 简单概括就是
万物皆对象
,万物皆来源于objc_object
,有以下两点结论:
- 所有以
objc_object
为模板 创建的对象
,都有isa
属性 - 所有以
objc_class
为模板,创建的类
,都有isa
属性
- 在结构层面可以通俗的理解为
上层OC
与底层
的对接
:
下层
是通过结构体
定义的模板
,例如objc_class、objc_object
上层
是通过底层的模板创建
的 一些类型,例如CJLPerson
objc_class、objc_object、isa、object、NSObject
等的整体的关系,如下图所示
类结构分析
主要是分析类信息
中存储了哪些内容
补充知识-内存偏移
在分析类结构之前,需要先了解内存偏移,因为类信息中访问时,需要使用内存偏移
【普通指针】
//普通指针 int a = 10; //变量 int b = 10; NSLog(@"%d -- %p", a, &a); NSLog(@"%d -- %p", b, &b);
打印结果如下图所示
- a、b都指向10,但是a、b的
地址不一样
,这是一种拷贝,属于值拷贝
,也称为深拷贝
- a,b的地址之间相差 4 个字节,这取决于a、b的类型
其地址指向如图所示
【对象指针】
//对象 CJLPerson *p1 = [CJLPerson alloc]; // p1 是指针 CJLPerson *p2 = [CJLPerson alloc]; NSLog(@"%d -- %p", p1, &p1); NSLog(@"%d -- %p", p2, &p2);
打印结果如图所示
- p1、p2 是指针,
p1
是 指向[CJLPerson alloc]
创建的空间地址,即内存地址,p2 同理 - &p1、&p2是
指向 p1、p2对象指针的地址
,这个指针 就是二级指针
其指针的指向如下图所示
【数组指针】
//数组指针 int c[4] = {1, 2, 3, 4}; int *d = c; NSLog(@"%p -- %p - %p", &c, &c[0], &c[1]); NSLog(@"%p -- %p - %p", d, d+1, d+2);
打印结果如下
&c
和&c[0]
都是取首地址
,即数组名等于首地址
&c
与&c[1]
相差4
个字节,地址之间相差的字节数,主要取决于存储的数据类型
- 可以通过
首地址+偏移量
取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于 偏移量 * 数据类型字节数
其指针指向如下所示
探索类信息中都有哪些内容
探索类信息
中有什么时,事先我们并不清楚类
的结构
是什么样的,但是我们可以通过类
得到一个首地址
,然后通过地址平移
去获取里面所有的值
根据前文提及的objc_class
的新版定义(objc4-781
版本)如下,有以下几个属性
struct objc_class : objc_object { // Class ISA; //8字节 Class superclass; //Class 类型 8字节 cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags //....方法部分省略,未贴出 }
isa
属性:继承自objc_object
的isa
,占8
字节superclass
属性:Class
类型,Class
是由objc_object
定义的,是一个指针
,占8
字节cache
属性:简单从类型class_data_bits_t
目前无法得知,而class_data_bits_t
是一个结构体
类型,结构体
的内存大小
需要根据内部的属性
来确定,而结构体指针才是8字节
bits
属性:只有首地址
经过上面3个属性的内存大小总和的平移,才能获取到bits
计算 cache 类的内存大小
进入cache类cache_t
的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),有如下几个属性
struct cache_t { #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED explicit_atomic<struct bucket_t *> _buckets; // 是一个结构体指针类型,占8字节 explicit_atomic<mask_t> _mask; //是mask_t 类型,而 mask_t 是 unsigned int 的别名,占4字节 #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 explicit_atomic<uintptr_t> _maskAndBuckets; //是指针,占8字节 mask_t _mask_unused; //是mask_t 类型,而 mask_t 是 uint32_t 类型定义的别名,占4字节 #if __LP64__ uint16_t _flags; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节 #endif uint16_t _occupied; //是uint16_t类型,uint16_t是 unsigned short 的别名,占 2个字节
- 计算
前两个属性
的内存大小
,有以下两种情况,最后的内存大小总和都是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
字节
_flags
是uint16_t
类型,uint16_t是unsigned short
的别名,占2
个字节_occupied
是uint16_t
类型,uint16_t是unsigned short
的别名,占2
个字节
总结:所以最后计算出cache类的内存大小 = 12 + 2 + 2 = 16
字节
获取bits
所以有上述计算可知,想要获取bits
的中的内容,只需通过类
的首地址平移32
字节即可
以下是通过lldb
命令调试的过程
- 获取类的首地址有两种方式
- 通过
p/x CJLPerson.class
直接获取首地址 - 通过
x/4gx CJLPerson.class
,打印内存信息获取
- 其中的
data()
获取数据,是由objc_class
提供的方法 - 从
$2
指针的打印结果中可以看出bits
中存储的信息,其类型是class_rw_t
,也是一个结构体类型。但我们还是没有看到属性列表、方法列表
等,需要继续往下探索
探索 属性列表,即 property_list
通过查看class_rw_t
定义的源码发现,结构体
中有提供
相应的方法
去获取 属性列表、方法列表
等,如下所示
在获取bits
并打印bits
信息的基础上,通过class_rw_t
提供的方法,继续探索 bits
中的属性列表
,以下是lldb 探索的过程图示
获取属性列表的lldb调试流程
p $8.properties()
命令中的propertoes
方法是由class_rw_t
提供的,方法中返回
的实际类型
为property_array_t
- 由于list的类型是
property_list_t
,是一个指针,所以通过p *$10
获取内存中的信息
,同时也证明bits
中存储了property_list
,即属性列表 p $11.get(1)
,想要获取CJLPerson
中的成员变量``bobby
, 发现会报错,提示数组越界
了,说明property_list
中只有 一个属性cjl_name
【问题】探索成员变量的存储
由此可得出property_list 中只有属性,没有成员变量
,属性与成员变量的区别就是有没有set、get方法,如果有,则是属性,如果没有,则是成员变量。
那么问题来了,成员变量存储在哪里?为什么会有这种情况?请移至文末的分析与探索
探索 方法列表,即methods_list
准备工作:在前文提及的CJLPerson中增加两个方法(实例方法 & 类方法)
//CJLPerson.h @property (nonatomic, copy) NSString *cjl_name; - (void)sayHello; + (void)sayBye; @end //CJLPerson.m @implementation CJLPerson - (void)sayHello {} + (void)sayBye {} @end
也是通过lldb调试来获取方法列表,步骤如图所示
获取方法列表的lldb调试流程
- 通过
p $4.methods()
获得具体的方法列表
的list
结构,其中methods
也是class_rw_t
提供的方法 - 通过打印的
count = 4
可知,存储了4
个方法,可以通过p $7.get(i)
内存偏移
的方式获取单个方法,i 的范围是0-3
- 如果在打印
p $7.get(4)
,获取第五个方法,也会报错,提示数组越界
新问题的探索
【问题】探索成员变量的存储
由上面的属性列表分析可得出property_list 中只有属性,没有成员变量
,那么问题来了,成员变量存储在哪里?为什么会有这种情况?
通过查看objc_class
中bits
属性中存储数据的类class_rw_t
的定义发现,除了methods、properties、protocols
方法,还有一个ro
方法,其返回类型是class_ro_t
,通过查看其定义,发现其中有一个ivars
属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t
类型的ivars
属性中呢?
下面是lldb的调试过程
成员变量存储探索的调试
class_ro_t
结构体中的属性如下所示,想要获取ivars
,需要ro的首地址平移48
字节
struct class_ro_t { uint32_t flags; //4 uint32_t instanceStart;//4 uint32_t instanceSize;//4 #ifdef __LP64__ uint32_t reserved; //4 #endif const uint8_t * ivarLayout; //8 const char * name; //1 ? 8 method_list_t * baseMethodList; // 8 protocol_list_t * baseProtocols; // 8 const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; //方法省略 }
通过图中可以看出,获取的ivars
属性,其中的count
为2
,通过打印发现 成员列表中除了有hobby,还有name,所以可以得出以下一些结论:
- 通过
{}
定义的成员变量
,会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表
,除了包括成员变量,还包括属性定义的成员变量 - 通过
@property
定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list
获取属性列表
,其中只包含属性
【问题】探索类方法的存储
由此可得出methods list 中只有 实例方法,没有类方法
,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下
在文章前半部分,我们曾提及了元类
,类对象
的isa
指向就是元类
,元类
是用来存储类
的相关信息的,所以我们猜测:是否类方法存储在元类的bits中呢
?可以通过lldb
命令来验证我们的猜测。下图是lldb命令的调试流程
类方法存储的探索流程
通过图中元类方法列表
的打印结果,我们可以知道,我们的猜测是正确的,所以可以得出以下结论:
类
的实例方法
存储在类的bits属性
中,通过bits --> methods() --> list
获取实例方法列表
,例如CJLPersong
类的实例方法sayHello
就存储在CJLPerson类的bits
属性中,类中的方法列表
除了包括实例方法
,还包括属性的set方法
和get方法
类
的类方法
存储在元类的bits属性
中,通过元类bits --> methods() --> list
获取类方法列表
,例如CJLPerson
中的类方法sayBye
就存储在CJLPerson
类的元类
(名称也是CJLPerson)的bits
属性中