引用自: 内存管理、自动释放池与循环引用
一、内存布局
1. 堆栈解释
- 栈(stack): 方法调用, 局部变量等, 是连续的, 高地址(0xc0000000)往低地址(0xc8048000)扩展
- 堆(heap): 通过alloc等分配的对象, 是离散的, 低地址往高地址扩展, 需要手动控制
- 未初始化数据(bss): 未初始化的全局变量等
- 已初始化数据(data): 已初始化的全局变量等
- 代码段(text): 程序代码
2. 64bit和32bit下long 和 char所占字节是不同的
char: 1字节(ASCII 2⁸ = 256个字符)
char*(即指针变量): 4个字节(32位的寻址控件是2³², 即32个bit,也就是4个字节. 同理64位编译器为8个字节)
short int: 2个字节范围 -2¹⁶~>2¹⁶即 -32768~>32767
int: 4个字节 范围 -2147483648 ~> 2147483647
unsigned int: :4个字节
long: 4个字节 范围和int 一样64位下8个字节, 范围-9223372036854775808 ~ 9223372036854775807
long long: 8个字节 范围 -9223372036854775808 ~ 9223372036854775807
unsigned long long: 8个字节 最大值: 184467440737095516
float: 4个字节
double: 8个字节
二、内存管理方案
- taggedPointer: 储存小对象如NSNumber.
- NONPOINTER_ISA(非指针型的isa): 在64位结构下, isa指针是占64比特位的, 实际上只有30多位就已经够用了, 为了提高利用率, 剩余的比特位储存了内存管理的相关数据内容.
- 散列表: 复杂的数据结构, 包括了引用计数表和弱引用表
通过SideTables()结构来实现的, SideTables()结构下, 有很多SideTable的数据结构.
而sideTable当中包含了自旋锁, 引用计数表, 弱引用表.
SideTables()实际上市一个哈希表, 通过对象的地址来计算该对象的引用计数在哪个sideTable中
自旋锁:
- 自旋锁是"忙等"的锁
- 适用于轻量访问
引用计数表和弱引用表实际是一个哈希表,来提高查找效率
三、MRC(手动引用计数) 和ARC(自动引用计数)
1. MRC: alloc, retain, release, retainCount, autorelease, dealloc
2. ARC:
- ARC是LLVM和Runtime协作的结果
- ARC进制手动调用retain, release, retainCount, autorelease关键字
- ARC新增weak, strong关键字
3. 引用计数管理:
- alloc: 经过一系列函数调用, 最终调用了calloc函数, 这里并没有设置引用计数为1
- retain: 经过两次哈希查找, 找到其对应引用计数值, 然后将引用计数加1(实际是加偏移量)
- release: 和retain相反, 经过两次哈希查找, 找到其对应引用计数值, 然后将引用计数减1
- dealloc:
4.弱引用管理
- 添加weak变量:通过哈希算法位置查找添加. 如果查找对应位置中已经有了当前对象所对应的弱引用数组, 就把新的弱引用变量添加到数组当中; 如果没有, 就创建一个弱引用数组, 并将该弱引用变量添加到该数组中.
- 当一个被weak修饰的对象被释放后, weak对象怎么处理?
清楚weak变量,同时设置指向为nil. 当对象被dealloc释放后, 在dealloc的内部实现中, 会电泳弱引用清除的相关函数, 会根据当前对象指针查找弱引用表, 找到当前对象所对应的弱引用数组,将数组中的所有弱引用的指针都设置为nil.
5.自动释放池
在档次runloop将要结束的时候调用objc_autoreleasePoolPop, 并push进来一个新的AutoreleasePool
AutoreleasePoolPage是以栈为节点通过双向链表的形式组合而成, 是和线程一一对应的. 内部属性有parent,child对应前后两个节点,thread对应线程, next指针指向栈中下一个可填充的位置.
- AutoreleasePool实现原理?
编译器会将@autoreleasepool{}改写为:
void * ctx = objc_autoreleasePoolRush;
{}
objc_autoreleasePoolPop(ctx);
- objc_autoreleasePoolPop:
根据传入的哨兵对象找到对应位置.
给上次push操作之后添加的对象一次发送release消息.
回退next指针到正确位的位置.
四、循环引用
循环引用的实质: 多个对象相互之间有强引用,不能释放让系统回收.
如何解决循环引用?
- 避免产生循环引用, 通常是将strong引用改为weak引用.
比如在修饰属性时用weak
在block内调用对象方法时, 使用其弱引用, 这里可以使用两个宏
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
#define ST(strongSelf) __strong __typeof(&*self)strongSelf = weakSelf;
还可以使用__block来修饰变量
在MRC下,__block不会增加引用计数, 避免了循环引用
在ARC下,__block修饰对象会被强引用,无法避免循环引用, 需要手动解除
2.在适合时机去手动断开循环引用.
通常我们使用第一种.
循环引用场景:
- 自循环引用
对象强持有的属性同时持有该对象 - 相互循环引用
- 多循环引用
1. 代理(delegate)循环引用属于相互循环引用
delegate是iOS开发中比较常遇到的循环引用, 一般在声明delegate的时候都要使用弱引用weak,或者assign,当然怎么选择使用assign还是weak, MRC的话只能用assign, 在ARC的情况下最好使用weak, 因为weak修饰的变量在释放后自动执行nil,防止野指针存在
2. NSTimer循环引用属于相互循环使用
在控制器内, 创建NSTimer作为其属性, 由于定时器创建后也会强引用改控制器对象, 那么该对象和定时器就相互循环引用了.
3. block循环引用
一个简单的例子:
@property(copy, nonatomic) dispatch_block_t myBlock;
@property(copy, nonatomic) NSString *blockString;
- (void)testBlock
{
self.myBloc = ^()
{
NSLog(@"%@",self.blockString);
};
}
由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用.
解决方案就是使用__weak修饰self即可
__weak typeof(self) weakSelf = self;
self.myBlock = ^()
{
NSLog(@"%@",weakSelf.blockString);
}
- 并不是所有block都会造成循环引用.
只有被强引用了的block才会产生循环引用
而比如下边这些系统方法等
dispatch_async(dispatch_get_main_queue(),^{
[UIView animateWithDuration:1 animations:^{
}]
}),
或者block并不是其属性而是临时变量,即栈block
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block
{
block();
}
还有一种场景, 在block执行开始时self对象还未被脂肪, 而执行过程中,self被释放了, 由于是用weak修饰的,那么weakSelf也被释放了,此时在block里访问weakSelf时,就可能会发生错误(向nil对象发消息并不会崩溃,但也没任何效果).
对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有, block执行结束后, 解除其持有.
__weak typeof(self) weakSelf = self;
self.myBlock = ^()
{
__strong __typeof(self) strongSelf = weakSelf;
[strongSelf test];
}