我们这里讨论类的结构,我们先定义2个类Strudent
和Person
,Strudent
继承自Person
,Person
继承自NSObject
。
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <malloc/malloc.h>
# define ISA_MASK 0x00007ffffffffff8ULL
@interface Person : NSObject{
NSString *nickname;
}
@property (nonatomic, copy)NSString *name;
-(void)eat;
+(void)drink;
@end
@implementation Person
-(void)eat{
NSLog(@"eat");
}
+(void)drink{
NSLog(@"drink");
}
@end
@interface Student : Person
@end
@implementation Student
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
Student *student = [[Student alloc]init];
Person *person = [[Person alloc]init];
NSLog(@"%@ - %@",student,person);
}
return 0;
}
我们先用lldb
调试,看看类的在内存中的地址。
我们可以看到,
p/x 0x001d8001000022e5 & 0x00007ffffffffff8ULL
和p/x 0x00000001000022b8 & 0x00007ffffffffff8ULL
打印的结果一致。这是为什么呢?因为0x00000001000022e0
是示例对象isa
经过掩码计算后得出的类对象的地址,而0x00000001000022b8
是类对象的isa
经过掩码计算后的元类对象的地址,元类是iOS底层一个抽象的概念,由编译器自动完成,所以两个结果是相同的。
元类
1.实例对象中存放成员变量,实例对象的
isa
指向类对象
2.类对象中存放实例方法,类对象的isa
指向元类对象
3.元类对象存放类方法,元类对象的isa
指向根元类NSObject
我们可以继续往下进行lldb
的调试,得到根类NSObject
,lldb
的说明和我们上面的一样。
然后我们打印
NSObject
,这里的两个地址不一样,为什么?难道是因为底层有另外一个NSObject
对象吗?我们接下来验证一下。image.png
Class class1 = [Person class];
Class class2 = [Person alloc].class;
Class class3 = object_getClass([Person alloc]);
Class class4 = [Person alloc].class;
NSLog(@"%p",class1);
NSLog(@"%p",class2);
NSLog(@"%p",class3);
NSLog(@"%p",class4);
打印结果:
这里说明在内存中,所有的类对象只会创建一份,为什么NSObject对象的地址会不一样呢。我们继续lldb调试。
这里一样了,因为我们刚才不一样的原因是一个是
NSObject
对象,一个是NSObject
的元类对象。大家明白了吗?然后我们看看经典的isa走位图,这个图片网上都有,因为很经典,所以大家都在用。
isa流程图.png
objc_class & objc_object
为什么对象,类,元类,都有isa
呢?我们查看源码,里面有一个类型。
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
然后我们查看发现有一个继承自他的类
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
说白了,我们的NSObject
对象只是OC
帮我们封装后的记过,在底层C/C++
的实现里,是没有对象的概念的,在底层类都是struct objc_class
类型的,然后继承自objc_object
(结构体)。
上面我们说到了,实例方法存在类对象里,类方法存在元类里,那么我们怎么验证呢?
看上方源码里的类结构,第一个是被注释掉的
//Class ISA
,因为我们是有了继承的ISA
,第二个是superclass
(即NSObject
),如果是那么我们打印的第二串地址里0x00000001000022d0
,应该存放的是我们的父类信息。看下图,我们得到了验证结果,是这样的。接下来我们先补充一段内存偏移的知识,这样我们才能一步步拿到类后面的信息。内存偏移
int a = 10;
int b = 10;
NSLog(@"%d----%p",a,&a);
NSLog(@"%d----%p",b,&b);
输出结果:
ab的内存地址差了4个字节。我们再来看数组指针,
//数组指针
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个字节,地址之间相差的字节数,主要取决于存储的数据类型 - 可以通过 首地址+偏移量取出数组中的其他元素,其中偏移量是数组的下标,内存中首地址
实际移动的字节数等于 偏移量 x 数据类型字节数
所以刚才我们打印出来的NSObject
就是根据内存偏移得出来的,那么接下来我们想要知道类里面的bits
信息,我们只需要知道cache
的大小,然后让内存偏移就行了。刚才的结果可不是蒙的哦~
计算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
。所有的内容我们只需要首地址偏移32字节即可。然后看lldb
调试(下图)。(bits
的类型class_data_bits_t
)
注: x/4gx
我们拿到Person
类的首地址0x100002138+32 = 0x100002158
(16进制)-
p $1->data()
是因为OC
底层有提供bits
的data()
方法,我们可以看到方法列表的类型是class_rw_t
(class_rw_t类型图如下)
class_rw_t
我们继续lldb
调试,打印其中的属性列表,方法列表。
但是属性好像只有一个
@"nickname"
,但是我们看看我们定义的属性。
@interface Person : NSObject{
NSString *name;
}
@property (nonatomic, copy)NSString *nickname;
-(void)eat;
+(void)run;
@end
明明有两个,那么name
这个成员变量跑哪里去了呢?为什么property_list
中只有属性,没有成员变量呢?
探索成员变量的存储位置
在刚才我们查看class_rw_t
的类型的时候,我们发现了methods(),properties(),protocols()
,然后在网上,我们还有一个类型没有注意到class_ro_t
(如下图)
那么我们是不是就可以猜测,这里存放的是成员变量呢?我们继续
lldb
调试。在里面,我们成功找到了
name
。知道了属性和成员变量的存储位置,那么接下来我们探讨方法的存储。
探索方法列表methods_list
刚才我们lldb
调试的是properities()
,这次我们用methods()
。
我们成功的找到了实例方法,
eat()
,我们继续往下看看方法列表里都放了什么方法。go on lldb
我们找到了很多方法,比如
eat(),cxx_destruct(),nickname
的getter
和setter
方法。但是好像没有我们上面自己定义的类方法,run()
。所以他应该不存在这里。很简单,我们验证我们上面的说法,究竟是不是放在元类里呢?继续lldb
呗?还能咋地?OK,看到了没,类方法已经被我们找到了。接下来我们来总结一下。
总结:
-
objc_object是我们OC底层实现对象的基类,里面重要的数据类型就是,
Class ISA,Class superclass,cache_t cache,class_data_bits_t bits,
重要的信息比如属性列表,方法列表,协议列表都放在bits
这里。 -
通过
{}
定义的属性没有set
和get
方法,存放在bits --> data() -->ro() --> ivars
获取成员变量列表 -
通过
@ property
定义的属性,存放在bits --> data() -->() --> list
获取成员属性列表 -
方法在底层的类型是
class_rw_t
类型,在class_rw_t
的实现内部,我们又发现了类方法的类型是class_ro_t
的类型。 -
类的实例方法存储在类的
bits
属性中,通过bits --> methods() --> list
获取实例方法列表,例如Person
类的实例方法eat
就存储在Person
类的bits
属性中,类中的方法列表除了包括实例方法,还包括属性的set
方法和get
方法 -
类的类方法存储在元类的
bits
属性中,通过元类bits --> methods() --> list
获取类方法列表,例如Person
中的类方法run
就存储在Person类的元类(名称也是Person)的bits
属性中
网友评论