类的结构分析
分析
isa
走向以及继承关系
1. 准备工作
- 【1】定义两个类
// 1.继承自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
//2. 继承自CJLPerson的类CJLTeacher
@interface CJLTeacher : CJLPerson
@end
@implementation CJLTeacher
@end
- 【2】在
main
中实现两个对象
int main(int argc, const char * argv[]) {
@autoreleasepool {
//ISA_MASK
CJLPerson *person = [CJLPerson alloc];
CJLTeacher *teacher = [CJLTeacher alloc];
NSLog(@"Hello, World! %@ - %@",person,teacher);
}
return 0;
}
2. 元类
- 我们都知道 对象的isa 是指向类,类的其实也是一个对象,可以称为类对象,其isa的位域指向苹果定义的元类
元类
是系统
给的,其定义和创建都是由编译器完成
,在这个过程中,类的归属来自于元类
元类
是类对象
的类,每个类都有一个独一无二的元类
用来存储类方法
的相关信息。元类
本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称
- 【1】在main中CJLTeacher部分加一个断点,运行程序
- 【2】开启
lldb调试
,调试的过程如下图所示类-元类-根元类.png
图中两LGPerson的来历-
0x0000000100002188
是person
对象的isa指针
地址&后得到的结果是创建person的类CJLPerson
-
0x0000000100002160
是isa
中获取的类信息所指的类的isa的指针地址
,即 CJLPerson类的类的isa指针地址,在Apple中,我们简称CJLPerson类的类为元类
-
总结
-
对象
的isa
指向类
(也可称为类对象
) -
类
的isa
指向元类
-
元类
的isa
指向根元类
,即NSObject
-
根元类
的isa
指向它自己
isa走位
& 继承关系
图
![](https://img.haomeiwen.com/i18769452/56be32d64205f38b.png)
objc_class & objc_object
isa走位我们理清楚了,又来了一个新的问题:为什么 对象 和 类都有isa属性呢?这里就不得不提到两个结构体类型:objc_class & objc_object
1. objc_class
- 从前面文章通过
clan
编译过的main.app
文件可以知道,NSObject
的底层
编译是NSObject_IMPL结构体
- 其中
Class
是isa
指针的类型,是由objc_class
定义的类型 - 而
objc_class
是一个结构体
。在iOS中,所有的Class都是以 objc_class 为模板创建的
- 其中
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
-
objc_class
底层实现
从定义中,可以看到objc_class
结构体类型是继承自objc_object
的
2. objc_object
- 以下是编译后的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
属性
总结
-
所有的
对象
、类
、元类
都有isa
属性 -
所有的
对象
都是由objc_object
继承来的 -
简单概括就是
万物皆对象
,万物皆来源于objc_object
,有以下两点结论:- 所有以
objc_object
为模板创建的对象,都有isa
属性 - 所有以
objc_class
为模板,创建的类,都有isa
属性
- 所有以
-
在结构层面可以通俗的理解为
上层OC
与底层
的对接:-
下层
是通过结构体
定义的模板
,例如objc_class
、objc_object
-
上层
是通过底层的模板创建
的 一些类型,例如CJLPerson
-
类的结构分析
主要是分析
类信息
中存储
了哪些内容
补充知识-内存偏移
在分析类结构之前,需要先了解内存偏移,因为类信息中访问时,需要使用内存偏移
【普通指针】
//普通指针
int a = 10; //变量
int b = 10;
NSLog(@"%d -- %p", a, &a);
NSLog(@"%d -- %p", b, &b);
打印结果如下图所示
![](https://img.haomeiwen.com/i18769452/ef47cb128f88f952.jpg)
- 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);
打印结果如下图所示
![](https://img.haomeiwen.com/i18769452/4b111b8134cc7a68.png)
-
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);
打印结果如下图所示
![](https://img.haomeiwen.com/i18769452/c4af14c7ec775cd5.png)
- &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
命令调试的过程
![](https://img.haomeiwen.com/i18769452/62cee02e01c2a3b5.png)
-
其中的
data()
获取数据,是由objc_class
提供的方法 -
从
$2
指针的打印结果中可以看出bits中存储的信息
,其类型是class_rw_t
,也是一个结构体类型
。但我们还是没有看到属性列表、方法列表
等,需要继续往下探索
探索 属性列表,即 property_list
通过查看class_rw_t
定义的源码发现,结构体中有提供相应的方法去获取属性列表、方法列表等,如下所示
![](https://img.haomeiwen.com/i18769452/c61f04d9daba522f.jpeg)
在获取bits
并打印bits
信息的基础上,通过class_rw_t
提供的方法,继续探索 bits中的属性列表,以下是lldb
探索的过程图示
![](https://img.haomeiwen.com/i18769452/9589d8159433be0e.png)
-
p $8.properties()
命令中的propertoes
方法是由class_rw_t
提供的,方法中返回的实际类型为property_array_t
-
由于
list
的类型是property_list_t
,是一个指针,所以通过p *$5
获取内存中的信息,同时也证明bits
中存储了property_list
,即属性列表
-
p $6.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调试来获取方法列表,步骤如图所示
![](https://img.haomeiwen.com/i18769452/863f21caf612133c.png)
-
通过
p $4.methods()
获得具体的方法列表的list结构
,其中methods
也是class_rw_t
提供的方法 -
通过打印的
count = 4
可知,存储了4
个方法,可以通过p $7.get(i)
内存偏移的方式获取单个方法,i 的范围是0-3 -
如果在打印 p $7.get(4),获取第五个方法,也会报错,提示数组越界
下面是lldb的调试过程通过查看
objc_class
中bits属性
中存储数据的类class_rw_t
的定义发现,除了methods
、properties
、protocols
方法,还有一个ro
方法,其返回类型是class_ro_t
,通过查看其定义,发现其中有一个ivars
属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t
类型的ivars
属性中呢?
![](https://img.haomeiwen.com/i18769452/a15ad7fe7ae72a87.png)
-
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
获取属性列表
,其中只包含属性
可以通过lldb命令来验证我们的猜测。下图是lldb命令的调试流程在文章前半部分,我们曾提及了
元类
,类对象的isa
指向就是元类,元类
是用来存储类的相关信息
的,所以我们猜测:是否类方法存储在元类的bits中呢?
![](https://img.haomeiwen.com/i18769452/d5d864c383228596.png)
通过图中元类方法列表的打印结果,我们可以知道,我们的猜测是正确的,所以可以得出以下结论:
-
类的
实例方法
存储在类的bits
属性中,通过bits --> methods() --> list
获取实例方法列表,例如CJLPersong类的实例方法sayHello 就存储在 CJLPerson类的bits属性中,类中的方法列表除了包括实例方法,还包括属性的set方法 和 get方法 -
类的
类方法
存储在元类的bits
属性中,通过元类bits --> methods() --> list
获取类方法列表,例如CJLPerson中的类方法sayBye 就存储在CJLPerson类的元类(名称也是CJLPerson)的bits属性中
网友评论