AutoReleasePool
内存管理一直是Objective-C 的重点,在MRC环境下,通过调用[obj autorelease]来延迟内存的释放,在现在ARC环境下,我们甚至可以不需要知道Autorelease就能很好的管理内存。但是了解 Objective-C 的内存管理机制仍然是十分必要的,看看编译器帮助我们怎么来管理内存。
AutoreleasePool简介
AutoreleasePool:自动释放池是 Objective-C 开发中的一种自动内存回收管理的机制,为了替代开发人员手动管理内存,实质上是使用编译器在适当的位置插入release、autorelease等内存释放操作。当对象调用 autorelease方法后会被放到自动释放池中延迟释放时机,当缓存池需要清除dealloc时,会向这些 Autoreleased对象做 release 释放操作。
@autoreleasepool 到底是什么?
main.m 文件中的内容是这样的:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在这个 @autoreleasepool 中,所有的事件、消息全部交给了 UIApplication 来处理.
需要注意的是:整个 iOS 的应用都是包含在一个自动释放池 block 中的。
我们通过一下方法在终端中运行,编译main.m文件,转译成c++文件看一下
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
转译后上面的mian.m中的代码为下面
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
}
这时会存在一个__AtAutoreleasePool __autoreleasepool; 我们在main.cpp
中查找会发现一个结构体
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这个结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时调用 objc_autoreleasePoolPop 方法。
这是我们可以再次重新整理main 函数的工作其实就是
int main(int argc, const char * argv[]) {
{
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// do whatever you want
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
AutoreleasePool 是什么
我们现在可以通过苹果开源代码看看AutoreleasePool 到底是一什么样的东西
在源码中我们可以找到NSObject.mm文件,我们可以找到两个方法
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
上面的方法看上去是对 AutoreleasePoolPage 对应静态方法 push 和 pop 的封装。
首先我们来了解一下AutoreleasePoolPage 的结构
image.png
- magic 用于对当前 AutoreleasePoolPage 完整性的校验
- thread 保存了当前页所在的线程
每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起,
如图所示:
parent 和 child 就是用来构造双向链表的指针。
next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中:
cold 与 hot
我们模拟一个程序,通过一个私有方法 _objc_autoreleasePoolPrint 打印一下 AutoreleasePoolPage
int main(int argc, const char * argv[]) {
@autoreleasepool { // r1 = push()
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0; i < 600; i++) {
MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
}
@autoreleasepool { // r3 = push()
MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint();
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
image.png
image.png
通过图片可以了解,第一个AutoreleasePoolPage 状态是full 然后系统默认会把它状态变为cold ,第二个page 是正在使用的page 状态为hot
POOL_BOUNDARY
当调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY停止,
objc_autoreleasePoolPush 方法
接下来我们来了解一下push 的整个流程
首先调用push 方法, 调用push 方法的时候,会检测当前的AutoreleasePoolPage 是否存在空间存储 ,如果没有新建一个调用autoreleaseNewPage 方法,如果内存足够调用 autoreleaseFast方法,直接存入
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY); //如果没有AutoreleasePoolPage new 一个新的,传入POOL_BOUNDARY
} else {
dest = autoreleaseFast(POOL_BOUNDARY); //如果当前的AutoreleasePoolPage还有内存,存入当前page
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
- autoreleaseNewPage 方法
//新建一个page
id *autoreleaseNewPage(id obj)
{
AutoreleasePoolPage *page = hotPage(); 调用hotpage方法
if (page) return autoreleaseFullPage(obj, page); hotPage 返回的page 调用autoreleaseFullPage
else return autoreleaseNoPage(obj);
}
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
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); //加入
}
- autoreleaseFast 方法
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);
}
}
- 有 hotPage 并且当前 page 不满
- 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
- 有 hotPage 并且当前 page 已满
- 调用 autoreleaseFullPage 初始化一个新的页
- 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
- 无 hotPage
- 调用 autoreleaseNoPage 创建一个 hotPage
- 调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
最后的都会调用 page->add(obj) 将对象添加到自动释放池中。
objc_autoreleasePoolPop 方法
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
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); //调用releaseUntil 方法
// 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();
}
}
}
//while循环,
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;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {//如果不是 POOL_BOUNDARY,就调用release 操作,引用计数减-1
objc_release(obj);
}
}
使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPage
调用 releaseUntil 方法释放栈中的对象,直到 stop
调用 child 的 kill 方法
autorelease 方法
我们已经对自动释放池生命周期有一个比较好的了解,最后需要了解的话题就是 autorelease 方法的实现,先来看一下方法的调用栈:
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└── id objc_object::rootAutorelease2()
└── static id AutoreleasePoolPage::autorelease(id obj)
└── static id AutoreleasePoolPage::autoreleaseFast(id obj)
├── id *add(id obj)
├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └── id *add(id obj)
└── static id *autoreleaseNoPage(id obj)
├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└── id *add(id obj)
在 autorelease
方法的调用栈中,最终都会调用上面提到的 autoreleaseFast 方法,将当前对象加到 AutoreleasePoolPage
中。
小结
整个自动释放池 AutoreleasePool 的实现以及 autorelease 方法都已经分析完了,我们再来回顾一下文章中的一些内容:
自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息
接下来还有一个常见的面试题:
AutoReleasePool的释放时机(原理)
例如:MJPerson *person = [[ [MJPerson alloc] init] autorelease];
其实这是再考runloop 的只是 我们来打印[NSRunLoop mainRunLoop]);
进入程序是调用 activities = 0x1 对应runloop的状态 kCFRunLoopEntry
// "<CFRunLoopObserver 0x6000028a8320 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000017ce100 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fab6c804038>\n)}}",
//
退出是调用, activities = 0xa0, = 160 = kCFRunLoopBeforeWaiting + kCFRunLoopExit
// "<CFRunLoopObserver 0x6000028a83c0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x6000017ce100 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fab6c804038>\n)}}"
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 调用kCFRunLoopEntry 会调用一次push 操作
当runloop 调用kCFRunLoopBeforeWaiting 会调用一次pop 操作,在调用一次push 操作
当runloop 调用kCFRunLoopExit 会调用一次pop 操作
所以AutoReleasePool 释放是在某次runloop 调用 kCFRunLoopExit 的时候