前言
作为 iOS 开发者,在面试过程中经常会碰到这样一个问题:在 ARC 环境下autorelease
对象在什么时候释放?如果你还不知道怎么回答,或者你只有比较模糊的概念,那么你绝对不能错过本文。
本文将通过Runtime objc4-756.2
版本源码、macOS 与 iOS 工程示例来分析@autoreleasepool
的底层原理。并在最后针对有关autorelease
和@autoreleasepool
的一些问题进行解答。
苹果在 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
修饰符修饰对象将对象注册到自动释放池中。详情请参阅《iOS - 老生常谈内存管理(三):ARC 面世 —— 所有权修饰符》
。
1. 自动释放池
官方文档
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.
以上是苹果对自动释放池的一段介绍,其意思为:AppKit 和 UIKit 框架在事件循环(RunLoop
)的每次循环开始时,在主线程创建一个自动释放池,并在每次循环结束时销毁它,在销毁时释放自动释放池中的所有autorelease
对象。通常情况下我们不需要手动创建自动释放池,但是如果我们在循环中创建了很多临时的autorelease
对象,则手动创建自动释放池来管理这些对象可以很大程度地减少内存峰值。
创建一个自动释放池
- 在
MRC
下,可以使用NSAutoreleasePool
或者@autoreleasepool
。建议使用@autoreleasepool
,苹果说它比NSAutoreleasePool
快大约六倍。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// Code benefitting from a local autorelease pool.
[pool release];
Q: 释放
NSAutoreleasePool
对象,使用[pool release]
与[pool drain]
的区别?Objective-C 语言本身是支持 GC 机制的,但有平台局限性,仅限于 MacOS 开发中,iOS 开发用的是 RC 机制。在 iOS 的 RC 环境下
[pool release]
和[pool drain]
效果一样,但在 GC 环境下drain
会触发 GC 而release
不做任何操作。使用[pool drain]
更佳,一是它的功能对系统兼容性更强,二是这样可以跟普通对象的release
区别开。(注意:苹果在引入ARC
时称,已在 OS X Mountain Lion v10.8 中弃用GC
机制,而使用ARC
替代)
- 而在
ARC
下,已经禁止使用NSAutoreleasePool
类创建自动释放池,只能使用@autoreleasepool
。
@autoreleasepool {
// Code benefitting from a local autorelease pool.
}
2. 原理分析
下面我们先通过macOS
工程来分析@autoreleasepool
的底层原理。
macOS
工程中的main()
函数什么都没做,只是放了一个@autoreleasepool
。
int main(int argc, const char * argv[]) {
@autoreleasepool {}
return 0;
}
__AtAutoreleasePool
通过 Clang clang -rewrite-objc main.m
将以上代码转换为 C 代码。
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
传入。
AutoreleasePoolPage
下面我们进入Runtime objc4
源码查看以上提到的两个函数的实现。
// NSObject.mm
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
可以得知,objc_autoreleasePoolPush()
和objc_autoreleasePoolPop()
两个函数其实是调用了AutoreleasePoolPage
类的两个类方法push()
和pop()
。所以@autoreleasepool
底层就是使用AutoreleasePoolPage
类来实现的。
下面我们来看一下AutoreleasePoolPage
类的定义:
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;
......
}
代码语言:txt复制备注: 本文使用的是
objc4-756.2
版本源码进行分析。在objc4-779.1
版本中,AutoreleasePoolPage
继承自AutoreleasePoolPageData
,如下。不过原理不变,不影响分析。
// objc4-779.1
// NSObject.mm
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment, power of 2
#endif
private:
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const COUNT = SIZE / sizeof(id);
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
// SIZE-sizeof(*this) bytes of contents follow
......
}
// NSObject-internal.h
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
: magic(), next(_next), thread(_thread),
parent(_parent), child(nil),
depth(_depth), hiwat(_hiwat)
{
}
};
整个程序运行过程中,可能会有多个AutoreleasePoolPage
对象。从它的定义可以得知:
- 自动释放池(即所有的
AutoreleasePoolPage
对象)是以栈
为结点通过双向链表
的形式组合而成; - 自动释放池与线程一一对应;
- 每个
AutoreleasePoolPage
对象占用4096
字节内存,其中56
个字节用来存放它内部的成员变量,剩下的空间(4040
个字节)用来存放autorelease
对象的地址。
其内存分布图如下:
下面我们通过源码来分析push()
、pop()
以及autorelease
方法的实现。
POOL_BOUNDARY
在分析这些方法之前,先介绍一下POOL_BOUNDARY
。
POOL_BOUNDARY
的前世叫做POOL_SENTINEL
,称为哨兵对象或者边界对象;POOL_BOUNDARY
用来区分不同的自动释放池,以解决自动释放池嵌套的问题;- 每当创建一个自动释放池,就会调用
push()
方法将一个POOL_BOUNDARY
入栈,并返回其存放的内存地址; - 当往自动释放池中添加
autorelease
对象时,将autorelease
对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY
; - 当销毁一个自动释放池时,会调用
pop()
方法并传入一个POOL_BOUNDARY
,会从自动释放池中最后一个对象开始,依次给它们发送release
消息,直到遇到这个POOL_BOUNDARY
。
push
代码语言:txt复制 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
哨兵对象。
下面我们来看一下autoreleaseFast()
方法的实现:
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
中; - ② 当前
Page
存在但已满时,通过autoreleaseFullPage(obj, page)
创建一个新的Page
,并将autorelease
对象添加进去; - ③ 当前
Page
不存在,即还没创建过Page
,通过autoreleaseNoPage(obj)
创建第一个Page
,并将autorelease
对象添加进去。
下面我们来看一下以上提到的三个方法的实现:
代码语言:txt复制 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
指针指向这个对象的下一个位置,然后将该对象的位置返回。
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->add(obj)
将autorelease
对象添加进去,并将该Page
设置为hotPage
。
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
代码语言:txt复制 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
代码语言:txt复制 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()
方法的传参token
即为POOL_BOUNDARY
对应在Page
中的地址。当销毁自动释放池时,会调用pop()
方法将自动释放池中的autorelease
对象全部释放(实际上是从自动释放池的中的最后一个入栈的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()
方法中,下面来看一下这个方法的实现:
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
。
AutoreleasePoolPage()
我们来看一下创建一个Page
的过程。AutoreleasePoolPage()
方法的参数为parentPage
,新创建的Page
的depth
加一,next
指针的初始位置指向begin
,将新创建的Page
的parent
指针指向parentPage
。将parentPage
的child
指针指向自己,这就形成了双向链表
的结构。
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
(栈顶)。
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();
}
小结:
push
操作是往自动释放池中添加一个POOL_BOUNDARY
,并返回它存放的内存地址;接着每有一个对象调用
autorelease
方法,会将它的内存地址添加进自动释放池中。pop
操作是传入一个POOL_BOUNDARY
的内存地址,从最后一个入栈的autorelease
对象开始,将自动释放池中的autorelease
对象全部释放(实际上是给它们发送一条release
消息),直到遇到这个POOL_BOUNDARY
。
3. 查看自动释放池的情况
可以通过以下私有函数来查看自动释放池的情况:
代码语言:txt复制extern void _objc_autoreleasePoolPrint(void);
4. 使用 macOS 工程示例分析
由于iOS
工程中,系统在自动释放池中注册了一些对象。为了排除这些干扰,接下来我们通过macOS
工程代码示例,结合AutoreleasePoolPage
的内存分布图以及_objc_autoreleasePoolPrint()
私有函数,来帮助我们更好地理解@autoreleasepool
的原理。
注意:由于
ARC
环境下不能调用autorelease
等方法,所以需要将工程切换为MRC
环境。如果使用
ARC
,则可以使用__autoreleasing
所有权修饰符替代autorelease
方法。
单个 @autoreleasepool
代码语言:txt复制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;
}
代码语言:txt复制// 自动释放池的情况
objc[68122]: ############## (print1)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 0 releases pending. //当前自动释放池中没有任何对象
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: ##############
objc[68122]: ############## (print2)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 1 releases pending. //当前自动释放池中有1个对象,这个对象为POOL_BOUNDARY
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: ##############
objc[68122]: ############## (print3)
objc[68122]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68122]: 3 releases pending. //当前自动释放池中有3个对象
objc[68122]: [0x102802000] ................ PAGE (hot) (cold)
objc[68122]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68122]: [0x102802040] 0x100704a10 HTPerson //p1
objc[68122]: [0x102802048] 0x10075cc30 HTPerson //p2
objc[68122]: ##############
objc[68156]: ############## (print4)
objc[68156]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68156]: 0 releases pending. //当前自动释放池中没有任何对象,因为@autoreleasepool作用域结束,调用pop方法释放了对象
objc[68156]: [0x100810000] ................ PAGE (hot) (cold)
objc[68156]: ##############
嵌套 @autoreleasepool
代码语言:txt复制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;
}
代码语言:txt复制// 自动释放池的情况
objc[68285]: ############## (print1)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 0 releases pending. //当前自动释放池中没有任何对象
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: ##############
objc[68285]: ############## (print2)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 1 releases pending. //当前自动释放池中有1个对象
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: ##############
objc[68285]: ############## (print3)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 3 releases pending. //当前自动释放池中有3个对象(1个@autoreleasepool)
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040] 0x100707d80 HTPerson //p1
objc[68285]: [0x102802048] 0x100707de0 HTPerson //p2
objc[68285]: ##############
objc[68285]: ############## (print4)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 5 releases pending. //当前自动释放池中有5个对象(2个@autoreleasepool)
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040] 0x100707d80 HTPerson //p1
objc[68285]: [0x102802048] 0x100707de0 HTPerson //p2
objc[68285]: [0x102802050] ################ POOL 0x102802050 //POOL_BOUNDARY
objc[68285]: [0x102802058] 0x1005065b0 HTPerson //p3
objc[68285]: ##############
objc[68285]: ############## (print5)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 7 releases pending. //当前自动释放池中有7个对象(3个@autoreleasepool)
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: [0x102802038] ################ POOL 0x102802038 //POOL_BOUNDARY
objc[68285]: [0x102802040] 0x100707d80 HTPerson //p1
objc[68285]: [0x102802048] 0x100707de0 HTPerson //p2
objc[68285]: [0x102802050] ################ POOL 0x102802050 //POOL_BOUNDARY
objc[68285]: [0x102802058] 0x1005065b0 HTPerson //p3
objc[68285]: [0x102802060] ################ POOL 0x102802060 //POOL_BOUNDARY
objc[68285]: [0x102802068] 0x100551880 HTPerson //p4
objc[68285]: ##############
objc[68285]: ############## (print6)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 5 releases pending. //当前自动释放池中有5个对象(第3个@autoreleasepool已释放)
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: [0x102802038] ################ POOL 0x102802038
objc[68285]: [0x102802040] 0x100707d80 HTPerson
objc[68285]: [0x102802048] 0x100707de0 HTPerson
objc[68285]: [0x102802050] ################ POOL 0x102802050
objc[68285]: [0x102802058] 0x1005065b0 HTPerson
objc[68285]: ##############
objc[68285]: ############## (print7)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 3 releases pending. //当前自动释放池中有3个对象(第2、3个@autoreleasepool已释放)
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: [0x102802038] ################ POOL 0x102802038
objc[68285]: [0x102802040] 0x100707d80 HTPerson
objc[68285]: [0x102802048] 0x100707de0 HTPerson
objc[68285]: ##############
objc[68285]: ############## (print8)
objc[68285]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[68285]: 0 releases pending. //当前自动释放池没有任何对象(3个@autoreleasepool都已释放)
objc[68285]: [0x102802000] ................ PAGE (hot) (cold)
objc[68285]: ##############
复杂情况 @autoreleasepool
由AutoreleasePoolPage
类的定义可知,自动释放池(即所有的AutoreleasePoolPage
对象)是以栈
为结点通过双向链表
的形式组合而成。每当Page
满了的时候,就会创建一个新的Page
,并设置它为hotPage
,而首个Page
为coldPage
。接下来我们来看一下多个Page
和多个@autoreleasepool
嵌套的情况。
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
。所以以上代码的自动释放池内存分布图如下所示。
objc[69731]: ##############
objc[69731]: AUTORELEASE POOLS for thread 0x1000aa5c0
objc[69731]: 1303 releases pending. //当前自动释放池中有1303个对象(3个POOL_BOUNDARY和1300个HTPerson实例)
objc[69731]: [0x100806000] ................ PAGE (full) (cold) /* 第一个PAGE,full代表已满,cold代表coldPage */
objc[69731]: [0x100806038] ################ POOL 0x100806038 //POOL_BOUNDARY
objc[69731]: [0x100806040] 0x10182a040 HTPerson //p1
objc[69731]: [0x100806048] ..................... //...
objc[69731]: [0x100806ff8] 0x101824e40 HTPerson //p504
objc[69731]: [0x102806000] ................ PAGE (full) /* 第二个PAGE */
objc[69731]: [0x102806038] 0x101824e50 HTPerson //p505
objc[69731]: [0x102806040] ..................... //...
objc[69731]: [0x102806330] 0x101825440 HTPerson //p600
objc[69731]: [0x102806338] ################ POOL 0x102806338 //POOL_BOUNDARY
objc[69731]: [0x102806340] 0x101825450 HTPerson //p601
objc[69731]: [0x102806348] ..................... //...
objc[69731]: [0x1028067e0] 0x101825d90 HTPerson //p1008
objc[69731]: [0x102804000] ................ PAGE (hot) /* 第三个PAGE,hot代表hotPage */
objc[69731]: [0x102804038] 0x101826dd0 HTPerson //p1009
objc[69731]: [0x102804040] ..................... //...
objc[69731]: [0x102804310] 0x101827380 HTPerson //p1100
objc[69731]: [0x102804318] ################ POOL 0x102804318 //POOL_BOUNDARY
objc[69731]: [0x102804320] 0x101827390 HTPerson //p1101
objc[69731]: [0x102804328] ..................... //...
objc[69731]: [0x102804958] 0x10182b160 HTPerson //p1300
objc[69731]: ##############
5. 使用 iOS 工程示例分析
从以上macOS
工程示例可以得知,在@autoreleasepool
大括号结束的时候,就会调用Page
的pop()
方法,给@autoreleasepool
中的autorelease
对象发送release
消息。
那么在iOS
工程中,方法里的autorelease
对象是什么时候释放的呢?有系统干预释放和手动干预释放两种情况。
- 系统干预释放是不指定
@autoreleasepool
,所有autorelease
对象都由主线程的RunLoop
创建的@autoreleasepool
来管理。 - 手动干预释放就是将
autorelease
对象添加进我们手动创建的@autoreleasepool
中。
下面还是在MRC
环境下进行分析。
系统干预释放
我们先来看以下 Xcode 11 版本的iOS
程序中的main()
函数,和旧版本的差异。
// 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);
}
代码语言:txt复制// Xcode 旧版本
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
注意:
网上对于
iOS
工程的main()
函数中的@autoreleasepool
有一种解释:在
iOS
工程的main()
函数中有一个@autoreleasepool
,这个@autoreleasepool
负责了应用程序所有autorelease
对象的释放。其实这个解释是错误的。
如果你的程序使用了
AppKit
或UIKit
框架,那么主线程的RunLoop
就会在每次事件循环迭代中创建并处理@autoreleasepool
。也就是说,应用程序所有autorelease
对象的都是由RunLoop
创建的@autoreleasepool
来管理。而main()
函数中的@autoreleasepool
只是负责管理它的作用域中的autorelease
对象。 在以上《使用 MacOS 工程示例分析》
章节中提到了嵌套@autoreleasepool
的情况。Xcode 旧版本的main
函数中是将整个应用程序运行(UIApplicationMain
)放在@autoreleasepool
内,而主线程的RunLoop
就是在UIApplicationMain
中创建,所以RunLoop
创建的@autoreleasepool
是嵌套在main
函数的@autoreleasepool
内的。RunLoop
会在每次事件循环中对自动释放池进行pop
和push
(以下会详细讲解),但是它的pop
只会释放掉它的POOL_BOUNDARY
之后的对象,它并不会影响到外层即main
函数中@autoreleasepool
。新版本 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
对象放在此处。
接着我们来看 “系统干预释放” 情况的示例:
代码语言:txt复制- (void)viewDidLoad {
[super viewDidLoad];
HTPerson *person = [[[HTPerson alloc] init] autorelease];
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[HTPerson dealloc]
// -[ViewController viewDidAppear:]
可以看到,调用了autorelease
方法的person
对象不是在viewDidLoad
方法结束后释放,而是在viewWillAppear
方法结束后释放,说明在viewWillAppear
方法结束的时候,调用了pop()
方法释放了person
对象。其实这是由RunLoop
控制的,下面来讲解一下RunLoop
和@autoreleasepool
的关系。
RunLoop 与 @autoreleasepool
学习这个知识点之前,需要先搞懂
RunLoop
的事件循环机制以及它的6
种活动状态,可以查看我的文章:《深入浅出 RunLoop(二):数据结构》
《深入浅出 RunLoop(三):事件循环机制》
iOS
在主线程的RunLoop
中注册了两个Observer
。
- 第1个
Observer
监听了kCFRunLoopEntry
事件,会调用objc_autoreleasePoolPush()
; - 第2个
Observer
① 监听了kCFRunLoopBeforeWaiting
事件,会调用objc_autoreleasePoolPop()
、objc_autoreleasePoolPush()
; ② 监听了kCFRunLoopBeforeExit
事件,会调用objc_autoreleasePoolPop()
。
所以,在iOS
工程中系统干预释放的autorelease
对象的释放时机是由RunLoop
控制的,会在当前RunLoop
每次循环结束时释放。以上person
对象在viewWillAppear
方法结束后释放,说明viewDidLoad
和viewWillAppear
方法在同一次循环里。
kCFRunLoopEntry
:在即将进入RunLoop
时,会自动创建了一个__AtAutoreleasePool
结构体对象,并调用objc_autoreleasePoolPush()
函数。kCFRunLoopBeforeWaiting
:在RunLoop
即将休眠时,会自动销毁一个__AtAutoreleasePool
对象,调用objc_autoreleasePoolPop()
。然后创建一个新的__AtAutoreleasePool
对象,并调用objc_autoreleasePoolPush()
。kCFRunLoopBeforeExit
,在即将退出RunLoop
时,会自动销毁最后一个创建的__AtAutoreleasePool
对象,并调用objc_autoreleasePoolPop()
。
手动干预释放
我们再来看一下手动干预释放的情况。
代码语言:txt复制- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
HTPerson *person = [[[HTPerson alloc] init] autorelease];
}
NSLog(@"%s", __func__);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
// -[HTPerson dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]
可以看到,添加进手动指定的@autoreleasepool
中的autorelease
对象,在@autoreleasepool
大括号结束时就会释放,不受RunLoop
控制。
相关问题
Q:ARC 环境下,方法里的局部对象什么时候释放?
以上都是在MRC
环境下分析,因为ARC
下不能给对象调用retain
release
、autorelease
等方法,LLVM
编译器会自动为我们插入这些代码。
那ARC
中方法里的局部对象什么时候释放?其实只要知道LLVM
编译器在编译时给对象插入release
还是autorelease
方法就知道了。如果插入的是release
,那么这个局部对象在方法结束时就会释放;如果插入的是autorelease
,那么这个局部对象的释放时机由RunLoop
控制。
备注: 在
MRC
下,通过alloc/new/copy/mutableCopy
或以这些命名开头的方法创建的对象直接持有,而通过其它方法创建的对象会通过调用autorelease
加入到自动释放池中。而ARC
下不能调用autorelease
方法,那么ARC
怎么做到这一点呢?《Objective-C 高级编程:iOS 与 OS X 多线程和内存管理》 书中是说:在
ARC
下,编译器会检查方法名是否以alloc/new/copy/mutableCopy
开始,如果不是则自动将返回值的对象注册到@autoreleasepool
。但经过测试,发现并不是如此。而且,以前在
MRC
下通过array
类方法创建的NSMutableArray
对象会被加入到@autoreleasepool
,但是在ARC
下并不会。所以,根据方法名并不能判断
ARC
会不会将对象加入到@autoreleasepool
。如果我们需要这么做,建议使用__autoreleasing
修饰符。
Q:ARC 环境下,autorelease 对象在什么时候释放?
回到我们最初的面试题,在ARC
环境下,autorelease
对象在什么时候释放?我们就分系统干预释放
和手动干预释放
两种情况回答。
Q:ARC 环境下,需不需要手动添加 @autoreleasepool?
AppKit 和 UIKit 框架会在RunLoop
每次事件循环迭代中创建并处理@autoreleasepool
,因此,你通常不必自己创建@autoreleasepool
,甚至不需要知道创建@autoreleasepool
的代码怎么写。但是,有些情况需要自己创建@autoreleasepool
。
例如,如果我们需要在循环中创建了很多临时的autorelease
对象,则手动添加@autoreleasepool
来管理这些对象可以很大程度地减少内存峰值。比如在for
循环中alloc
图片数据等内存消耗较大的场景,需要手动添加@autoreleasepool
。
苹果给出了三种需要手动添加
@autoreleasepool
的情况:① 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
② 如果你编写的循环中创建了大量的临时对象;
你可以在循环内使用
@autoreleasepool
在下一次迭代之前处理这些对象。在循环中使用@autoreleasepool
有助于减少应用程序的最大内存占用。③ 如果你创建了辅助线程。
一旦线程开始执行,就必须创建自己的
@autoreleasepool
;否则,你的应用程序将存在内存泄漏。
版本更新
版本日期 | 更新内容 |
---|---|
2020.3.17 | 首次发布文章 |
2020.4.17 |
|