iOS-底层原理 08:类 & 类结构分析
本文的主要目的是分析 类 & 类的结构,整篇都是围绕一个类
展开的一些探索
类 的分析
类的分析 主要是分析 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调试,调试的过程如下图所示
根据调试过程,我们产生了一个疑问
:为什么图中的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
指向根元类
,即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 走位链(两条)
-
teacher的isa走位链:
teacher(子类对象) --> CJLTeacher (子类)--> CJLTeacher(子元类) --> NSObject(根元类) --> NSObject(跟根元类,即自己)
-
person的isa走位图:
teacher(父类对象) --> CJLTeacher (父类)--> CJLTeacher(父元类) --> 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-runtime-new.h
,这个是objc4-781
最新优化的,我们后面的类的结构分析也是基于新版来分析的。
从新版的定义中,可以看到objc_class
结构体类型是继承自objc_object
的,
-
-
在objc4源码中搜索
objc_object (或者 objc_object {
,这个类型也有两个版本- 一个位于
objc.h
,没有被废除,从编译的main.cpp
中可以看到,使用的这个版本的objc_object
- 位于
objc-privat.h
- 一个位于
以下是编译后的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
字节 -
suoerclass
属性: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 探索的过程图示
-
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调试来获取方法列表,步骤如图所示
-
通过
p $4.methods()
获得具体的方法列表
的list
结构,其中methods
也是class_rw_t
提供的方法 -
通过打印的
count = 4
可知,存储了4
个方法,可以通过p $7.get(i)
内存偏移
的方式获取单个方法,i 的范围是0-3
-
如果在打印
p $7.get(4)
,获取第五个方法,也会报错,提示数组越界
【问题】探索类方法的存储
由此可得出methods list 中只有 实例方法,没有类方法
,那么问题来了,类方法存储在哪里?为什么会有这种情况?下面我们来仔细分析下
在文章前半部分,我们曾提及了元类
,类对象
的isa
指向就是元类
,元类
是用来存储类
的相关信息的,所以我们猜测:是否类方法存储在元类的bits中呢
?可以通过lldb
命令来验证我们的猜测。下图是lldb命令的调试流程
通过图中元类方法列表
的打印结果,我们可以知道,我们的猜测是正确的,所以可以得出以下结论:
-
类
的实例方法
存储在类的bits属性
中,例如CJLPersong
类的实例方法sayHello
就存储在CJLPerson类的bits
属性中 -
类
的类方法
存储在元类的bits属性
中,例如CJLPerson
中的类方法sayBye
就存储在CJLPerson
类的元类
(名称也是CJLPerson)的bits
属性中