iOS 自动释放池
https://juejin.cn/post/6844904094503567368#heading-23
ARC和MRC
苹果在 iOS 5 中引入了ARC(Automatic Reference Counting)
自动引用计数内存管理技术,通过LLVM
编译器和Runtime
协作来进行自动管理内存。LLVM
编译器会在编译时在合适的地方为 OC 对象插入retain
、release
和autorelease
代码,省去了在MRC(Manual Reference Counting)
手动引用计数下手动插入这些代码的工作,减轻了开发者的工作量。
在MRC
下,当我们不需要一个对象的时候,要调用release
或autorelease
方法来释放它。调用release
会立即让对象的引用计数减 1 ,如果此时对象的引用计数为 0,对象就会被销毁。调用autorelease
会将该对象添加进自动释放池中,它会在一个恰当的时刻自动给对象调用release
,所以autorelease
相当于延迟了对象的释放。
在ARC
下,autorelease
方法已被禁用,我们可以使用__autoreleasing
修饰符修饰对象将对象注册到自动释放池中。
自动释放池
AppKit 和 UIKit 框架在事件循环(RunLoop
)的**每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease
对象。**通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease
对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。
如何创建一个自动释放池
在MRC下,可以使用NSAutoreleasepool或@autoreleasePool,一般用autoreleasePool,比NSAutoreleasepool快 ;
在ARC下,禁止使用NSAutoreleasePool
类创建线程池,只能使用@autoreleasepool
平时创建一个main函数代码时,会发现其中有一个这个东西@autoreleasepool{}
,使用clang编译之后:@autoreleasepool{...}
被编译成了{__AtAutoreleasePool __autoreleasepool; ... }
。
这个__AtAutoreleasePool
到底是什么?
它其实是一个结构体,在创建__AtAutoreleasePool结构体变量的时候调用了objc_autoreleasePoolPush(void),销毁的时候会调动objc_autoreleasePoolPop(void *),其实构造函数和析构函数,所以我们可以看出其其实是一个C++封装的自动释放池变量,会将@autoreleasepool{...}中{}中的内容添加到自动释放池中,方便内存管理。
objectivec
struct __AtAutoreleasePool {
// 构造函数
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
// 析构函数
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
从上边的__AtAutoreleasePool
我们可以看到这两个方法objc_autoreleasePoolPush
和objc_autoreleasePoolPop
objectivec
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
我们可以看出这里又引入了新的类AutoreleasePoolPage
objectivec
class AutoreleasePoolPage {
magic_t const magic;//AutoreleasePoolPage 完整性校验
id *next;//存放下一个autorelease对象的地址
pthread_t const thread; //AutoreleasePoolPage 所在的线程
AutoreleasePoolPage * const parent;//父节点
AutoreleasePoolPage *child;//子节点
uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
uint32_t hiwat;
}
自动释放池本质上是一个由AutoreleasePoolPage
构成的双向链表,parent和child表示前趋和后继;
其中有 56 bit 用于存储AutoreleasePoolPage
的成员变量,剩下的0x100816038 ~ 0x100817000都是用来存储加入到自动释放池中的对象。**(这里存储autorelease对象的是由 id next; 构成的栈存储的)*
每一个AutoreleasePoolPage
的大小都是4096
字节(16 进制 0x1000)。
- 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
- 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY (感觉不太准确,建议看下面的)
- id *next指向了下一个能存放autorelease对象地址的区域
Runloop和Autorelease
App启动时会创建两个观察者监听主线程runloop,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
。用于创建和释放自动释放池 ;
AutoreleasePool创建
- 第一个
Observer
监视的事件是Entry(即将进入Loop)
,其回调内会调用
_objc_autoreleasePoolPush()
创建自动释放池。其 order 是-2147483647,优先级最高
,保证创建释放池发生在其他所有回调之前。
AutoreleasePool释放
- 第二个
Observer
监视了两个事件:BeforeWaiting(准备进入休眠)
时调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即将退出Loop)
时调用_objc_autoreleasePoolPop()
来释放自动释放池。这个Observer
的 order 是 2147483647,优先级最低
,保证其释放池子发生在其他所有回调之后。
(这里解释一下order,在 Cocoa 中,RunLoop Observers 是一种机制,用于监听 RunLoop 的运行状态并在特定时机执行回调。每个 Observer 都有一个 order
属性,用于指定观察者的优先级,决定了观察者的回调执行顺序。order
属性是一个整数类型的值,它决定了观察者的执行顺序。数值越小的观察者,其优先级越高,会先于数值较大的观察者执行。)
- 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的
AutoreleasePool
环绕着,所以不会出现内存泄漏。
一些面试的点
-
自动释放池的本质是
AutoreleasePoolPage
创建的双向链表,AutoreleasePoolPage
是一个结构体,是一个栈存储的页 ; -
自动释放池的
压栈
和出栈
主要是通过结构体的构造函数和析构函数调用底层的objc_autoreleasePoolPush
和objc_autoreleasePoolPop
,实际上是调用AutoreleasePoolPage
的push
和pop
两个方法。 -
AutoreleasePoolPage 对象并不是每次调用
push
操作都会创建一个新的。AutoreleasePoolPage 对象是用来管理自动释放对象的,而push
操作通常会在当前 AutoreleasePoolPage 的栈顶添加一个标记对象,比如 POOL_BOUNDARY,而不是每次都创建新的 AutoreleasePoolPage 对象。 -
push
内部调用autoreleaseFast
方法处理,主要有以下三种情况:a.当
page存在,且不满
时,调用add
方法将对象添加至page
的next
指针处,并将next
指向下一位;b.当
page存在,且已满
时,调用autoreleaseFullPage
初始化一个新的page
,然后调用add
方法将对象添加至page
栈中;c.当
page不存在
时,调用autoreleaseNoPage
创建一个hotPage
,然后调用add
方法将对象添加至page
栈中。 -
调用
pop操作
时,会传入一个值,这个值就是push
操作的返回值,即哨兵POOL_BOUNDARY
的内存地址token
。所以pop
内部的实现就是根据token
找到哨兵对象
所处的page
中,然后使用objc_release
释放token
之前的对象,并把next
指针到正确位置。
原理
objectivec
int main(int argc, const char * argv[]) {
@autoreleasepool {}
return 0;
}
通过 Clang clang -rewrite-objc main.m
将以上代码转换为 C++ 代码。
objectivec
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{ __AtAutoreleasePool __autoreleasepool; }
return 0;
}
@autoreleasepool
底层是创建了一个__AtAutoreleasePool
结构体对象;- 在创建
__AtAutoreleasePool
结构体时会在构造函数中调用objc_autoreleasePoolPush()
函数,并返回一个atautoreleasepoolobj
(POOL_BOUNDARY
存放的内存地址,下面会讲到); - 在释放
__AtAutoreleasePool
结构体时会在析构函数中调用objc_autoreleasePoolPop()
函数,并将atautoreleasepoolobj
传入。
两个函数的实现:
objectivec
// NSObject.mm
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
@autoreleasepool
底层就是使用AutoreleasePoolPage
类来实现的。
AutoreleasePoolPage类
objectivec
class AutoreleasePoolPage
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1) // EMPTY_POOL_PLACEHOLDER:表示一个空自动释放池的占位符
# define POOL_BOUNDARY nil // POOL_BOUNDARY:哨兵对象
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 用来标记已释放的对象
static size_t const SIZE = // 每个 Page 对象占用 4096 个字节内存
#if PROTECT_AUTORELEASEPOOL // PAGE_MAX_SIZE = 4096
PAGE_MAX_SIZE; // must be muliple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id); // Page 的个数
magic_t const magic; // 用来校验 Page 的结构是否完整
id *next; // 指向下一个可存放 autorelease 对象地址的位置,初始化指向 begin()
pthread_t const thread; // 指向当前线程
AutoreleasePoolPage * const parent; // 指向父结点,首结点的 parent 为 nil
AutoreleasePoolPage *child; // 指向子结点,尾结点的 child 为 nil
uint32_t const depth; // Page 的深度,从 0 开始递增
uint32_t hiwat;
......
}
- 自动释放池与线程一一对应;自动释放池(即所有的
AutoreleasePoolPage
对象)是以``AutoreleasePoolPage为结点通过
双向链表`的形式组合而成; - 每个
AutoreleasePoolPage
对象占用4096
字节内存,其中56
个字节用来存放它内部的成员变量,剩下的空间(4040
个字节)用来存放autorelease
对象的地址。
push (这里是自动释放池的push哦,别理解成autorelease对象的push)
objectivec
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); // 传入 POOL_BOUNDARY 哨兵对象
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
当创建一个自动释放池时,会调用push()
方法。push()
方法中调用了autoreleaseFast()
方法并传入了POOL_BOUNDARY
哨兵对象。
先解释一下这个POOL_BOUNDARY
哨兵对象:
-
POOL_BOUNDARY
的前世叫做POOL_SENTINEL
,称为哨兵对象或者边界对象; -
POOL_BOUNDARY
用来区分不同的自动释放池,以解决自动释放池嵌套的问题; -
每当创建一个自动释放池,就会调用
push()
方法将一个POOL_BOUNDARY
入栈,并返回其存放的内存地址;(注意,将一个POOL_BOUNDARY
入栈是作为自动释放池的边界,也就是说一个POOL_BOUNDARY对应一个自动释放池) -
当往自动释放池中添加
autorelease
对象时,将autorelease
对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY
; -
当销毁一个自动释放池时,会调用
pop()
方法并传入一个POOL_BOUNDARY
,会从自动释放池中最后一个对象开始,依次给它们发送release
消息,直到遇到这个POOL_BOUNDARY
。
总的来说:POOL_BOUNDARY称为哨兵对象或者边界对象,作为每一个自动释放池的边界,方便了自动释放池的销毁 ;
而且关于自动释放池的内存比较模糊,没找到比较好的说法,我个人总结一下:同一个线程中的自动释放池会存在于线程栈顶的同一个双向链表中,它们通过哨兵对象区分开来,不同线程间这个双向链表也是不一样的(不一定准确)
下面我们来看一下autoreleaseFast()
方法的实现:(要注意,这个方法的实现是将一个对象加入AutoreleasePoolPage为节点的双向链表中的某个节点的next中去,而这里的参数是POOL_BOUNDARY,他可以区分不同的自动释放池;这也佐证了我上面说的)
objectivec
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage(); // 双向链表中的最后一个 Page
if (page && !page->full()) { // 如果当前 Page 存在且未满
return page->add(obj); // 将 autorelease 对象入栈,即添加到当前 Page 中;
} else if (page) { // 如果当前 Page 存在但已满
return autoreleaseFullPage(obj, page); // 创建一个新的 Page,并将 autorelease 对象添加进去
} else { // 如果当前 Page 不存在,即还没创建过 Page
return autoreleaseNoPage(obj); // 创建第一个 Page,并将 autorelease 对象添加进去
}
}
autoreleaseFast()
中先是调用了hotPage()
方法获得未满的Page
,从AutoreleasePoolPage
类的定义可知,每个Page
的内存大小为4096
个字节,每当Page
满了的时候,就会创建一个新的Page
。hotPage()
方法就是用来获得这个新创建的未满的Page
。 autoreleaseFast()
在执行过程中有三种情况:
① 当前Page
存在且未满时,通过page->add(obj)
将autorelease
对象入栈,即添加到当前Page
中;
objectivec
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
page->add(obj)
其实就是将autorelease
对象添加到Page
中的next
指针所指向的位置,并将next
指针指向这个对象的下一个位置,然后将该对象的位置返回。
② 当前Page
存在但已满时,通过autoreleaseFullPage(obj, page)
创建一个新的Page
,并将autorelease
对象添加进去;
objectivec
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
// The hot page is full.
// Step to the next non-full page, adding a new page if necessary.
// Then add the object to that page.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
autoreleaseFullPage()
方法中通过while
循环,通过Page
的child
指针找到最后一个Page
。
- 如果最后一个
Page
未满,就通过page->add(obj)
将autorelease
对象添加到最后一个Page
中; - 如果最后一个
Page
已满,就创建一个新的Page
并将该Page
设置为hotPage
,通过page->add(obj)
将autorelease
对象添加进去。
③ 当前Page
不存在,即还没创建过Page
,通过autoreleaseNoPage(obj)
创建第一个Page
,并将autorelease
对象添加进去。
objectivec
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
assert(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
return page->add(obj);
}
autoreleaseNoPage()
方法中会创建第一个Page
。该方法会判断是否有空的自动释放池存在,如果没有会通过setEmptyPoolPlaceholder()
生成一个占位符,表示一个空的自动释放池。接着创建第一个Page
,设置它为hotPage
。最后将一个POOL_BOUNDARY
添加进Page
中,并返回POOL_BOUNDARY
的下一个位置。
以上就是
push
操作的实现,往自动释放池中添加一个POOL_BOUNDARY
,并返回它存放的内存地址。接着每有一个对象调用autorelease
方法,会将它的内存地址添加进自动释放池中。
autorelease (这里就是autorelease对象的push了)
autorelease
方法的函数调用栈如下:
objectivec
// NSObject.mm
① objc_autorelease
// objc-object.h
② objc_object::autorelease
// NSObject.mm
③ autorelease
④ _objc_rootAutorelease
// objc-object.h
⑤ objc_object::rootAutorelease
// NSObject.mm
⑥ objc_object::rootAutorelease2
⑦ AutoreleasePoolPage::autorelease
AutoreleasePoolPage
类的autorelease
方法实现如下:
objectivec
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
可以看到,调用了autorelease
方法的对象,也是通过以上解析的autoreleaseFast()
方法添加进Page
中。
pop (销毁一个自动释放池)
objectivec
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
pop ()方法会传入一个POOL_BOUNDARY
对应在Page
中的地址,当销毁自动释放池时,会调用pop()方法将自动释放池中的release对象全部释放**(实际上是从自动释放池的中的最后一个入栈的autorelease
对象开始,依次给它们发送一条release
消息,直到遇到这个POOL_BOUNDARY
)**
pop()
方法的执行过程如下:
① 判断token
是不是EMPTY_POOL_PLACEHOLDER
,是的话就清空这个自动释放池;
② 如果不是的话,就通过pageForPointer(token)
拿到token
所在的Page
(自动释放池的首个Page
);
③ 通过page->releaseUntil(stop)
将自动释放池中的autorelease
对象全部释放,传参stop
即为POOL_BOUNDARY
的地址;
④ 判断当前Page
是否有子Page
,有的话就销毁。
pop()
方法中释放autorelease
对象的过程在releaseUntil()
方法中,下面来看一下这个方法的实现:
objectivec
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
id obj = *--page->next; // next指针是指向最后一个对象的后一个位置,所以需要先减1
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
releaseUntil()
方法其实就是通过一个while
循环,从最后一个入栈的autorelease
对象开始,依次给它们发送一条release
消息,直到遇到这个POOL_BOUNDARY
。
有关这里push和pop,以及release方法的小结
小结:
push
操作是往自动释放池中添加一个POOL_BOUNDARY
,并返回它存放的内存地址;- 接着每有一个对象调用
autorelease
方法,会将它的内存地址添加进自动释放池中。 pop
操作是传入一个POOL_BOUNDARY
的内存地址,从最后一个入栈的autorelease
对象开始,将自动释放池中的autorelease
对象全部释放(实际上是给它们发送一条release
消息),直到遇到这个POOL_BOUNDARY
。
首先要明确,这里研究的push和pop指的是自动释放池的创建与销毁 ;但我们在理解它们的底层实现发现,它们都在对next指针处进行操作(也就是release对象所在的地方) ;
push会在next加入哨兵对象POOL_BOUNDARY
,标志着一个自动释放池的创建;
而pop会从next往前release那些autorelease对象,知道遇到哨兵对象POOL_BOUNDARY
,这也标志着一个自动释放池的销毁 ;
这时再去思考自动释放池的本质和自动释放池的内存模式,我觉得会有新的理解 ;
AutoreleasePoolPage()
我们来看一下创建一个Page
的过程。AutoreleasePoolPage()
方法的参数为parentPage
,新创建的Page
的depth
加一,next
指针的初始位置指向begin
,将新创建的Page
的parent
指针指向parentPage
。将parentPage
的child
指针指向自己,这就形成了双向链表
的结构。
objectivec
AutoreleasePoolPage(AutoreleasePoolPage *newParent)
: magic(), next(begin()), thread(pthread_self()),
parent(newParent), child(nil),
depth(parent ? 1+parent->depth : 0),
hiwat(parent ? parent->hiwat : 0)
{
if (parent) {
parent->check();
assert(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
begin、end、empty、full
下面再来看一下begin
、end
、empty
、full
这些方法的实现。
begin
的地址为:Page
自己的地址+Page
对象的大小56
个字节;end
的地址为:Page
自己的地址+4096
个字节;empty
判断Page
是否为空的条件是next
地址是不是等于begin
;full
判断Page
是否已满的条件是next
地址是不是等于end
(栈顶)。
objectivec
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
bool empty() {
return next == begin();
}
bool full() {
return next == end();
}
复杂情况下@autoreleasepool的探讨
可以通过以下私有函数来查看自动释放池的情况:
objectivec
extern void _objc_autoreleasePoolPrint(void);
由于iOS
工程中,系统在自动释放池中注册了一些对象。为了排除这些干扰,接下来我们通过macOS
工程代码示例:
单个 @autoreleasepool
objectivec
int main(int argc, const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool {
_objc_autoreleasePoolPrint(); // print2
HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
}
_objc_autoreleasePoolPrint(); // print4
return 0;
}
嵌套 @autoreleasepool
objectivec
int main(int argc, const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool { //r1 = push()
_objc_autoreleasePoolPrint(); // print2
HTPerson *p1 = [[[HTPerson alloc] init] autorelease];
HTPerson *p2 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
@autoreleasepool { //r2 = push()
HTPerson *p3 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print4
@autoreleasepool { //r3 = push()
HTPerson *p4 = [[[HTPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print5
} //pop(r3)
_objc_autoreleasePoolPrint(); // print6
} //pop(r2)
_objc_autoreleasePoolPrint(); // print7
} //pop(r1)
_objc_autoreleasePoolPrint(); // print8
return 0;
}
复杂情况@autoreleasepool
由AutoreleasePoolPage
类的定义可知,自动释放池(即所有的AutoreleasePoolPage
对象)是以栈
为结点通过双向链表
的形式组合而成。每当Page
满了的时候,就会创建一个新的Page
,并设置它为hotPage
,而首个Page
为coldPage
。接下来我们来看一下多个Page
和多个@autoreleasepool
嵌套的情况。
objectivec
int main(int argc, const char * argv[]) {
@autoreleasepool { //r1 = push()
for (int i = 0; i < 600; i++) {
HTPerson *p = [[[HTPerson alloc] init] autorelease];
}
@autoreleasepool { //r2 = push()
for (int i = 0; i < 500; i++) {
HTPerson *p = [[[HTPerson alloc] init] autorelease];
}
@autoreleasepool { //r3 = push()
for (int i = 0; i < 200; i++) {
HTPerson *p = [[[HTPerson alloc] init] autorelease];
}
_objc_autoreleasePoolPrint();
} //pop(r3)
} //pop(r2)
} //pop(r1)
return 0;
}
一个AutoreleasePoolPage
对象的内存大小为4096
个字节,它自身成员变量占用内存56
个字节,所以剩下的4040
个字节用来存储autorelease
对象的内存地址。又因为64bit
下一个OC
对象的指针所占内存为8
个字节,所以一个Page
可以存放505
个对象的地址。POOL_BOUNDARY
也是一个对象,因为它的值为nil
。所以以上代码的自动释放池内存分布图如下所示。
使用iOS工程示例分析
从以上macOS
工程示例可以得知,在@autoreleasepool
大括号结束的时候,就会调用Page
的pop()
方法,给@autoreleasepool
中的autorelease
对象发送release
消息。
那么在iOS
工程中,方法里的autorelease
对象是什么时候释放的呢?有系统干预释放和手动干预释放两种情况。
- 系统干预释放是不指定
@autoreleasepool
,所有autorelease
对象都由主线程的RunLoop
创建的@autoreleasepool
来管理。 - 手动干预释放就是将
autorelease
对象添加进我们手动创建的@autoreleasepool
中。
下面还是在MRC
环境下进行分析。
系统干预释放
我们先来看以下 Xcode 11 版本的iOS
程序中的main()
函数,和旧版本的差异。
objectivec
// 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);
}
objectivec
// Xcode 旧版本
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
网上对于iOS
工程的main()
函数中的@autoreleasepool
有一种解释:
在iOS
工程的main()
函数中有一个@autoreleasepool
,这个@autoreleasepool
负责了应用程序所有autorelease
对象的释放。
其实这个解释是错误的。
应用程序所有autorelease
对象的都是由RunLoop
创建的@autoreleasepool
来管理。而main()
函数中的@autoreleasepool
只是负责管理它的作用域中的autorelease
对象。
新版本 Xcode 11 中的 main 函数发生了哪些变化?
旧版本是将整个应用程序运行放在@autoreleasepool
内,由于RunLoop
的存在,要return
即程序结束后@autoreleasepool
作用域才会结束,这意味着程序结束后main
函数中的@autoreleasepool
中的autorelease
对象才会释放。
而在 Xcode 11中,触发主线程RunLoop
的UIApplicationMain
函数放在了@autoreleasepool
外面,这可以保证@autoreleasepool
中的autorelease
对象在程序启动后立即释放。正如新版本的@autoreleasepool
中的注释所写 "Setup code that might create autoreleased objects goes here.
"(如上代码),可以将autorelease
对象放在此处。
这里非常推荐去看参考博客关于这里的分析,很精彩也很好懂
手动干预释放
即手动使用添加的@autoreleasepool,会在大括号结束时就会释放 ;
Q:ARC 环境下,autorelease 对象在什么时候释放?
回到我们最初的面试题,在ARC
环境下,autorelease
对象在什么时候释放?我们就分系统干预释放
和手动干预释放
两种情况回答。
Q:ARC 环境下,需不需要手动添加 @autoreleasepool?
AppKit 和 UIKit 框架会在RunLoop
每次事件循环迭代中创建并处理@autoreleasepool
,因此,你通常不必自己创建@autoreleasepool
,甚至不需要知道创建@autoreleasepool
的代码怎么写。但是,有些情况需要自己创建@autoreleasepool
。
苹果给出了三种需要手动添加@autoreleasepool
的情况:
- ① 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
- ② 如果你编写的循环中创建了大量的临时对象;
你可以在循环内使用@autoreleasepool
在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool
有助于减少应用程序的最大内存占用。 - ③ 如果你创建了辅助线程。(不开启runloop)
一旦线程开始执行,就必须创建自己的@autoreleasepool
;否则,你的应用程序将存在内存泄漏。
Q:如果对 NSAutoreleasePool 对象调用 autorelease 方法会发生什么情况?
objectivec
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool autorelease];
答:抛出异常NSInvalidArgumentException
并导致程序Crash
,异常原因:不能对NSAutoreleasePool
对象调用autorelease
。