iOS 底层探索 文章汇总
目录
- 一、前言
- 二、类的结构分析
- 三、【百度面试题】objc_object 与 对象的关系
- 总结
一、前言
上一篇文章iOS 对象的本质我们分析了对象的底层结构,并在iOS isa底层结构分析中提到了 对象,类,元类,根元类等概念,这篇文章我们就一起来分析类的底层结构到底是什么。
在开始探究之前,先补充一下 内存偏移 的概念,主要是为了更好理解后面的类的结构体。
int c[4] = {1,2,3}; // 这里先定义一个int数组 c
int *d = c; // 然后定义一个指针d指向 c
NANSLog(@"%p - %p - %p - %p",&c,&c[0],&c[1],&c[2]);
NANSLog(@"%p - %p - %p",d,d+1,d+2);
打印结果:0x7ffeefbff500 - 0x7ffeefbff500 - 0x7ffeefbff504 - 0x7ffeefbff508
打印结果: 0x7ffeefbff500 - 0x7ffeefbff504 - 0x7ffeefbff508
看这里我们会发现 数组c 的地址 和 c[0] 是同一个地址, 而指针d也是等于 数组c的首地址
并且通过指针d+1,d+2 也能找到数组相应的元素,所以说通过指针偏移可以指向接下来连续的内存地址。
二、类的结构分析
1. 类的底层实现
我们都知道,所有的类都是继承于NSObject
,那NSObject
本身不就是一个类吗?下面先结合源码来看一下
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
}
return 0;
}
通过NSObject
点击进入底层
@interface NSObject {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
这里我们可以看到 NSObject
里面仅有一个Class isa
,那么这个Class
又是什么,继续点进去
typedef struct objc_class *Class;
typedef struct objc_object *id;
这里我们可以看到Class
是一个结构体,也就是之前说到的,类的本质就是一个结构体。而objc_class
又是继承自objc_object
, 这也说明了我们常说的万物皆对象
。
-
NSObject
本身是一个类,在底层实现就是objc_class
。 -
objc_object
是c
的结构类型,NSObject
是OC
的类型,NSObject
就是对objc_object
的封装。
objc_object
和objc_class
关系图如下:
2. 类的属性、方法、成员变量、协议...分析
上面我们知道了类的结构是什么样的,那么类里面具体都包含了一些什么内容呢,下面我们就来分析一下objc_class
struct objc_class : objc_object {
// Class ISA; // 8字节
Class superclass; // 8字节
cache_t cache; // formerly cache pointer and vtable 16字节
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
....省略后面的代码.....
}
1.第一个属性Class ISA
被注释掉的,意思就是从父类继承过来的,我们进入 objc_object
里面可以看到只有一个isa
,占用8个字节。
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
....省略后面的代码.....
};
2.第二个属性Class superclass
父类,一个指针占用8个字节。
3.第三个属性cache_t cache
一个结构体,顾名思义是一些缓存的信息,总共占用16个字节
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets; // 结构体指针8个字节
explicit_atomic _mask; //typedef uint32_t mask_t; int占4字节
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
....省略.....
#if __LP64__
uint16_t _flags;// 2字节
#endif
uint16_t _occupied;// 2字节
}
其他的static变量和方法均不存在结构体内存中,因此不占内存
....省略后面的代码.....
4.第四个属性bits
是什么?这里我们来看一下。
typedef unsigned long uintptr_t;
struct class_data_bits_t {
// 相当于 unsigned long bits; 占64位
// bits实际上是一个地址(是一个对象的指针,可以指向class_ro_t,也可以指向class_rw_t)
uintptr_t bits;
... 省略...
}
从这里可以看到bits
应该就是一个64位的数据段,那么里面存了什么数据呢,还要继续往下分析。
在class_data_bits_t bits
的注释:class_rw_t * plus custom rr/alloc flags,意思是class_data_bits_t
就相当于class_rw_t *
加上rr/alloc
标志。它提供了data()
方法返回class_rw_t *
指针。
而在bits后面就紧接着声明了一个 class_rw_t *
指针,通过bits.data()
返回,接下来就来看看这个bits.data()
class_rw_t *data() {
// 这里的bits就是上面定义的class_data_bits_t bits;
return bits.data();
}
class_rw_t* data() const {
// FAST_DATA_MASK的值是0x00007ffffffffff8UL
//(lldb) p/t 0x00007ffffffffff8 打印二进制 看一下
//(long) $0 = 0b0000000000000000011111111111111111111111111111111111111111111000
// bits和FAST_DATA_MASK按位与,实际上就是取了bits中的[3,46]共44位
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
那么这个class_rw_t *
是什么呢?
struct class_rw_ext_t {
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
char *demangledName;
uint32_t version;
};
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
explicit_atomic ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
private:
...省略代码
public:
...省略代码
const class_ro_t *ro()
void set_ro(const class_ro_t *ro)
const method_array_t methods()
const property_array_t properties()
const protocol_array_t protocols()
在这个 class_rw_t
结构体中我们发现这里有methods(方法)
、properties(属性)
、protocols(协议)
这些信息,那么我们所需要的类中的方法、属性、成员变量等信息是不是在这里存储的呢?下面我们就用代码来验证下。
@interface NAPerson ()
{
NSString *hobby;
}
@property (nonatomic, copy) NSString *nickName;
- (void)sayHello;
+ (void)sayHappy;
@end
先定义一个NAPerson
类,里面有 属性:nickName
、 成员变量:hobby
、 对象方法:sayHello
、 类方法:sayHappy
然后我们通过lldb
指令打印查看,结合上面的分析,来看看这几个成员都存储在了什么地方
在找
bits
的时候是通过内存偏移
方法来找到,这也就是开头先补充的内存偏移的概念。 因为在objc_class
的结构中,isa占8字节
,superclass占用8字节
,cache占用16个字节
,将cls
的地址偏移32个字节
即0x20
便是bits
的地址。
获取类的首地址有两种方式
- 通过
p/x CJLPerson.class
直接获取首地址 - 通过
x/4gx CJLPerson.class
,打印内存信息获取
注意:这里获取
bits
是通过类的内存地址加上偏移量而不是通过isa
的地址加上偏移量,这也是类和数组不同的地方。类的地址也是第一个元素地址,只是通过x/4gx
读出来的是类地址后面存的值,不是第一个元素地址。isa
的地址比类的地址低(0x28)40字节
(当我们实现了类之后就会直接去处理元类,所以类和元类是连续的,而类的大小 到bit
之后正好40
在,这里涉及到内存平移到元类)。
通过查看class_rw_t
定义的源码发现,结构体中有提供相应的方法去获取 属性列表、方法列表等
方法的符号绑定:对于
v16@0:8
v
表示方法的返回值为void
16
表示方法参数的大小(通过clang
编译的cpp文件
可知该方法存在两默认的参数:(NAPerson * self, SEL _cmd
))
@
表示方法的第一个参数类型
0
表示方法的第一个参数占用内存的起始位置
:
表示方法的第二个参数类型
8
表示方法的第二个参数占用内存的起始位置
对于set方法会多一个参数所以方法的符号会有区别
关于参数类型编码参考这个链接
通过以上打印可以看到,在class_rw_t
中找到了我们所定义的nickName
属性、对象方法sayHello
、nickName
的setter/getter
方法,但是成员变量hobby
和类方法sayHappy
都没有找到。
此时再从class_rw_t
找一找其他线索,发现有一个const class_ro_t *ro()
的方法,该方法返回一个常量结构体指针
,那么我们要找的成员变量和类方法会不会在这里呢,点进去看一下
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList; //方法列表
protocol_list_t * baseProtocols; //协议列表
const ivar_list_t * ivars; //成员变量列表
const uint8_t * weakIvarLayout;
property_list_t *baseProperties; //属性列表
method_list_t *baseMethods() const {
return baseMethodList;
}
....省略代码
}
进来一看发现,这里竟然跟class_rw_t
看起来差不多,同样有方法、属性、协议列表
,而且还有一个ivars 列表
,那么这个ivars
会不会就是成员变量列表呢。接下来继续用lldb
指令来查看
果然,我们定义的成员变量hobby
是在class_ro_t
里面的。同样在baseMethodList
、baseProperties
里面也找到了我们所定义的属性和对象方法,这里就不截图了。
此时我们来总结一下:
1.在
class_rw_t
里面存放的有methods
、properties
、protocols
2.在class_ro_t
里有baseMethodList
、baseProperties
、baseProtocols
、ivars
3.class_ro_t
这个结构体是通过const
定义,说明在编译时候就确定好了,后面取出来使用是不可以更改的。
4.成员变量不生成setter/getter
方法,并且存在class_ro_t
的ivars
里面。
5.此时还有一个类方法sayHappy
没有找到。
通过以上分析我们大概可以知道,类的属性
、成员变量
、方法
、协议
等信息存在什么位置了。但是class_rw_t
和class_ro_t
为什么会存了一些相同的信息呢?这就需要我们进一步的分析了。
3. 内的信息是如何存储的
通过前面的分析,在class_rw_t
结构中可以拿到类的属性等相关信息了,class_ro_t
结构中可以拿到类的成员变量等信息,由此就形成以这样的一个结构。
通过上图可以得出以下一些结论:
- 通过
@property
定义的属性
,会存储在bits
属性中,通过bits --> data() --> properties() --> list
获取属性列表
,其中只包含属性- 通过
{}
定义的成员变量
,也会存储在类的bits
属性中,通过bits --> data() -->ro() --> ivars
获取成员变量列表
,除了包括成员变量,还包括属性定义的成员变量
4. 探索类方法存储位置
此时此刻我们就把class_rw_t
和 class_ro_t
存储类信息的过程探索的差不多了。
但是类方法sayHappy还没找到,既然我们在类里面没有找到sayHappy
,那么我们想一下它会存到哪里呢?通过iOS isa底层结构分析这篇文章分析,猜想它会不存到元类里面去了,那就去元类找找看
从上图打印来看,我们在元类
里面找到了类方法sayHappy
,证明了类方法
是存在元类
里面的。此刻我们所生命的方法、属性、成员变量已经全部找到了,也大概了解了类的结构以及类的成员信息都存在哪里。
三、【百度面试题】objc_object 与 对象的关系
- 所有的
对象
都是以objc_object
为模板继承
过来的 - 所有的
对象
是 来自NSObject(OC)
,但是真正到底层的 是一个objc_object(C/C++)
的结构体类型
【总结】objc_object
与 对象的关系 是 继承关系
总结
- 所有的
对象
+类
+元类
都有isa
属性 - 所有的
对象
都是由objc_object
继承来的 - 类的本质是一个
struct objec_class:objc_object
结构体, 万物皆对象,类也- 是一个对象。 - 属性会自动生成
setter/getter
方法,成员变量不会。并且属性在编译之后会生成带有_
的成员变量存储在ivars
里面。 - 类的
对象方法
存在本类当中
,而类方法
存在元类中