[iOS 理解] 内存管理 自动释放池

ARC 无需显式调用 retain release autorelease
编译器在正确的位置加上管理对象引用计数的函数。

对象的所有权修饰符
__strong:赋值时,旧的 release,新的 retain
__weak:需要操作 weak 表,单独分析
__unsafe_unretained:当作是纯C语言指针的赋值
__autoreleasing:会把对象加入自动释放池,文章最后有一个经典案例

内存管理核心思想:谁创建谁释放

最底层函数实现

retain 使对象的引用计数+1
尝试在bits里计数+1,如果溢出,把 bits 最大容量的一半(RC_HALF)存入 sidetable
bits 留一半(防止retain release 频繁操作 sidetable),保存bits

release 使对象的引用计数 -1 或 dealloc
尝试 bits 里计数 -1。如果没有 underflow(旧 bits 为 0 则 underflow),保存 bits
如果 underflow:
	如果bits 的 has_sidetable_rc 位为0,即 sidetable没有计数
		则设置 deallocating 位为 1,不需要保存新bits,旧 bits 里计数仍然是 0,
		发消息 dealloc 对象
	如果 has_sidetable_rc, 尝试从 sidetable 中减少 RC_HALF 个给 bits
		如果减少了0个,说明 sidetable 中之前存过,但已经取光了,则同上 dealloc 对象,
		如果减少了 RC_HALF 个,则保存 bits 中计数新值为 (RC_HALF - 1)

bits 中保存计数值的变量名叫 extra_rc,一个对象 alloc 之后,extra_rc 为 0。
MRC 中获取 retainCount 的函数,返回值是 1 + extra_rc。

autorelease 之前,先学 AutoreleasePool
AutoreleasePool 之前,要先学习 runloop
runloop 另一篇文章学过了,AutoreleasePool 在下面,先继续内存管理

autorelease 
主线程的 runloop 里有创建池、释放池的步骤
所以一般只需要 把对象加入自动释放池 即可。
自动释放池释放时给对象发送 release。
autorelease ARC 下编译优化

对象作为函数的返回值时,编译器会自动将其注册到自动释放池,即帮忙调用 autorelease
autorelease 开销很大,因为管理自动释放池可能会分配页、执行很多代码。怎么优化呢?

观察以下 ARC 代码

+ (instancetype)createSark {
   return [self new]; // 编译器应该会插入 autorelease 
}
Sark *sark = [Sark createSark]; // 编译器应该会调用 retain
// 引用计数现在应该为 2
// 结束时编译器应该会调用 objc_storeStrong(&sark, nil); 即 release 一次

实际优化后:

+ (instancetype)createSark {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 类似 autorelease  
} 
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 类似 retain
Sark *sark = tmp; // 引用计数为 1
objc_storeStrong(&sark, nil); // 相当于 release

在返回值身上调用 objc_autoreleaseReturnValue,直接返回 object(不调用autorelease);同时,在外部接收这个返回值的 objc_retainAutoreleasedReturnValue里,直接返回这个object(不调用retain)。
免去了对返回值的内存管理。

这要求调用者 和 被调者 配合完成,一方不配合就得按照第一种方式完成。
对于 alloc,new,copy,mutableCopy,及他们开头的函数,调用者一定不配合(被调者可能配合)
如果被调用者内部操作很复杂,也不会配合,例如
[NSMutableArray array] 内部会配合,因为内部返回 alloc init
[NSMutableArray arrayWithObjects:@“obj”, nil] 内部不会配合,即内部会调用真的 autorelease。

AutoreleasePool

这篇文章讲了核心概念,细节没讲
下面是我的文章,最好结合源码看。


概览

AutoreleasePool 没有单独的结构,是一个 AutoreleasePoolPage 的双向链表
一个线程 - 一个 runloop - 一个 AutoreleasePool - 一个 AutoreleasePoolPage 双向链表
如果有的话。

AutoreleasePoolPage

为这个类的对象申请内存时,申请的是物理页大小,4KB,里面保存:
1 自己的实例变量
2 autorelease 的对象数组,id 的数组

每当一个对象想 autorelease,就加入 page 中,满了就申请一个新 page,形成双向链表。

自动释放原理,很简单

如果创建一个池子,就在最新 page 的链表里加入一个哨兵 = 0,返回插入的位置,记录下来。
如果一个对象想 autorelease,就加入一个 id。想释放池子时,就根据记录的哨兵的位置,一直到最新插入的位置,给中间对象全部发送 release 消息,更新链表、最新位置即可。

@autoreleasepool {	
	…
} 
等价于
void* pool = objc_autoreleasePoolPush(); // 返回值就是哨兵
…       
objc_autoreleasePoolPop(pool); // 释放池子

因此池子可以嵌套。

细节

双向链表当前节点 即当前 page,其指针是保存在线程变量中的。
可以理解为一个线程范围的全局变量,称为 hotPage。

