大家好,我是OB!今天来聊聊AutoReleasePool!
一、AutoReleasePool
1:AutoReleasePool底层结构
自动释放池,如果有大量的临时数据可以使用AutoReleasePool,能更加快捷的释放临时对象。
下面来看看他的底层结构
OC的autoreleasepool
在经过clang
编译后,变成了结构体__AtAutoreleasePool
。
struct __AtAutoreleasePool {
void * atautoreleasepoolobj;
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
};
然后查阅NSObject
源码发现了c++的对象AutoreleasePoolPage
void * objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
那么AutoreleasePoolPage
又是什么呢?继续看NSObject.mm
发现
class AutoreleasePoolPage {
# define POOL_BOUNDARY nil
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
AutoreleasePoolPage(...) {}
~AutoreleasePoolPage() {}
public:
static inline id autorelease(id obj) {}
static inline void *push(){}
static inline void pop(void *token){}
};
AutoReleasePool
是一个由AutoreleasePoolPage
为Node
组成的双向链表的数据结构。AutoreleasePoolPage
的大小是4096,也就是4k。
AutoreleasePoolPage
的大小是4096字节?
CPU是通过寻址来访问内存的。32位CPU的寻址宽度是 0~0xFFFFFFFF ,计算后得到的大小是4G,也就是说可支持的物理内存最大是4G。
但在实践过程中,碰到了这样的问题,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。
为了解决此类问题,现代CPU引入了 MMU(Memory Management Unit 内存管理单元)。
MMU 的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由 MMU 负责将虚址映射为物理地址。
MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。
内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页(page)和页帧(page frame),并保证页与页帧的大小相同。
2:AutoReleasePool方法解析
push()方法
AutoreleasePoolPage的push()方法主要是将POOL_BOUNDARY
添加到page中,放在page的开始位置,之后删除就是从hotPage的尾部删除,直到遇到POOL_BOUNDARY
的位置,表示删除完毕。
POOL_BOUNDARY 哨兵对象,其实就是地址为nil的对象
static inline void *push() {
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
static inline id *autoreleaseFast(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
id *add(id obj) {
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
哨兵对象/边界对象(POOL_BOUNDARY)
#define POOL_BOUNDARY nil
边界对象其实就是nil的别名,而它的作用事实上也就是为了起到一个标识的作用。
每当自动释放池初始化调用objc_autoreleasePoolPush方法时,总会通过AutoreleasePoolPage的push方法,将POOL_BOUNDARY放到当前page的栈顶,并且返回这个边界对象;
而在自动释放池释放调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。
pop()方法
AutoreleasePoolPage的pop()方法主要是出栈。找到hotPage,并从hotPage尾部release对象,直到遇到POOL_BOUNDARY
的位置
static inline void pop(void *token) {
AutoreleasePoolPage *page;
page = pageForPointer(token);
stop = (id *)token;
page->releaseUntil(stop);
}
void releaseUntil(id *stop) {
while (this->next != stop) {
// code ...
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
AutoReleasePool嵌套
可以通过extern void _objc_autoreleasePoolPrint(void);
去观察AutoReleasePool,
多个AutoReleasePool嵌套时,每添加一个池子,就会执行push()方法,就会添加一个哨兵对象,删除时也是删除到哨兵对象为止,表示当前内部的page释放完毕。
extern void _objc_autoreleasePoolPrint(void);
#import "OBAnimal.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
for (int i = 0; i < 10; i ++) {
OBAnimal * dog = [[[OBAnimal alloc] init] autorelease];
}
@autoreleasepool {
for (int i = 0; i < 5; i ++) {
OBAnimal * dog = [[[OBAnimal alloc] init] autorelease];
}
_objc_autoreleasePoolPrint();
}
}
return 0;
}
嵌套时,push操作会获取当前的hotpage。也就是说,嵌套的pool只会有一个链表,只是,链表中有多个哨兵对象POOL_BOUNDARY
。删除时,内部的也就是后创建的pool先删除,一直删除到创建pool插入的POOL_BOUNDARY
的位置,表示内部的pool删除完毕
objc[2012]: ##############
objc[2012]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[2012]: 17 releases pending.
objc[2012]: [0x101803000] ................ PAGE (hot) (cold)
objc[2012]: [0x101803038] ################ POOL 0x101803038
objc[2012]: [0x101803040] 0x1005267b0 OBAnimal
objc[2012]: [0x101803048] 0x100526d40 OBAnimal
objc[2012]: [0x101803050] 0x1005265c0 OBAnimal
objc[2012]: [0x101803058] 0x10050de20 OBAnimal
objc[2012]: [0x101803060] 0x100524b40 OBAnimal
objc[2012]: [0x101803068] 0x100524b50 OBAnimal
objc[2012]: [0x101803070] 0x100524b60 OBAnimal
objc[2012]: [0x101803078] 0x100524b70 OBAnimal
objc[2012]: [0x101803080] 0x100524b80 OBAnimal
objc[2012]: [0x101803088] 0x100524b90 OBAnimal //外部的pool开始删除
objc[2012]: [0x101803090] ################ POOL 0x101803090 //内部的pool销毁
objc[2012]: [0x101803098] 0x100524ba0 OBAnimal //内部的pool删除介绍
objc[2012]: [0x1018030a0] 0x100524bb0 OBAnimal
objc[2012]: [0x1018030a8] 0x100524bc0 OBAnimal
objc[2012]: [0x1018030b0] 0x100524bd0 OBAnimal
objc[2012]: [0x1018030b8] 0x100524be0 OBAnimal //内部的pool开始删除
objc[2012]: ##############
一个自动释放池只能匹配一个线程,一个线程可以有多个释放池;子线程没有自己的AutoReleasePool,里面的对象使用主线程的RunLoop创建的AutoReleasePool,除非在线程内部创建新的AutoReleasePool;
二、RunLoop和AutoReleasePool的关系
系统通过@autoreleasepool {}这种方式来为我们创建自动释放池的,一个线程对应一个runloop,系统会为每一个runloop隐式的创建一个自动释放池,所有的autoreleasePool构成一个栈式结构,在每个runloop结束时,当前栈顶的autoreleasePool会被销毁,而且会对其中的每一个对象做一次release。
运行项目,打个断点,在控制台输出po [NSRunLoop currentRunLoop]
看看当前的NSRunLoop
里面有什么?
observers = (
"<CFRunLoopObserver activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler ",
"<CFRunLoopObserver activities = 0xa0, callout = _wrapRunLoopWithAutoreleasePoolHandler"
)
我发现了两个观测者(其实还有很多其他观测者,我没有写出来),这两个观测者想干嘛?他们在观察什么?
先看看这两个观测者观察的那些状态:activities = 0x1,activities = 0xa0,对应的是
0x1 = kCFRunLoopEntry
,0xa0 = kCFRunLoopBeforeWaiting | kCFRunLoopExit
。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
原来这两个观测者是观察RunLoop
的进入,休眠,退出的状态时,回调到_wrapRunLoopWithAutoreleasePoolHandler
函数执行相应的操作。
AutoReleasePool
在主线程的Runloop中注册了2个Observer
第一个Observer
监听Runloop
的kCFRunLoopEntry
事件,然后通过回调_wrapRunLoopWithAutoreleasePoolHandler
执行objc_autoreleasePoolPush()
第二个Observer
监听了Runloop
的两个状态kCFRunLoopBeforeWaiting
和kCFRunLoopBeforeExit
。
当收到kCFRunLoopBeforeWaiting
时,调用回调_wrapRunLoopWithAutoreleasePoolHandler
执行objc_autoreleasePoolPop(),然后在执行objc_autoreleasePoolPush()
当收到kCFRunLoopBeforeExit时,回调objc_autoreleasePoolPop()
结论
在AutoReleasePool中的对象什么时候释放?
自动释放池具有延迟释放的特性;当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop结束或者作用域超出{}或者超出[pool release]时被释放。
环境 | 对象释放时刻 |
---|---|
MRC | 当Observer 收到RunLoop 进入休眠或者是退出的回调时,会执行objc_autoreleasePoolPop() 操作,释放对象 |
ARC | 在当前作用域结束时,就会释放,因为编译器自动在作用域尾部添加了[obj release] |
[pool release] | 自动释放池结束时 |
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// code...
//手动释放 自动释放池时,自动释放池会对加入他中的对象做一次release操作
[pool release];
以下代码分别在MRC和ARC环境下运行
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%s",__func__);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%s",__func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%s",__func__);
NSLog(@"111");
Animal *animal = [[[Animal alloc] init] autorelease];
NSLog(@"222");
}
MRC
收到回调后才会释放,在viewWillAppear
后,RunLoop
就回去进入休眠,就会通知Observer
,执行objc_autoreleasePoolPop()
释放对象,然后重新objc_autoreleasePoolPush()
,为什么还要push呢?因为要为下一次循环做准备。
Test_AutoReleasePool[22706:13757539] -[ViewController viewDidLoad]
Test_AutoReleasePool[22706:13757539] 111
Test_AutoReleasePool[22706:13757539] 222
Test_AutoReleasePool[22706:13757539] -[ViewController viewWillAppear:]
Test_AutoReleasePool[22706:13757539] -[Animal dealloc]
Test_AutoReleasePool[22706:13757539] -[ViewController viewDidAppear:]
ARC
作用域结束就会释放,因为编译器自动在作用域尾部添加了[obj release]
Test_AutoReleasePool[22808:13772253] -[ViewController viewDidLoad]
Test_AutoReleasePool[22808:13772253] 111
Test_AutoReleasePool[22808:13772253] 222
Test_AutoReleasePool[22808:13772253] -[Animal dealloc]
Test_AutoReleasePool[22808:13772253] -[ViewController viewWillAppear:]
Test_AutoReleasePool[22808:13772253] -[ViewController viewDidAppear:]
三、release和autorelease
release 底层可能会调用objc_msgSend(this, SEL_dealloc);
方法,也就是可能会释放该对象。
autorelease是将该对象交给AutoreleasePool
管理。
也就是说:在MRC环境下,一个对象没有调用autorelease,那么该对象不会由AutoreleasePool管理,由开发者自己调用[obj release]
去释放对象,如果不调用[obj release]
那么该对象就算写在自动释放里面也不会释放
int main(int argc, const char * argv[]) {
@autoreleasepool {
OBAnimal *cat = [[[OBAnimal alloc] init] autorelease];
cat.name = @"Tom";
OBAnimal *dog = [[OBAnimal alloc] init];
dog.name = @"jack";
//[dog release]; //不调用release,则不会释放
}
return 0;
}
打印如下
2020-07-24 11:36:42.323129+0800 Tom call -[OBAnimal dealloc]
Program ended with exit code: 0
autorelease最终会执行到 AutoreleasePoolPage::add(obj)
方法。也就是说会将对象交由AutoreleasePool
自动释放池管理。
static inline id *autoreleaseFast(id obj) {
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
在ARC环境下。编译器会将我们的对象进行管理。
- | 临时变量 | 非临时变量 |
---|---|---|
ARC | 在作用域尾部插入release:[dog release] | 在对象创建时插入autorelease:[[[OBAnimal alloc] init] autorelease] |
autorelease 会将该对象加入到离他最近的AutoreleasePool中,所以在子线程中可以加入新的自动释放池,这样可以快速释放对象。
以下代码如果不加入新的autoreleasepool,那么对象无法释放,因为RunLoop的保活,子线程不会退出,子线程的NSRunLoop没有注册observer,没有回调,对象无法释放。只有创建新的autoreleasepool就会将该对象放入新的池中,池子销毁他就会销毁。
dispatch_async(dispatch_queue_create("ob", DISPATCH_QUEUE_CONCURRENT), ^{
for (int i = 0 ; i< 10; i ++) {
@autoreleasepool {
Animal *animal = [[[Animal alloc] init] autorelease];
animal.name = @"dog";
}
}
@autoreleasepool {
Animal *animal = [[[Animal alloc] init] autorelease];
animal.name = @"person";
}
[[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
});
由于系统会为每一个runloop隐式的创建一个自动释放池
子线程 | 子线程创建了NSRunLoop | 子线程没有创建NSRunLoop |
---|---|---|
对象 | 对象加入到currentRunLoop创建的autoreleasepool中 | 对象加入到主线程的RunLoop创建的autoreleasepool中 |
对象释放 | 由于子线程中autoreleasepool没有注册observer,所以只有等到autoreleasepool release时才能释放 | 线程结束时,子线程中autoreleasepool执行[pool release],释放对象 |
结论
autoreleasepool释放对象的两个时刻:
- autoreleasepool向NSRunLoop注册了observer回调,那么runloop休眠或者退出时,清理池子
- autoreleasepool执行到[pool release]时,里面的对象执行[obj release]并回收池子
注意:系统会为每一个runloop隐式的创建一个自动释放池,只有执行[obj
autorelease]的对象,才会add到autoreleasepool中,这个对象才能被池子管理及释放。一个线程可以有多个池子,一个池子只能有一个线程。