@autoreleasepool {
// Code benefitting from a local autorelease pool.
}
1、原理分析
1.1、__AtAutoreleasePool
下面我们先通过macOS工程来分析@autoreleasepool
的底层原理。 macOS工程中的main()函数什么都没做,只是放了一个@autoreleasepool
。
int main(int argc, const char * argv[]) {
@autoreleasepool {}
return 0;
}
通过 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
传入。
1.2、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
类来实现的。
自动释放池的数据结构
- 自动释放池的主要数据结构是:
__AtAutoreleasePool
、AutoreleasePoolPage
; - 调用了 autorelease的对象最终都是通过
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;
......
}
整个程序运行过程中,可能会有多个AutoreleasePoolPage
对象。从它的定义可以得知:
自动释放池(即所有的
AutoreleasePoolPage
对象)是以栈为结点通过双向链表的形式组合而成;自动释放池与线程一一对应;
每个
AutoreleasePoolPage
对象占用4096
字节内存,其中56
个字节用来存放它内部的成员变量,剩下的空间(4040
个字节)用来存放autorelease对象的地址
。
其内存分布图如下:
1.2.1、POOL_BOUNDARY
在分析这些方法之前,先介绍一下POOL_BOUNDARY
。
-
POOL_BOUNDARY
的前世叫做POOL_SENTINEL
,称为哨兵对象或者边界对象; -
POOL_BOUNDARY
用来区分不同的自动释放池,以解决自动释放池嵌套的问题; - 每当创建一个自动释放池,就会调用
push()
方法将一个POOL_BOUNDARY
入栈,并返回其存放的内存地址; - 当往自动释放池中添加
autorelease对象
时,将autorelease对象的内存地址
入栈,它们前面至少有一个POOL_BOUNDARY
; - 当销毁一个自动释放池时,会调用
pop()
方法并传入一个POOL_BOUNDARY
,会从自动释放池中最后一个对象开始,依次给它们发送release消息
,直到遇到这个POOL_BOUNDARY
。
1.2.2、push
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对象添加进去。
1.2.3、pop
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
,有的话就销毁。
1.2.4、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();
}
2、查看自动释放池的情况
可以通过以下私有函数来查看自动释放池的情况:
extern void _objc_autoreleasePoolPrint(void);
3、iOS 工程示例分析
在iOS工程中,方法里的autorelease对象
是什么时候释放的呢?
有系统干预释放和手动干预释放两种情况。
- 系统干预释放是不指定
@autoreleasepool
,所有autorelease对象
都由主线程的RunLoop创建的@autoreleasepool
来管理。 - 手动干预释放就是将
autorelease对象
添加进我们手动创建的@autoreleasepool
中。
下面还是在MRC环境下进行分析。
3.1、系统干预释放
我们先来看以下 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);
}
// Xcode 旧版本
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
新版本 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对象放在此处。
接着我们来看 “系统干预释放” 情况的示例:
- (void)viewDidLoad {
[super viewDidLoad];
Person *person = [[[Person 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:]
// -[Person dealloc]
// -[ViewController viewDidAppear:]
可以看到,调用了autorelease
方法的person对象
不是在viewDidLoad方法
结束后释放,而是在viewWillAppear方法
结束后释放,说明在viewWillAppear方法
结束的时候,调用了pop()
方法释放了person对象
。
其实这是由RunLoop
控制的,下面来讲解一下RunLoop
和@autoreleasepool
的关系。
3.2、RunLoop 与 @autoreleasepool
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()
。
3.3、手动干预释放
我们再来看一下手动干预释放的情况。
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
Person *person = [[[Person 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__);
}
// -[Person dealloc]
// -[ViewController viewDidLoad]
// -[ViewController viewWillAppear:]
// -[ViewController viewDidAppear:]
可以看到,添加进手动指定的@autoreleasepool
中的autorelease对象
,在@autoreleasepool
大括号结束时就会释放,不受RunLoop
控制。