前面章节介绍了内存的分配与释放机制,没有从基类以及子类的视角出发,本节将从这个角度,梳理类在继承体系中的内存管理。
首先,来研究一下类成员变量在内存中的布局。
// .h文件
@interface ObjectA1 : NSObject
@property (nonatomic) NSInteger i1;
@property (nonatomic) NSInteger i2;
@property (nonatomic) NSInteger i3;
@property (nonatomic) NSInteger i4;
@end
// .m文件
@implementation ObjectA1
- (instancetype)init {
if (self = [super init]) {
_i1 = 0x1001;
_i2 = 0x2002;
_i3 = 0x3003;
_i4 = 0x4004;
}
return self;
}
- (void)memoryTest {
ObjectA1 *a1 = [[ObjectA1 alloc] init];
NSLog(@"%@",a1);
}
在NSLog处设置断点,用memory read命令读取该对象的内存,如下:
(lldb) po a1
<ObjectA1: 0x60400025d2b0>
(lldb) memory read 0x60400025d2b0
0x60400025d2b0: 40 e0 99 0c 01 00 00 00 01 10 00 00 00 00 00 00 @...............
0x60400025d2c0: 02 20 00 00 00 00 00 00 03 30 00 00 00 00 00 00 . .......0......
(lldb) memory read 0x60400025d2d0
0x60400025d2d0: 04 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .@..............
现在调整ObjectA1对象的声明,改成如下:
@interface ObjectA1 : NSObject
@property (nonatomic) NSInteger i2;
@property (nonatomic) NSInteger i1;
@property (nonatomic) NSInteger i4;
@property (nonatomic) NSInteger i3;
@end
再次读取a1对象的内存,结果变成了:
(lldb) po a1
<ObjectA1: 0x604000056920>
(lldb) memory read 0x604000056920
0x604000056920: 40 70 da 05 01 00 00 00 02 20 00 00 00 00 00 00 @p....... ......
0x604000056930: 01 10 00 00 00 00 00 00 04 40 00 00 00 00 00 00 .........@......
(lldb) memory read 0x604000056930
0x604000056930: 01 10 00 00 00 00 00 00 04 40 00 00 00 00 00 00 .........@......
(lldb)
仔细观察,不难发现,类成员变量在内存中的布局,由其声明的顺序决定。编译器根据类成员变量的声明顺序,确定相应的偏移指针值。
下面来看继承时的内存分配,如下代码:
@interface ObjectA1 : NSObject
@property (nonatomic) NSInteger i1;
@property (nonatomic) NSInteger i2;
@end
@interface ObjectA2 : ObjectA1
@property (nonatomic) NSInteger i3;
@property (nonatomic) NSInteger i4;
@end
@implementation ObjectA1
- (instancetype)init {
if (self = [super init]) {
_i1 = 0x1001;
_i2 = 0x2002;
}
return self;
}
@end
@implementation ObjectA2
- (instancetype)init {
if (self = [super init]) {
_i3 = 0x3003;
_i4 = 0x4004;
}
return self;
}
@end
- (void)memoryTest {
ObjectA2 *a2 = [[ObjectA2 alloc] init];
NSLog(@"%@",a2);
}
在NSLog处设置断点,查看a2的内存布局,如下:
(lldb) memory read 0x608000055a50
0x608000055a50: 80 f0 d1 04 01 00 00 00 01 10 00 00 00 00 00 00 ................
0x608000055a60: 02 20 00 00 00 00 00 00 03 30 00 00 00 00 00 00 . .......0......
(lldb) memory read 0x608000055a70
0x608000055a70: 04 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .@..............
由上可知在内存中,基类的成员变量会放在本类的成员变量“前面”。假如不调用基类的初始化方法,会发生啥呢?
@implementation ObjectA2
- (instancetype)init {
if (self) {
_i3 = 0x3003;
_i4 = 0x4004;
}
return self;
}
(lldb) po a2
<ObjectA2: 0x60000004e640>
(lldb) memory read 0x60000004e640
0x60000004e640: 78 40 d7 02 01 00 00 00 00 00 00 00 00 00 00 00 x@..............
0x60000004e650: 00 00 00 00 00 00 00 00 03 30 00 00 00 00 00 00 .........0......
(lldb) memory read 0x60000004e660
0x60000004e660: 04 40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .@..............
编译器给出了警告,执行后,可见为基类的成员变量分配了空间,布局不受影响,变化只是没有调用基类的初始化方法给成员变量赋初值。
综上,在给对象分配空间时,一定是先分配基类的存储区域,然后分配子类的存储区域,基类存储区域在头部,子类存储区域在尾部。初始化该区域时,没有严格的先后顺序,可以先初始化子类再初始化基类,只不过这样做只有坏处没有好处,除非有非常特殊的目的,否则都应该先初始化基类的变量,再初始化子类。
那么释放是如何进行的呢?其过程和初始化刚好相反。当对对象调用release方法后,如果引用计数为0,会调用对象的dealloc方法回收内存,一般,先释放自己的成员变量,然后调用[super dealloc]释放基类的成员变量。
最后,编译器并不会强制要求对象的初始化和释放顺序,只是给出了警告,但我们为什么要遵循先初始化基类、再初始化子类,先释放子类、再释放基类这种顺序呢?主要原因是子类可能会使用基类的成员变量。如果不按照这种顺序,在初始化时如果子类用到了基类的数据,就可能会出错;同理,如果先释放了基类的数据,再释放子类的数据时,如果还要用到基类数据,就可能会出错或导致crash。