在第一个池子创建时
objc_autoreleasePoolPush()
	ret AutoreleasePoolPage::push()
		ret autoreleaseFast(0)
			ret autoreleaseNoPage(0)
				ret setEmptyPoolPlaceholder()  
				// hotPage 保存一个地址 1,并不真的创建 page,返回 1
				// 而且,连第一个池子的哨兵都没加,因为池子还没建

也就是说,如果一个释放池是初次创建,那就先不实际创建,看看是否加入对象,加入时再创建。

第一个对象加入
[self autorelease]
	_objc_rootAutorelease(self) // 中间省略两步没用的
		AutoreleasePoolPage::autorelease(self);
			autoreleaseFast(obj);
				取出 hotPage,发现是假 page,创建真 page 并维护双向链表、更新 hotPage
				如果之前是假 page,别忘了加入之前池子的哨兵0
				
				加入 obj

第一个加入的如果不是对象,是直接嵌套一个池子,
则 autoreleaseFast(0) ,其余和上面一样,obj = 0 而已。
autoreleaseFast 返回值是加入的地址,保存起来。

后续加入

取出 hotPage,发现没满,就加入;
page 满了就看 page 循环链表有没有下一个,没有的话创建,有的话使用:
维护循环列表、更新 hotPage。
加入 page

释放池子

coldPage 是 hotPage 最远的祖先;
因为池子可以嵌套(最外层的池子的哨兵地址是 1,最特殊,内层池子哨兵地址正常地址),
所以释放时,如果是内层的池子:

objc_autoreleasePoolPop(void *token)  // 参数是保存的哨兵地址
	AutoreleasePoolPage::pop(token);
		从 token 所在的页的位置,一直到最新页,所有对象都要 release
		所以先找到 token 所在的页。
		因为 malloc 4KB 时系统调用返回的地址是 4K 的倍数,这个是操作系统特性
		内存页就是一页 4KB,这样地址后12位关闭就是所在的页地址。
		得到了 token 所在的页和 token
		从 hotPage 开始,直到 token 页内 token 地址,
		之间的对象全部发送 release 消息,更新 hotPage
		
		如果此时 token 页内剩下的空间超过一半,则释放后面所有的页
		如果超过一半,释放孙子页及后面所有的页  

如果是最外层池子:
如果池子没用过,也就是说 page 没创建过,更新 hotPage 为 nil;
如果用过了, 根据上面的逻辑,要么还有一页,要么两页,不管怎样,只有第一页有内容,先释放掉这些内容 ,然后如果有第二页的话,释放,保留第一页。结束。
最外层的池子,的确把内容 release 了,但 page 不释放了,同时哨兵还在。

因为有可能当前线程过一会又需要用释放池了,然后想创建一个池子(最外层的池子)
这时回想前面创建池子过程:取出 hotPage,发现是真 page!直接存哨兵就好了!
这时创建的池子本质上是第二层,返回的哨兵地址是正常地址。

池子最后剩下一页怎么回收?
线程结束时,会销毁线程变量,也就是前面说过 hotPage 的存储位置。
线程变量在创建时,要求提供一个销毁函数,线程结束时会调用,以销毁可能很复杂的自定义的线程变量。
于是在这个函数里回收最后的 page。

应用

官方给了三个应用

1 最主要的应用还是,降低内存占用峰值
一个函数内循环创建大量临时对象,函数结束后并没有回收,而是 runloop 进入等待状态之前回收
所以会增加内层峰值,并不会泄露。
所以系统容器类的 block 版本的枚举器(enumerateWithBlock 类似方法)内会自动添加 AutoreleasePool。
自己写的循环就手动使用释放池包围内层循环吧。

2 If you spawn a secondary thread. 生成子线程。
创建的子线程内,没有自动释放池!所以 autorelease 的对象无法释放,问题很大
所以要创建自动释放池环绕着。想起另一个问题,子线程中使用 timer,由于子线程的 runloop 需要手动获取后创建,因此需要获取子线程 runloop 后再使用 timer;
dispatch async 内 会自动环绕 autoreleasepool

3 If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.
子线程内如果没使用 cocoa 框架内的东西,那就不涉及 objc_object 的引用计数问题,肯定就不需要自动释放池了。

4 题目:找出代码中的问题

- (BOOL)validateDictionary:(NSDictionary *)dict with:(Checker *)checker error:(NSError **) error {
   __block BOOL isValid = YES;
  [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
    if([checker checkObject:obj forKey:key]) return;
    *stop = YES;isValid = NO;
    if(error) *error = [NSError errorWithDomain:NSCocoaErrorDomain code:1 userInfo:0];
  }];
  return isValid;
}

编译是可以通过的,有一处不规范,有一处导致错误结果。
1 对象的指针,得用 __autoreleasing 修饰,即参数 NSError * __autoreleasing * 这点并不致命
2 这个闭包内是自带释放池的,而 *error 创建后 autorelease,因为它是 __autoreleasing 对象,离开释放池后被释放,所以 error 会成为野指针。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值