autorelease
在MRC
时代,我们如果希望一个对象延迟释放的时候,通常会把这个对象标记为autorelease
, 如
NSString *str = [[[NSString alloc] initWithString:@"hello"] autorelease];
后来在ARC
的时候,我们甚至不用关心一个对象是什么时候release
的, 系统总是能够在合适的时候帮我们去释放这个对象, 背后它究竟做了什么?
ARC和MRC下的autorelease
ARC
ARC
是苹果引入的一种自动内存管理机制,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码(release)
以及在 Runtime
做一些优化。
在ARC
的情况下,编译器会在RunLoop
休眠前执行释放的,而它能够释放的原因就是系统在每个runloop
迭代中都加入了自动释放池Push
和Pop
MRC
MRC
机制下,对一个对象标记autorelease
后,这个对象并不会马上被释放,而是当这段语句所处的 autoreleasepool
进行 drain
操作时,所有标记了 autorelease
的对象的 retainCount
会被 -1。即 release
消息的发送被延迟到 pool
释放的时候了。
autoreleasepool
ARC下
,我们使用@autoreleasepool{}
来使用一个autoreleasepool,随后编译器将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// @autoreleasepool{}中的代码
objc_autoreleasePoolPop(context);
autoreleasePoolPage的结构
class AutoreleasePoolPage
{
PAGE_MAX_SIZE;//最大size 4096字节
magic_t const magic; //用来校验AutoreleasePoolPage的结构是否完整
id *next;//指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满
pthread_t const thread;//指向当前线程,一个AutoreleasePoolPage只会对应一个线程,但一个线程可以对应多个AutoreleasePoolPage;
AutoreleasePoolPage * const parent;//指向父结点,第一个结点的 parent 值为 nil;
AutoreleasePoolPage *child;//指向子结点,最后一个结点的 child 值为 nil;
uint32_t const depth;//代表深度,第一个page的depth为0,往后每递增一个page,depth会加1;
}
autoreleasePool工作原理
autoreleasepool
本质上就是一个指针堆栈,内部结构是由若干个以AutoreleasePoolPage
对象为结点的双向链表
组成,系统会在需要的时候动态地增加或删除page节点,如下图即为AutoreleasePoolPage
组成的双向链表:
运行流程:
- 在运行循环开始前,系统会自动创建一个
autoreleasepool
(一个autoreleasepool会存在多个AutoreleasePoolPage),此时会调用一次objc_autoreleasePoolPush
函数,runtime
会向当前的AutoreleasePoolPage
中添加一个POOL_BOUNDARY
(哨兵对象),代表autoreleasepool的起始边界地址),并返回此哨兵对象
的内存地址 next指针
则会指向POOL_BOUNDARY
(哨兵对象)后面的地址(对象地址1)- 后面我们创建对象,如果对象调用了
autorelease
方法(ARC编译器会给对象自动插入autorelease),则会被添加进AutoreleasePoolPage
中,位置是在next
指针指向的位置,如上面next指向的是对象地址1,这是后添加的对象地址就在对象地址1这里,然后next就会 指向到对象地址2 ,以此类推,每添加一个地址就会向前移动一次,直到指向end()
表示已存满 - 当不断的创建对象时,
AutoreleasePoolPage
不断存储对象地址,直到存满后,则又会创建一个新的AutoreleasePoolPage
,使用child
指针和parent
指针指向下一个page
和上一个page
,从而形成一个双向链表
. - 当调用
objc_autoreleasePoolPop
(哨兵对象地址)时,假设我们如上图,添加最后一个对象地址8,那么这时候就会依次由对象地址8 -> 对象地址1,每个对象都会调用release方法释放,直到遇到哨兵对象地址为止
autoreleasepool的嵌套
当多个autoreleasepool
嵌套,对象的释放,会是什么情况呢?
每次新建一个@autoreleasepool
,就会执行一次push
操作,对应的具体实现就是往AutoreleasePoolPage
中的next
位置插入一个POOL_BOUNDARY
(哨兵对象)
@autoreleasepool {//autoreleasepool1
NSObject * obj1 = [[NSObject alloc] init];
@autoreleasepool {//autoreleasepool2
NSObject * obj2 = [[NSObject alloc] init];
NSObject * obj3 = [[NSObject alloc] init];
}
}
释放流程:
- 当autoreleasepool1创建时,会添加哨兵对象1,接着obj1的创建,则把obj1地址添加进来。
- 当autoreleasepool2创建,会添加哨兵对象2,位置是obj1后面(上面next指针指向原理),然后依次把obj2和obj3加进来。
- 当autoreleasepool2结束时,obj3,obj2,会找到离它们最近的autoreleasepool即 autoreleasepool2,然后依次调用release,直到哨兵对象2位置。
- 当autoreleasepool1结束时,当obj1调用release,直到哨兵对象1位置,
ARC与MRC下autoreleasepool的区别
MRC
下需要手动管理自动释放池的创建和释放,ARC
下只需要使用@autoreleasepool
将对应的代码包含起来即可。
MRC
下调用自动释放池release
方法后,会对autorelease
对象进行释放,因此,此后访问的属性变量为野指针,再去访问自然会导致crash。而ARC
下,@autoreleasepool
并不会立即在结束括号符后,立即释放person变量,而是会在runloop休眠前进行释放
autoreleasepool的用处
for (int i = 0; i < 100000000; i++)
{
@autoreleasepool
{
NSString* string = @"ab c";
NSArray* array = [string componentsSeparatedByString:string];
}
}
当我们需要创建和销毁大量的对象时,使用手动创建的 autoreleasepool
可以有效的避免内存峰值的出现。因为如果不手动创建的话,外层系统创建的 pool
会在整个 runloop circle
结束之后才进行 drain
,手动创建的话,会在 block
结束之后就进行 drain
操作。
如果不使用 autoreleasepool
,需要在循环结束之后释放 100000000 个字符串,如果 使用的话,则会在每次循环结束的时候都进行 release
操作。
Runloop和Autorelease
iOS在主线程的Runloop中
注册了2个Observer
第1个Observer监听了kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
第2个Observer
监听了kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
监听了kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
main.m 中 Autorelease Pool
Xcode 11前
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在Xcode11
之前,是将整个应用程序运行放在@autoreleasepool
内的,由于主线程Runloop
的存在,这个函数永远不会返回,意味着程序结束后main函数
中的autorelease对象
才会释放。
Xcode 11后
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
在Xcode 11后
,触发主线程 RunLoop
的 UIApplicationMain 函数
放在了 @autoreleasepool 外面
,这可以保证 @autoreleasepool
中的 autorelease
对象在程序启动后立即释放。正如新版本的 @autoreleasepool
中的注释所写 “Setup code that might create autoreleased objects goes here.”,这里的autoreleasepool
中的autorelease对象
在程序启动后立即释放
本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.