前言
iOS界比较流行的一句话:万物皆对象!为什么会有这么一个说法呢?难道类(class)也是对象(object)?
本文所采用的源码为苹果开源的最新 objc4-781 版本。
- 苹果官方源码下载地址
- 也可以直接下载GitHub
1、类的本质是什么?
在iOS底层原理04:isa底层原理分析上文章中,我们使用clang编译过main.m文件,并且从源码中得知:
- 1.
对象的本质是个结构体。 - 2.
NSObject_IVARS本质是class类型的isa。 - 3.
objc_class是一个结构体。在iOS中,所有的Class都是以objc_class为模板创建的。
struct Person_IMPL {
struct NSObject_IMPL NSObject_IVARS;
NSString *_name;
};
struct NSObject_IMPL {
Class isa;
};
typedef struct objc_class *Class;
于是在objc4-781源码中搜索objc_class的定义,源码中对objc_class的定义有两个版本。
旧版的定义在runtime.h中,已经废除。- 新版的定义在
objc-runtime-new.h中。
从新版的定义中,可以看到 objc_class 结构体类型是继承自 objc_object的。
总结
类的本质是objc_class 结构类型的指针,是继承objc_object,所有的Class都是以 objc_class 为模板创建的。
2、objc_class 和 objc_object的关系?
我们在objc4源码中搜索objc_object {,源码中对objc_object的定义有两个版本。
- 定义在
objc.h中。
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
- 定义在
objc-private.h中。
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
...
}
通过上述源码,可以总结出几点:
- 1、
objc_class继承自objc_object,其中objc_object也是一个结构体,且有一个isa属性,所以objc_class也拥有了isa属性。 - 2、
NSObject的定义是Class类型的isa,其中Class的底层编码来自objc_class类型,所以NSObject也拥有了isa属性。 - 3、
NSObject是一个类,用它初始化一个实例对象objc,objc也拥有isa属性,主要是因为NSObject类 从objc_class继承过来的,而objc_class继承自objc_object,objc_object有isa属性。 - 4、所有以
objc_object为模板 创建的对象,都有isa属性。 - 5、所有以
objc_class为模板,创建的类,都有isa属性
万物皆对象,万物皆来源于objc_object。
objc_object、objc_class、isa、object、NSObject等的整体的关系,如下图所示
3、什么是地址平移?
1、普通指针
我们先来看一段代码:
void normalPointer(){
int a = 10;
int b = 10;
MyNSLog(@"a的地址是:%p a的值是:%d",&a,a);
MyNSLog(@"b的地址是:%p b的值是:%d",&b,b);
}
打印结果:
a的地址是:0x7ffeefbff590 a的值是:10
b的地址是:0x7ffeefbff594 b的值是:10
- a、b地址不同,相差4个字节。这个取决于a、b的类型(int),a、b的值相同,说明他们都指向10这个常量,这也是值拷贝。
- 地址指向如下图所示:
2、对象指针
接着看一段代码:
void objectPointer(){
Person *p1 = [Person alloc];
Person *p2 = [Person alloc];
MyNSLog(@"%p -- %p", p1, &p1);
MyNSLog(@"%p -- %p", p2, &p2);
}
打印结果:
0x101859b70 -- 0x7ffeefbff588
0x101858440 -- 0x7ffeefbff590
-
p1、p2 是指针,
p1是 指向[Person alloc]创建的空间地址,即内存地址,p2 也是一样。 -
&p1、&p2是
指向 p1、p2对象指针的地址,这个指针 就是二级指针。 -
来个图说明一下:
3、数组指针
继续看一段代码:
void arrayPointer(){
int c[4] = {1, 2, 3, 4};
int *d = c;
MyNSLog(@"%p -- %p - %p", &c, &c[0], &c[1]);
MyNSLog(@"%p -- %p - %p", d, d+1, d+2);
}
打印结果:
0x7ffeefbff580 -- 0x7ffeefbff580 - 0x7ffeefbff584
0x7ffeefbff580 -- 0x7ffeefbff584 - 0x7ffeefbff588
- &c 和 &c[0] 都是取数组的
首地址,&c[0] 和 &c[1]相差4个字节,取决于它们的数据类型。 *d = c,即将c的首地址赋值给d,d+1,即在内存中取下一个地址,地址平移的字节数 = 偏移量 * 数据类型字节数,这就是地址平移。- 来个图说明一下:
地址平移的作用
- 我们探索
类信息的时候,事先我们并不清楚类的结构是什么样的,但是我们可以通过类得到一个首地址,然后通过地址平移去获取里面所有的值。
4、类的结构分析
1、类的结构有什么?
根据上文在objc4-781源码中搜索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();
}
...
以下部分定义了方法,此处省略
}
isa:继承自objc_object的isa,占8字节superclass:Class类型,Class是由objc_object定义的,是一个指针,占8字节cache:只知道是cache_t类型,至于里面是什么,下文揭晓。bits:看得出是class_data_bits_t,至于里面是什么,下文揭晓。
2、cache_t探索
查看cache_t源码(忽略静态属性和方法),主要的结构如下:
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 是 unsigned int 类型,占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic<uintptr_t> _maskAndBuckets;
mask_t _mask_unused;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
// _maskAndBuckets stores the mask shift in the low 4 bits, and
// the buckets pointer in the remainder of the value. The mask
// shift is the value where (0xffff >> shift) produces the correct
// mask. This is equal to 16 - log2(cache_size).
explicit_atomic<uintptr_t> _maskAndBuckets; //uintptr_t 是 unsigned long 类型,占8字节
mask_t _mask_unused; //mask_t 是 unsigned int 类型,占4字节
#if __LP64__
uint16_t _flags; //uint16_t 是 unsigned short 类型,占2字节
#endif
uint16_t _occupied; //uint16_t 是 unsigned short 类型,占2字节
}
- 计算cache_t内存大小:
if流程:buckets类型是struct bucket_t *,是结构体指针类型,占8字节。_mask是mask_t类型,而mask_t是unsigned int类型,占4字节。
else流程:_maskAndBuckets类型是uintptr_t,而uintptr_t是unsigned long类型,占8字节。_mask是mask_t类型,而mask_t是unsigned int类型,占4字节。
if - else不管执行哪一个,这一部分的内存大小 = 8 + 4 =12字节。_flags是uint16_t类型,uint16_t是unsigned short类型,占2字节_occupied也是是uint16_t类型,占2字节
小结:
cache_t的内存大小 = 12 + 2 + 2 = 16字节。
3、bits探索
通过上述的探索,我们知道类的属性isa占8字节,superclass占8字节,cache占16字节,因此我们想要获取bits的内容,那么类的首地址需要平移 8 + 8 + 16 = 32字节。
接下来我们结合代码,探索一番。
- 首先申明个p1对象,打个断点,通过lldb命令调试
- 执行
p/x Person.class获取首地址:
(lldb) p/x Person.class
(Class) $0 = 0x00000001000032b8 Person
- 将首地址平移
32字节,获取bits的地址,32的16进制是0x20,执行p (class_data_bits_t *) 0x00000001000032d8
p (class_data_bits_t *) 0x00000001000032d8
(class_data_bits_t *) $1 = 0x00000001000032d8
- 通过bits地址获取data数据,执行
p $1->data(),获取到了class_rw_t类型的数据
class_rw_t *data() const {
return bits.data();
}
- 打印结果:
p $1->data()
(class_rw_t *) $2 = 0x00000001006bf040
- 执行
p *$2,打印数据中的信息
(lldb) p *$2
(class_rw_t) $3 = {
flags = 2148007936
witness = 1
ro_or_rw_ext = {
std::__1::atomic<unsigned long> = 4294979904
}
firstSubclass = nil
nextSiblingClass = NSUUID
}
- 通过上面的步骤,我们成功获取了bits的地址,从而获取到了data数据,那么
class_rw_t里面又是什么呢?我们接着往下探索。
4、class_rw_t探索
点击查看class_rw_t源码,发现有很多内容,有很多不懂的东西,继续往下看代码,终于看到了写熟悉的字眼...
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
#if SUPPORT_INDEXED_ISA
uint16_t index;
#endif
...此处省略代码
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是一个结构体,存储了方法列表,属性列表,协议列表等等。 -
对于类的底层探索,就先到这里了,那么如何去获取类的属性,方法等,在下一篇文章继续探索。