@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对象的地址。
其内存分布图如下:
图片.png
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()。
图片.png
所以,在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控制。











网友评论