准备工作
创建一个LGPerson类并且创建一个LGTeach类继承自LGPerson类
@interface LGPerson : NSObject {
NSString * hobby;
}
@property (nonatomic, strong) NSArray *array1;
- (void)sayHello;
+ (void)eat;
@end
@implementation LGPerson
- (void)sayHello { }
+(void)eat { }
@end
@interface LGTeracher : LGPerson
@end
@implementation LGTeracher
@end
然后在main函数LGPerson创建一个对象p和LGTeacher创建teacher对象如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
LGPerson *p = [LGPerson alloc];
LGTeracher * teacher = [LGTeracher alloc];
NSLog(@"%@-----%@",p,teacher);
}
return 0;
}
通过lldb调试如下图
根据调试过程,我们产生了一个疑问:
为什么上图中的p/x 0x001d80010000336d & 0x00007ffffffffff8ULL
与 p/x 0x0000000100003340 & 0x00007ffffffffff8ULL
中的类信息打印出来都是LGTeracher
?
0x001d80010000336d
是teacher
对象的isa
指针地址,其&mask
后得到的结果是 创建teacher
的类LGTeracher
0x0000000100003340
是isa中获取的类信息
所指的类的isa
的指针地址,即 LGTeracher类的类 的isa指针地址
,在Apple中,我们简称LGTeracher类的类为 元类
所以,两个打印都是LGTeracher
的根本原因就是因为元类
导致的
什么是元类?
对象
的isa
指向类
,类
也是一个对象
,可以称为类对象
。元类
就是类对象
所属的类
。元类
是系统给的,其定义和创建都是由编译器
完成,在这个过程中,类
的归属来自于元类
。
- 对象的isa指向类
- 类的isa指向元类
- 元类的isa指向根元类(NSObject)
- 根元类的isa指向本身
类的信息存在几份
通过以下代码验证
//MARK:--- 分析类对象内存 存在个数
void testClassNum(){
Class class1 = [LGTeracher class];
Class class2 = [LGTeracher alloc].class;
Class class3 = object_getClass([LGTeracher alloc]);
NSLog(@"\n%p-\n%p-\n%p-\n%p", class1, class2, class3);
}
NSLog打印出来的结果如下:
0x100003368-
0x100003368-
0x100003368-
0x100003368
从结果中可以看出,打印的地址都是同一个,所以NSObject只有一份,即NSObject(根元类)在内存中永远只存在一份
isa走向分析
附上经典的isa走位图
isa
走位
isa
的走向有以下几点说明:
实例对象(Instance of Subclass)
的 isa 指向类(class)
类对象(class)
isa 指向元类(Meta class)
元类(Meta class)
的isa 指向根元类(Root metal class)
根元类(Root metal class)
的isa 指向它自己
本身,形成闭环,这里的根元类就是NSObject
superclass
走位
superclass(即继承关系)
的走向也有以下几点说明:
类 之间 的继承关系:
类(subClass)
继承自父类(superClass)
父类(superClass)
继承自根类(RootClass)
,此时的根类是指NSObject
根类
继承自 nil,所以根类
即NSObject
。
元类也存在继承,元类之间的继承关系如下:
子类的元类(metal SubClass)
继承自父类的元类(metal SuperClass)
父类的元类(metal SuperClass)
继承自根元类(Root metal Class)
根元类(Root metal Class)
继承于根类(Root class)
,此时的根类是指NSObject
【注意】实例对象之间没有继承关系,类之间有继承关系
类的结构分析
引---内存偏移
int arr[4] = {1, 3, 5, 6};
int *p = arr;
for (int i=0; i<4; i++) {
printf("p[%d] == %d\n", i, p[i]);
}
// output
p[0] == 1
p[1] == 3
p[2] == 5
p[3] == 6
通过lldb调试打印
// 1. 打印p指针
p p
(int *) $0 = 0x00007ffeefbff4b0
// 2. 打印数组的地址
p &arr
(int (*)[4]) $10 = 0x00007ffeefbff4b0
// 3. p 指向 arr 的地址
p arr
(int [4]) $3 = ([0] = 1, [1] = 3, [2] = 5, [3] = 6)
// 4. 打印数组首元素的地址
p/x &arr[0] 等于 arr 的地址
(int *) $5 = 0x00007ffeefbff4b0
// 5. 打印数组首元素的值
p *arr
(int) $11 = 1
// 6. 打印数组后续元素的值, 最好带个括号,易读性强
(lldb) p * (arr + 1)
(int) $13 = 3
(lldb) p * (arr + 3)
(int) $14 = 6
通过数组的首地址,然后拿到偏移量就可以获取到其它的元素
objc_class & objc_object
通过我们之前下载的objc781的源码我们在源码中查找得到:
总结:
objc_class继承于objc_object,objc_class中有一个公用的isa,所以所有的对象都继承于objc_object,万物皆来源于objc_object
objc_class、objc_object、isa、object、NSObject
等的整体的关系,如下图所示
类的结构分析
通过上述源码可知:
isa属性
:objc_class
继承自objc_object
,故objc_class
含有isa
属性,占8
个字节。
superclass
属性:Class
类型,Class
是由objc_class
定义的结构体,是一个指针,占8
字节
cache属性:简单从类型class_data_bits_t目前无法得知,而class_data_bits_t是一个结构体类型,结构体的内存大小需要根据内部的属性来确定,而结构体指针才是8字节
bits属性:只有首地址经过上面3个属性的内存大小总和的平移,才能获取到bits
- 计算cache类的内存大小
进入cache
类cache_t
的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中),代码如下
struct cache_t {
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
explicit_atomic _buckets;
explicit_atomic _mask;
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
explicit_atomic _maskAndBuckets;
mask_t _mask_unused;
计算前两个属性的内存大小,有以下两种情况,最后的内存大小总和都是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
所以有上述计算可知,想要获取bits
的中的内容,只需通过类的首地址平移32
字节即可
探索 属性列表,即 property_list
通过class_rw_t
源码我们查找到以下代码,结构体
中有提供相应的方法去获取 属性列表
、方法列表
等,
const method_array_t methods() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->methods;
} else {
return method_array_t{v.get()->baseMethods()};
}
}
const property_array_t properties() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->properties;
} else {
return property_array_t{v.get()->baseProperties};
}
}
const protocol_array_t protocols() const {
auto v = get_ro_or_rwe();
if (v.is()) {
return v.get()->protocols;
} else {
return protocol_array_t{v.get()->baseProtocols};
}
}
通过lldb调试如下图:
lldb指令解析:
-
p $3.properties()
打印出属性数组 -
p $4.list
获取属性数组的属性列表 -
p *$5
获取属性列表的第一个属性
通过上述的打印看,只打印出属性,没有打印出成员变量。属性与成员变量的区别就是有没有set、get
方法,那成员变量存储在哪里?
成员变量列表
通过查看objc_class
中bits
属性中存储数据的类class_rw_t
的定义发现,除了methods
、properties
、protocols
方法,还有一个ro
方法,其返回类型是class_ro_t
,通过查看其定义,发现其中有一个ivars属性,我们可以做如下猜测:是否成员变量就存储在这个ivar_list_t类型的ivars属性中呢?
通过调试得出 ivar_list_t
类型的属性中不仅包含了成员变量
还包括了我们之前的属性
列表。
实例方法列表
通过lldb调试来获取方法列表,步骤如图所示
通过p $5.methods()
获得具体的方法列表的list
结构,其中methods
也是class_rw_t
提供的方法
通过打印p *$7
的count = 4
可知,存储了4
个方法,可以通过p $8.get(i)
内存偏移的方式获取单个方法,i
的范围是0-3
如果在打印 p $8.get(4)
,获取第五个方法,也会报错,提示数组越界
但是我们发现我们声明的类方法
并不存在类
中,而实例
方法存在类
中,那么类方法
会不会存在元类
中呢?下面我们探索一下。
类方法列表
通过调试流程我们得以验证我们这之前的猜测是正确的,类方法的存储在元类的bits
中 。
总结:
通过{}
定义的成员变量,会存储在类的bits
属性中,通过bits--> data() -->ro() --> ivars
获取成员变量列表,除了包括成员变量
,还包括属性定义的成员变量
通过@property
定义的属性,也会存储在bits
属性中,通过bits --> data() --> properties() --> list
获取属性列表,其中只包含属性
类的实例方法存储在类的bits
属性中,通过bits --> methods() --> list
获取实例方法
列表,例如LGPerson
类的实例方法instanceMethod
就存储在 LGPerson
类的bits
属性中
类的类方法
存储在元类
的bits
属性中,通过元类bits --> methods() --> list
获取类方法
列表