类的初探
在我们平常的 iOS 开发中,类和对象是出现很高频的名词,在之前的isa 底层结构分析 中介绍了对象,那么类到底是什么呢?它的内部结构如何?怎么探索呢?今天我们就来揭开它神秘的面纱
准备工作
首先我们定义两个类,LCPerson,LCTeacher,且 LCTeacher 继承于 LCPerson
- LCPerson 类,继承自 NSObject
@interface LCPerson : NSObject {
NSString *name;
}
@property (nonatomic, copy) NSString *nickname;
- (void)sayHello;
+ (void)say666;
@end
@implementation LCPerson
- (void)sayHello {
}
+ (void)say666 {
}
@end
- LCTeacher 类,继承自 LCPerson
@interface LCTeacher : LCPerson
@end
@implementation LCTeacher
@end
- 在 main 函数中添加如下代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
LCPerson *person = [LCPerson alloc];
LCTeacher *teacher = [LCTeacher alloc];
NSLog(@"%@ - %@", person, teacher);
}
return 0;
}
LLDB 调试
- 首先我们打印 person 的信息以及内存地址
- 根据 isa 底层结构分析 我们知道对象的第一个属性是
isa
,里面存储的类信息(类的指针地址),我们可以通过它的 16 进制地址与上
ISA_MASK
就可以得到person
的类LCPerson
,并打印类的指针地址
- 我们再来看下
LCPerson
的内存情况,并打印LCPerson
内存中isa
指针指向的内容
- 这个打印的依然是
LCPerson
,继续之前的操作,如下
在以上的过程中,为什么会打印两次 LCPerson
、NSObject
,再之后就一直打印 NSObject
?
-
0x001d800100002195
是person
的isa
指针地址,其&
上后得到的是person
的类LCPerson
-
0x0000000100002168
是LCPerson
类的isa的指针地址,指向的是LCPerson
的元类 -
0x00000001003330f0
是LCPerson
的元类 的isa
指针地址,指向的是根元类
-
根元类的
isa
指向的是它自己
所以,上述打印两个 LCPerson
的根本原因就是因为元类导致的
元类
-
对象的
isa
是指向类,类其实也是一个对象,可以称为类对象,它的isa
指向苹果定义的元类
。 -
元类
是系统给的,其定义和创建都是由编译器完成,在这个过程中,类的归属来自于元类
-
元类
是类对象的类,每个类都有一个独一无二的元类
用来存储类方法的相关信息 -
元类
本身是没有名称的,由于与类相关联,所以使用了同类名一样的名称
类存在几份
从上面的我们可以看到,最后的根元类打印了很多份,那么他们是同一份吗?与我们开发中的 NSObject
是同一份吗?
验证方式
通过三种不同的方式获取类,看他们打印的地址是否相同
Class class1 = [LCPerson class];
Class class2 = [LCPerson alloc].class;
Class class3 = object_getClass([LCPerson alloc]);
NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
运行代码,打印结果如下
从结果中可以看出,打印的地址都是同一个,所以 NSObject
只有一份,即 NSObject
(根元类)在内存中永远只存在一份
总结:类的信息在内存中永远只存在一份,所以 类对象
只有一份
isa 的走位
根据上面的探索以及各种验证,对象
、类
、元类
、根元类
的关系如下图所示
⚠️实例对象之间没有继承关系,类之间才有继承关系
objc_object vs objc_class
为什么 对象
和 类
都有 isa
属性呢?首先我们去源码查看他们的底层结构
-
NSObject
的底层编译是NSObject_IMPL
结构体-
Class
是isa
指针的类型,是由objc_class
定义的类型 -
objc_class
是一个结构体指针。在 iOS 中,所有的类
(Class) 都是以objc_class
为模板创建的
-
-
Class
的源码
typedef struct objc_class *Class;
- 源码中搜索
objc_class
的定义
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();
}
...//很长,省略
}
⚠️objc_class
的定义有两个版本,早期的版本已经被废弃,使用最新本的定义是在 objc-runtime-new.h
中,后面的分析也是基于最新版的
此时,我们可一得知,objc_class
是一个继承于 objc_object
的结构体,在源码中搜索 objc_object
的定义
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
-
objc_object
也是一个结构体,而且它的成员只有一个isa
,那么objc_class
通过继承objc_object
也会获取到唯一的isa
属性
objc_object 与 对象 的关系
- 所有的
对象
都是以objc_object
为模板继承过来的 - 所有的
对象
是 来自NSObject
(OC) ,但是真正到底层的 是一个objc_object
(C/C++)的结构体类型
总结
- 所有的
对象
+类
+元类
都有isa
属性 - 所有的
对象
都是由objc_object
继承来的 - 在结构层面可以通俗的理解为
上层OC
与底层
的对接:-
下层
是通过结构体
定义的模板
,例如objc_class
、objc_object
-
上层
是通过底层的模板创建
的 一些类型,例如LCPerson
-
类的内存分布
在探索之前我们并不知道类的结构是什么样子的,进入源码
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
//....方法部分省略,未贴出
}
从上面我们可以得知,objc_class
有 三个属性:superclass
、cache
、bits
,因为继承自 objc_object
,所以它会有一个默认的属性 isa
,如果我们想要查看属性 bits
的内部结构,此时我们要怎么操作呢?
我们知道,获取一个类,就是获取它的首地址(即 isa),然后根据首地址平移前面属性占用的内存和来获取其他的属性(即属性 bits
的地址=首地址 + isa
的内存大小 + superclass 的内存大小 + cache 的内存大小)
拓展-内存偏移
1.普通变量
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 的类型
2.对象指针
LCPerson *p1 = [LCPerson alloc];
LCPerson *p2 = [LCPerson alloc];
NSLog(@"%d -- %p", p1, &p1);
NSLog(@"%d -- %p", p2, &p2);
打印结果如下
-
p1、p2 是指针,p1 是指向 [LCPerson alloc] 创建的空间地址,即内存地址,p2 同理
-
&p1、&p2 是 指向 p1、p2 对象指针的地址,这个指针是 二级指针
3.数组指针
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个字节,地址之间相差的字节数,主要取决于存储的数据类型
- 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址实际移动的字节数 等于 偏移量 * 数据类型字节数
获取 bits
计算前面三个属性的内存大小
-
isa
占 8 字节 -
superclass
也是Class
类型,所以占 8 字节 -
cache
占 16 字节-
static
类型 和函数方法
不占用内存大小,所以最终占用内存的只有下面 这四个成员 - _buckets 的真正类型是 struct 指针,占用 8 字节
- _mask 真正的类型是 uint32_t 占用 4 字节
- _flags 真正的类型是 unsigned short 类型占 2 字节
- _occupied 真正的类型是 unsigned short 类型占 2 字节
-
cache_t 的源码定义 ,有如下属性
explicit_atomic<struct bucket_t *> _buckets;
explicit_atomic<mask_t> _mask;
uint16_t _flags;
uint16_t _occupied;
想要获取 bits 的信息,需要将类的地址平移 32 字节才可以获取到
- 获取类对象的首地址
- 打印首地址中
isa
指针的内存信息
- 首地址 0x1000021f0 平移 32 字节 就是 0x100002210,就是 bits 存储地址,打印 bits 数据
class_rw_t *data() const {
return bits.data();
}
bits
中存储的信息,其类型是 class_rw_t
,也是一个结构体类型。里面存储类的 属性列表
、方法列表
、协议列表
等
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->methods;
} else {
return method_array_t{v.get<const class_ro_t *>()->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->properties;
} else {
return property_array_t{v.get<const class_ro_t *>()->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is<class_rw_ext_t *>()) {
return v.get<class_rw_ext_t *>()->protocols;
} else {
return protocol_array_t{v.get<const class_ro_t *>()->baseProtocols};
}
}
属性列表
在上述基础上,通过 class_rw_t
的源码定义,探索 bits
中的 属性列表
- 获取属性列表数据,根据结果取出 list 数据并打印出来
- 获取每个属性,并打印出来
获取 LCPerson
中的成员变量 name
, 发现会报错,提示数组越界了,说明 property_list 中只有 一个属性 nickname。
方法列表
- 获取方法列表数据,根据结果取出 list 数据并打印出来
- 获取每个方法信息,并打印出来
在获取第五个方法,也会报错,提示数组越界
网友评论