目录
1:类的本质
2:类的结构
3:类以及类的元类是什么时候创建的
4:lldb对类动态调试
5:总结
我们知道在面向对象的语言中,万物皆对象,在iOS系统中也是如此,由类派生出对象,而其实Class(类)本身也是一个对象。
一 :类的本质
在iOS系统中,NSObject
是所有类的基类,我们一般创建的类都会基于它或者它的子类去派生。那么我们就从这个入口出发,去探索下在系统的底层,类是怎样的一种结构。
#import <Foundation/Foundation.h>
@interface China : NSObject
{
NSString *gdp;
}
@property (nonatomic, strong) NSString *people;
@property (nonatomic, strong) NSString *area;
- (void)countPeople;
+ (void)gdpIncrease;
@end
@implementation China
- (void)countPeople{
}
+ (void)gdpIncrease{
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
China *object = [China alloc];
// **断点位置**
}
return 0;
}
在main
函数里,我们alloc
出来一个object
对象,下面我们通过clang
命令把代码转换成c++
形式去查看,命令如下:
clang -rewrite-objc main.m -o main.cpp
转换成功之后我们打开main.cpp
,天哪居然代码行数飙升到9000多行!不过没关系,其他代码我们先不看,我们直接搜索关键字China,发现也有好多的结果,仔细逐个查看之后发现了这个定义:
typedef struct objc_object China;
可以看出,我们创建的China类是一个通过typedef struct objc_objec
定义的结构体。那么Class
究竟是什么呢?我们知道,底层对象都是struct结构体表示而且类型都以objc_
开头,我们把关键词切换成typedef struct objc_
继续搜索,又出来一大堆结果,有点崩溃...我们通过Commmand + G
翻阅,很幸运,翻阅了几次就找到了如下定义:
typedef struct objc_class *Class;
So,在底层Class
是一个objc_class
类型的结构体指针。我们把关键词改成typedef struct objc_class
继续搜索,看看有没有具体的定义,结果发现结果为空,现在只能另辟蹊径,通过源码来搜索了。
这边我使用的是objc4-756
的源码配置的工程,打开工程,全局搜索struct objc_class
查看结果:
// 在objc-runtime-new.h这个文件发现了这段定义
struct objc_class : objc_object {
// Class ISA;
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 8
class_rw_t *data() {
return bits.data();
}
//下面还有很多方法,在这里暂时我们不关注
};
可以发现,objc_class
在底层是一个继承于objc_object
的结构体,而objc_object
又是什么?我们看下面两段源码大家就会明白了:
// 在objc.h文件中
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
有没有发现特别的相似?其实在底层,struct objc_object
就是在OC中的NSObject派生的对象。而struct objc_class
就是NSObject的Class类对象。
二 :类的结构
类的结构其实也很清晰,通过系统给成员的命名,大概就能猜出其成员的大概作用:
struct objc_class : objc_object {
// Class ISA;
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 8
class_rw_t *data() {
return bits.data();
}
//下面还有很多方法,在这里暂时我们不关注
};
2.1: isa
-- isa指针
在iOS底层探究-isa的初始化&走位分析中我们研究过对象内部是有个isa指针,是实现类和对象建立绑定的,而类中的isa是指向元类的,也就是说类是其元类的实例。
2.2: Class superclass
指向父类的指针,它也是一个Class
类型,如果已经是基类NSObject
那么superclass
为nil
。
2.3: cache_t cache
cache是用来缓存方法的,后面会详细分析,曾经调用过的方法会缓存到这里,加速下一次的读取。
2.4: class_data_bits_t bits
用来保存类的信息,方法列表,属性列表,协议列表等等。
2.5: class_rw_t *data()
这个函数用来获取该类的可读写信息,由第4点的bit->data()调用,返回的为class_rw_t
类型:
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods; // 方法列表
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
可以看出方法列表,属性列表,和协议列表都在这个结构体当中,我们知道在苹果runtime
中,方法、属性、协议都可以动态添加,也就是说这三个属性都是可读可写的,class_rw_t
中的rw
可以理解为readwrite,也隐含着这个寓意。那大家可能就会有疑问了,那成员变量呢?在runtime
中只读的成员变量放到哪里面去啦?大家可以留意这个成员const class_ro_t *ro
其中的ro
是不是代表着readonly
呢?嘻嘻,我们一起看下class_ro_t
这个结构体:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;//instance对象占用的内存空间
#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;
}
};
const ivar_list_t * ivars
这个便是成员变量列表。而且是被const
修饰的,说明是不可修改,也印证了我们之前的猜测。但是在class_ro_t
中我们发现了:
method_list_t * baseMethodList; // 方法列表
protocol_list_t * baseProtocols; // 协议列表
property_list_t *baseProperties; // 属性列表
在
class_ro_t
中:method_list_t * baseMethodList
、protocol_list_t * baseProtocols
、property_list_t > *baseProperties
这三个和我们在class_rw_t
中看到的method_array_t methods
、property_array_t properties
、protocol_array_t protocols
有什么区别呢?
class_rw_t
中的methods
方法列表,properties
属性列表都是二维数组,是可读可写的,包含类的初始内容,分类内容。
class_ro_t
里面的baseMethodList
,baseProtocols
,ivars
,baseProperties
是一维数组,是只读的,包含类的初始化内容.
三:类以及类的元类是什么时候创建的
我们知道,类派生的对象是alloc的时候创建的,而类包括元类本身是什么时候创建的呢?我们通过代码探索一下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
// **断点位置**
China *object = [China alloc];
}
return 0;
}
运行工程之后我们在控制台进行打印:
(lldb) p/x China.class
(Class) $1 = 0x00000001000012f8 China
// China Class
(lldb) x/4xg 0x00000001000012f8
0x1000012f8: 0x001d8001000012d1 0x0000000100afd140
0x100001308: 0x00000001003a0260 0x0000000000000000
// isa & MASK
(lldb) p/x 0x001d8001000012d1 & 0x00007ffffffffff8
(long) $3 = 0x00000001000012d0
// China metaClass
(lldb) x/4xg 0x00000001000012d0
0x1000012d0: 0x001d800100afd0f1 0x0000000100afd0f0
0x1000012e0: 0x0000000101f004e0 0x0000000200000003
我们看到,在进行 alloc 操作之前,类和元类就已经被编译器创建出来了。接下来用MachOView
打开工程的mach-o文件 :
四:lldb对类动态调试
上面我们已经通过源码,对类的结构体有了一个认知,但是还是停留在代码层面上的,下面我们通过lldb
对类的结构进行调试打印,去印证底层代码的逻辑。我们还是以前面创建的工程为例:
目标:我们要通过调试,去发现类的属性,成员变量,方法,分别都存在了哪里?带着这个目标我们往下看。
#import <Foundation/Foundation.h>
@interface China : NSObject
{
NSString *gdp;
}
@property (nonatomic, strong) NSString *people;
@property (nonatomic, strong) NSString *area;
- (void)countPeople;
+ (void)gdpIncrease;
@end
@implementation China
- (void)countPeople{
}
+ (void)gdpIncrease{
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
China *object = [China alloc];
// **断点位置**
}
return 0;
}
运行工程程序停留在了断点的位置,此时object对象已经创建。我们通过lldb命令打印:
(lldb) x/6xg object.class
0x100001268: 0x001d800100001241 0x0000000100afd140
0x100001278: 0x000000010122d590 0x0000000200000003
0x100001288: 0x000000010122d424 0x0000000000000000
我们先看一张表格
类成员 | 占用内存 |
---|---|
isa | 8 |
superclass | 8 |
cache | 16 |
isa
和superclass
都是指针占用8个字节,而cache
是个结构体,我们通过之前了解的内存对齐原则计算出其大小为16字节
很显然0x100001268
到0x100001278
之前的16个字节分别是isa
和superclass
,而0x100001278到0x100001288是cache缓存,为了避免篇幅过长,cache
这块我们在下篇文章去讨论,这里详细调试bits
的结构。
调试bits
我们需要从类的首地址平移0x20
也就是0x100001288
是bits
的首地址,接下来打印一下:
// 这里需要把地址强转成class_data_bits_t *类型
(lldb) p (class_data_bits_t *)0x100001288
(class_data_bits_t *) $4 = 0x0000000100001288
// 根据源码的函数调用返回class_rw_t结构体
(lldb) p $4->data()
(class_rw_t *) $5 = 0x000000010122d420
// 取一下结构体的值
(lldb) p *$5
(class_rw_t) $6 = {
flags = 2148139008
version = 0
ro = 0x00000001000011d8
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000010b0
arrayAndFlag = 4294971568
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x00000001000011b0
arrayAndFlag = 4294971824
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = NSDate
demangledName = 0x0000000000000000
}
// 我们先研究下properties
(lldb) p $6.properties
(property_array_t) $8 = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x00000001000011b0
arrayAndFlag = 4294971824
}
}
}
// 打印下数量
(lldb) p $8.list->count
(uint32_t) $10 = 2
//看看里面都有什么
(lldb) p $8.list[1]
(property_list_t) $13 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 3970
count = 1
first = (name = "T@\"NSString\",&,N,V_area", attributes = "")
}
}
(lldb) p $8.list[0]
(property_list_t) $14 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 2
first = (name = "people", attributes = "T@\"NSString\",&,N,V_people")
}
}
// 我们看到area和people都存在了properties中,可是gdp这个成员变量呢?我们先保留这个疑问继续往下探索
// 打印一下methods
(lldb) p $6.methods
(method_array_t) $15 = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x00000001000010b0
arrayAndFlag = 4294971568
}
}
}
// 看一下方法的数量
(lldb) p $15.count()
(uint32_t) $17 = 6 // 里面有6个方法
// 看下都有哪些方法
(lldb) p $15.list
(method_list_t *) $19 = 0x00000001000010b0
(lldb) p $19->get(0)
(method_t) $20 = {
name = "countPeople"
types = 0x0000000100000f3a "v16@0:8"
imp = 0x0000000100000cf0 (LMTest`-[China countPeople] at main.m:21)
}
(lldb) p $19->get(1)
(method_t) $21 = {
name = "setPeople:"
types = 0x0000000100000f4a "v24@0:8@16"
imp = 0x0000000100000d20 (LMTest`-[China setPeople:] at main.m:13)
}
(lldb) p $19->get(2)
(method_t) $22 = {
name = ".cxx_destruct"
types = 0x0000000100000f3a "v16@0:8"
imp = 0x0000000100000dc0 (LMTest`-[China .cxx_destruct] at main.m:19)
}
(lldb) p $19->get(3)
(method_t) $23 = {
name = "people"
types = 0x0000000100000f42 "@16@0:8"
imp = 0x0000000100000d00 (LMTest`-[China people] at main.m:13)
}
(lldb) p $19->get(4)
(method_t) $24 = {
name = "setArea:"
types = 0x0000000100000f4a "v24@0:8@16"
imp = 0x0000000100000d80 (LMTest`-[China setArea:] at main.m:14)
}
(lldb) p $19->get(5)
(method_t) $25 = {
name = "area"
types = 0x0000000100000f42 "@16@0:8"
imp = 0x0000000100000d60 (LMTest`-[China area] at main.m:14)
}
//我们通过上边的打印看到
1、我们自己写的对象方法
2、C++构造方法
3、是@property 自动生成的属性的get和set方法
到此为止总结一下,类的结构存储了isa
、superClass
、cache
和bits
,对象方法和属性都存在bits.data()
中的methods
和properties
,那成员变量和类方法都去哪里了?不着急我们继续探索下ro
的结构
探索ro
:
(lldb) p $6.ro
(const class_ro_t *) $7 = 0x00000001000011f8
(lldb) p *$7
(const class_ro_t) $8 = {
flags = 388
instanceStart = 8
instanceSize = 32
reserved = 0
ivarLayout = 0x0000000100000edc "\x03"
name = 0x0000000100000ed6 "China"
baseMethodList = 0x00000001000010d0
baseProtocols = 0x0000000000000000
ivars = 0x0000000100001168
weakIvarLayout = 0x0000000000000000
baseProperties = 0x00000001000011d0
}
// 我们来看下baseProperties都存了哪些
(lldb) p $8.baseProperties
(property_list_t *const) $9 = 0x00000001000011d0
(lldb) p *$9
(property_list_t) $10 = {
entsize_list_tt<property_t, property_list_t, 0> = {
entsizeAndFlags = 16
count = 2
first = (name = "people", attributes = "T@\"NSString\",&,N,V_people")
}
}
(lldb) p $10.get(0)
(property_t) $11 = (name = "people", attributes = "T@\"NSString\",&,N,V_people")
(lldb) p $10.get(1)
(property_t) $12 = (name = "area", attributes = "T@\"NSString\",&,N,V_area")
// 奇怪了为什么还是只有area和people呢,gdp去哪里了?
// 这个ivars看起来比较可疑,我们看一下
(lldb) p $8.ivars
(const ivar_list_t *const) $13 = 0x0000000100001168
(lldb) p *$13
(const ivar_list_t) $14 = {
entsize_list_tt<ivar_t, ivar_list_t, 0> = {
entsizeAndFlags = 32
count = 3
first = {
offset = 0x0000000100001248
name = 0x0000000100000f24 "gdp"
type = 0x0000000100000f51 "@\"NSString\""
alignment_raw = 3
size = 8
}
}
}
(lldb) p $14.get(0)
(ivar_t) $15 = {
offset = 0x0000000100001248
name = 0x0000000100000f24 "gdp"
type = 0x0000000100000f51 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $14.get(1)
(ivar_t) $16 = {
offset = 0x0000000100001250
name = 0x0000000100000f28 "_people"
type = 0x0000000100000f51 "@\"NSString\""
alignment_raw = 3
size = 8
}
(lldb) p $14.get(2)
(ivar_t) $17 = {
offset = 0x0000000100001258
name = 0x0000000100000f30 "_area"
type = 0x0000000100000f51 "@\"NSString\""
alignment_raw = 3
size = 8
} // 原来gdp在这里,而且还有属性生成的_area,_people两个成员变量
// 我们在看下方法baseMethodList
(lldb) p $8.baseMethodList
(method_list_t *const) $18 = 0x00000001000010d0
(lldb) p *$18
(method_list_t) $19 = {
entsize_list_tt<method_t, method_list_t, 3> = {
entsizeAndFlags = 26
count = 6
first = {
name = "countPeople"
types = 0x0000000100000f36 "v16@0:8"
imp = 0x0000000100000cd0 (LMTest`-[China countPeople] at main.m:24)
}
}
}
(lldb) p $19.get(0)
(method_t) $20 = {
name = "countPeople"
types = 0x0000000100000f36 "v16@0:8"
imp = 0x0000000100000cd0 (LMTest`-[China countPeople] at main.m:24)
}
(lldb) p $19.get(1)
(method_t) $21 = {
name = "setPeople:"
types = 0x0000000100000f46 "v24@0:8@16"
imp = 0x0000000100000d10 (LMTest`-[China setPeople:] at main.m:13)
}
(lldb) p $19.get(2)
(method_t) $22 = {
name = ".cxx_destruct"
types = 0x0000000100000f36 "v16@0:8"
imp = 0x0000000100000db0 (LMTest`-[China .cxx_destruct] at main.m:22)
}
(lldb) p $19.get(3)
(method_t) $23 = {
name = "people"
types = 0x0000000100000f3e "@16@0:8"
imp = 0x0000000100000cf0 (LMTest`-[China people] at main.m:13)
}
(lldb) p $19.get(4)
(method_t) $24 = {
name = "setArea:"
types = 0x0000000100000f46 "v24@0:8@16"
imp = 0x0000000100000d70 (LMTest`-[China setArea:] at main.m:14)
}
(lldb) p $19.get(5)
(method_t) $25 = {
name = "area"
types = 0x0000000100000f3e "@16@0:8"
imp = 0x0000000100000d50 (LMTest`-[China area] at main.m:14)
}
// 和rw_t中的一样,那么我们的类方法呢?
研究到这里我们的属性,成员变量,对象方法都在内存里面找到了,可是类方法仍然没有头绪。类方法究竟是怎样存储的,接下来我们用 Runtime 的 API 来实际测试一下。
首先我们在测试项目里面添加一个函数:
void testInstanceMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getInstanceMethod(pClass, @selector(gdpIncrease));
Method method2 = class_getInstanceMethod(metaClass, @selector(gdpIncrease));
NSLog(@"%p-%p",method1,method2);
NSLog(@"%s",__func__);
}
然后在main()
里面调用这个函数:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
China *object = [China alloc];
testInstanceMethod_classToMetaclass([object class]);
}
return 0;
}
运行之后我们看控制台打印:
2020-01-16 20:22:15.724385+0800 LMTest[55630:25157174] 0x0-0x1000010d0
2020-01-16 20:22:15.724493+0800 LMTest[55630:25157174] testInstanceMethod_classToMetaclass
首先 testInstanceMethod_classToMetaclass
方法测试的是分别从类和元类去获取类方法的结果。由打印结果我们可以知道:
对于元类对象来说,gdpIncrease
是元类对象的实例方法,所以存在元类中。
我们把函数内部的class_getInstanceMethod改成class_getClassMethod在进行测试
void testInstanceMethod_classToMetaclass(Class pClass){
const char *className = class_getName(pClass);
Class metaClass = objc_getMetaClass(className);
Method method1 = class_getClassMethod(pClass, @selector(gdpIncrease));
Method method2 = class_getClassMethod(metaClass, @selector(gdpIncrease));
NSLog(@"%p-%p",method1,method2);
NSLog(@"%s",__func__);
}
2020-01-16 20:32:47.612936+0800 LMTest[55674:25167897] 0x1000010d0-0x1000010d0
2020-01-16 20:32:47.613052+0800 LMTest[55674:25167897] testInstanceMethod_classToMetaclass
WHAT?为什么类和元类都会有值?可是我们在类结构中并没有发现呀?
为了解开谜题我进入了class_getClassMethod
这个函数内部看看实现:
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
Class getMeta() {
if (isMetaClass()) return (Class)this;
else return this->ISA();
}
很明显,class_getClassMethod
方法底层其实调用的是 class_getInstanceMethod
,而 cls->getMeta() 方法底层就是通过判断最后返回的元类。这样就可以理解为什么调用class_getClassMethod
类和元类都会有结果了。
通过lldb可以对元类的内存进行打印,最终找到类方法:
1:通过isa()
找到元类
2:找到元类的ro
3:打印baseMethodsList
具体过程笔者就不在赘述。
五:总结
-
万物皆对象,类也是对象
-
类的本质是一个结构体
-
属性 =
getter
+setter
+ _成员变量 -
class_rw_t
是在运行时来拓展类的一些属性、方法和协议等内容,class_ro_t
是在编译时就已经确定了的,存储的是类的成员变量、属性、方法和协议等内容 -
类和元类的创建时机是在编译期
-
类在
class_ro_t
结构中存储了编译时确定的属性、成员变量、方法和协议等内容 -
实例方法存在类中而类方法存在该类的元类中
我们完成了对 iOS
中类的底层探索,可是还是有个遗留,就是类中的cache
,也就是方法缓存,在下篇文章中我会和大家一起探索,感谢阅读~