原文:https://zhuanlan.zhihu.com/p/520047056
在原文基础上增加了一些注释。
说明:源码在runtime源码中,不同runloop源码中。
使用
使用场景
在ARC下,AutoreleasePool主要应用
在大量创建临时对象的场景
,通过AutoreleasePool控制内存峰值
,是一个很好的选择。
NSAutoreleasePool
在MRC可以调用NSAutoreleasePool使对象延迟释放,在ARC下这个API已经被禁用。
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// ...
[pool release];
@autoreleasepool
除了NSAutoreleasePool还可以使用@autoreleasepool,并且苹果推荐使用@autoreleasepool,因为这个API性能更好,在ARC下依然可以使用@autoreleasepool。
无论是MRC还是ARC,autorelease
最大的作用,是在大量创建对象的同时,通过修饰让内存得到提前释放
,从而降低内存峰值
。
for (size_t i = 0; i < frameCount; i++) {
@autoreleasepool {
[bitmapRep setProperty:NSImageCurrentFrame withValue:@(i)];
float frameDuration = [[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue];
NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapRep.CGImage size:CGSizeZero];
SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:frameImage duration:frameDuration];
[frames addObject:frame];
}
}
__autoreleasing
在ARC下,需要被自动释放的对象,可以用__autoreleasing修饰,让对象延迟释放。
+ (NSArray *)parseString:(NSString *)originalM3U8Str m3u8Host:(NSString *)m3u8url error:(NSError *__autoreleasing *)errorPtr;
源码分析
__AtAutoreleasePool结构体
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
//在创建的时候会执行objc_autoreleasePoolPush函数
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
//在释放的时候会执行析构函数,并执行objc_autoreleasePoolPop函数
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
@autoreleasepool
本质上会被系统转换成C++的__AtAutoreleasePool结构体
,@autoreleasepool的大括号开始,对应着objc_autoreleasePoolPush函数
。大括号结束,对应着objc_autoreleasePoolPop函数
。通过clang
命令将OC代码转成C++代码
,可以看到有一个__AtAutoreleasePool结构体
。
__AtAutoreleasePool结构体
在创建的时候会执行objc_autoreleasePoolPush
函数,在释放的时候会执行析构函数,并执行objc_autoreleasePoolPop
函数。在这两个函数内部,会调用AutoreleasePoolPage的push和pop
函数。
AutoreleasePoolPage
在运行时代码中
,objc_autoreleasePoolPop和objc_autoreleasePoolPush
,都调用了AutoreleasePoolPage类
的实现。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
说明:AutoreleasePoolPage的实现在runtime的源码里。
在AutoreleasePoolPage的定义中,可以看到有parent和child的定义,当page中对象太多存储不下时,会创建其他的page对象来存储,AutoreleasePoolPage
的结构是一个双向链表
。在插入新的autorelease对象时,也会从链表头向后查找,直到找到未满的page
。
class AutoreleasePoolPage
{
magic_t const magic; // 校验page的结构是否完整
id *next; // 指向下一个可以存放autorelease对象的地址
pthread_t const thread; // 当前所在的线程
AutoreleasePoolPage * const parent; // 当前page的父节点
AutoreleasePoolPage *child; // 当前page的子节点
uint32_t const depth; // page的深度
uint32_t hiwat;
}
AutoreleasePoolPage是一个C++的类,每个page占4096个字节,也就是16进制的0x1000,也就是4kb的空间。这些空间中,其自身的成员变量只占56个字节,也就是下面七个成员变量,每个占8字节,总共56个字节
。其他的四千多个字节,都用来存放被autorelease修饰的对象内存地址
。
POOL_BOUNDARY
POOL_BOUNDARY
的作用是,区分不同的自动释放池
,也就是不同的@autoreleasepool
。调用push时,会传入POOL_BOUNDARY并返回一个地址例如0x1038,0x1038是不存储@autorelease对象的地址的,起到一个标识作用,用来分割不同的@autoreleasepool。
调用pop时,会传入end的地址,并从后到前调用对象的release方法,直到POOL_BOUNDARY为止。如果存在多个page,会从child的page的最末尾开始调用,直到POOL_BOUNDARY。page的结构是一个栈结构,释放的时候也是从栈顶开始释放。
next指针指向栈顶,是栈里面很常见的一个设计。AutoreleasePoolPage和POOL_BOUNDARY的区别在于,AutoreleasePoolPage负责维护存储区域
,而POOL_BOUNDARY则负责分割存储在page中的对象地址
,以@autoreleasepool为单位进行分割
。
多层嵌套
@autoreleasepool {
NSObject *p1 = [[NSObject alloc] init];
NSObject *p2 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p3 = [[NSObject alloc] init];
@autoreleasepool {
NSObject *p4 = [[NSObject alloc] init];
}
}
}
如果是多层@autoreleasepool的嵌套,会用同一个AutoreleasePoolPage对象。以下面的三个嵌套为例,在同一个page中的顺序是下图这样。不同的@autoreleasepool以POOL_BOUNDARY做分割。
push
创建一个autoreleasePool之后,就会调用push函数。在push函数中会判断是否调试模式下,如果调试模式会每次生成一个新的page。debug环境代码可以直接忽略,只保留autoreleaseFast函数。
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
return dest;
}
autoreleaseFast
在函数内部,会通过hotPage
获取当前的page
,hotPage函数内部本质
上是一个page和key的映射
。
- 如果
page
不为空并且有空间
,则调用page的add函数将对象添加到page中,并将POOL_BOUNDARY添加在当前的位置。 - 如果
page
已经被创建
但没有空间
,会调用autoreleaseFullPage函数创建新的page,并且将链表的末尾指向新创建的page。 - 如果
没有创建page
,则调用autoreleaseNoPage函数创建一个新的page,并且将当前线程的hotPage设置为新创建的page。
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);
}
}
autoreleaseFullPage
- 在autoreleaseFullPage函数中,会从
page的链表
中,从前往后找到末尾的节点
。 - 创建一个新的page,在创建函数AutoreleasePoolPage中会处理parent和child指针的问题,返回的page可以直接用。
- 调用
setHotPage
将page设置到哈希表中,并且调用page的add函数将autorelease修饰的对象,添加到page中。
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
疑惑点:autorelease修饰的对象是指哪里
参考文章:http://08643.cn/p/d0558e4b0d21
说明:当一个对象发送了autorelease消息,就是将当前这个对象加入到AutoreleasePoolPage的栈顶next指向的位置
示例代码如下:
// MRC
NSAutoreleasePool *pool = [NSAutoreleasePool alloc] init];
id obj = [NSObject alloc] init];
[obj autorelease];
[pool drain];
// ARC
@autoreleasepool {
id obj = [NSObject alloc] init];
}
autoreleaseNoPage
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
return page->add(obj);
}
autoreleaseNoPage函数的核心代码比较简单,就是创建一个新的page,随后设置POOL_BOUNDARY标志,并且把对象添加进去。在函数中需要留意POOL_BOUNDARY标志,很多地方都用来做page是否为空的判断。
add
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next;
*next++ = obj;
protect();
return ret;
}
add函数比较简单,核心逻辑就是将obj放入next指针的位置,并且对next指针进行++,指向下一个位置。*next++表示先用后加,先将obj存入next的地址,随后+1。
pop
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
// 1.
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
page = hotPage();
if (!page) {
return setHotPage(nil);
}
page = coldPage();
token = page->begin();
} else {
page = pageForPointer(token);
}
// 2.
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
} else {
return badPop(token);
}
}
// 3.
return popPage<false>(token, page, stop);
}
调用pop函数时,有三步处理。
- 判断autoreleasepool是否为空,通过EMPTY_POOL_PLACEHOLDER占位符判断,为空则清空这个page。
- 传入的stop是否不等于POOL_BOUNDARY标识,如果不等于则可能是一个有问题的page。
- 调用popPage方法,释放对象。
popPage
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
- popPage函数核心代码就是调用releaseUntil函数,在最开始会调用releaseUntil函数去完成释放操作。
- 按照page达到一半就扩容的原则,后面的if语句会判断执行pop后page链表的状态。
2.1 如果少于半满,就将子节点删除。
2.2 如果大于半满,则保留子节点,并删除后面的节点。
releaseUntil
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();//获取当前的page
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) {
objc_release(obj);
}
}
setHotPage(this);
}
在releaseUntil
函数内部,核心逻辑是从当前page,从后到前调用objc_release
,释放被autorelease修饰的对象
。
获取当前的hotPage。
判断page是否为空,如果为空则表示里面的对象被释放完,则将page的父节点page设置为hotPage。
获得上一个节点,->的算数优先级比--要高
,所以是先通过next获取当前节点地址,这是一个为空的待存入节点,随后执行--操作获取上一个对象地址。
通过memset将上一个节点释放
。
判断上一个节点是否占位符号POOL_BOUNDARY,如果不是则调用objc_release释放对象。
在while循环结束后,将当前page设置为hotPage。
autorelease
static inline id autorelease(id obj)
{
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
return obj;
}
对象调用autorelease
方法会被编译器转换为objc_autoreleaseReturnValue
方法,并且经过多层调用,会来到底层的autorelease函数
。
在这个函数中会判断传入的对象是否tagged pointer
,因为tagged pointer没有引用计数的概念。随后会调用autoreleaseFast
函数,函数内部调用add函数将obj对象加入到page中,并且会判断是否需要创建新的page。
hotPage、coldPage
hotPage
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;
}
hotPage
可以被理解为,page链表的末尾
,也就是调用push函数被插入的位置
。执行hotPage
函数获取,以及调用setHotPage
设置,都是操作链表的末尾page
。
AutoreleasePoolPage对象和线程一一对应
,并且都被存储在tls的哈希表
中。通过tls_get_direct
函数并传入key可以获取
到对应的自动释放池
。
hotPage函数中的判断是下面的定义,这个标示意思是当前page为空,也就是从未存储过任何对象。是一个标志位,下面是标志位的定义。
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
coldPage
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
coldPage只有获取函数,没有设置函数。这是因为coldPage
函数本质上,就是寻找page链表的根节点
,从源码中的while循环可以看到。
调试
_objc_autoreleasePoolPrint
如果想调试自动释放池,可以通过_objc_autoreleasePoolPrint私有API来进行。将项目改为MRC,并且在命令行项目中增加下面这些调试代码。
int main(int argc, const char * argv[]) {
_objc_autoreleasePoolPrint(); // print1
@autoreleasepool {
_objc_autoreleasePoolPrint(); // print2
Person *p1 = [[[Person alloc] init] autorelease];
Person *p2 = [[[Person alloc] init] autorelease];
_objc_autoreleasePoolPrint(); // print3
}
_objc_autoreleasePoolPrint(); // print4
return 0;
}
打印结果如下,可以看到POOL_BOUNDARY在page中也占了一个位置。
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]: ##############
UIApplicationMain
项目中经常会看到下面的代码,很多人的解释是“这个autoreleasepool是为了释放主线程的autorelease对象的”。但是,这个说法是错误的。autoreleasepool只负责自己作用域中添加的对象,而主线程在运行过程中,也会隐式创建autoreleasepool对象,这个pool是包含在main函数的pool里面的。
所以,主线程runloop每次执行循环后,释放的对象是主线程的。而main函数的autoreleasepool释放的,是main函数中直接创建的对象。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
释放时机
区分
如果是在viewDidLoad方法中创建一个autorelease对象,并不是在这个方法结束后释放对象,这个说法是错误的。即便执行到viewDidAppear,依然不会释放对象。
被autorelease修饰的对象,释放时机有两种。
- 如果通过代码添加一个autoreleasepool,在作用域结束时,随着pool的释放,就会释放pool中的对象。这种情况是及时释放的,并不依赖于runloop。
- 另一种就是由系统自动进行释放,系统会在runloop开始的时候创建一个pool,结束的时候会对pool中的对象执行release操作。
runloop
如果是系统创建的pool,需要手动开启runloop,主线程默认已经开启并运行,子线程需要调用currentRunLoop方法开启并运行runloop,子线程中系统创建pool的流程才会正常工作。
包括主线程在内的每个线程,如果在线程中使用到了AutoreleasePool,则会创建两个Observer并添加到当前线程的Runloop中,通过这两个Observer
进行对象的自动内存管理
。
// activities = 0x1,kCFRunLoopEntry
<CFRunLoopObserver 0x60000012f000 [0x1135c2bb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
// activities = 0xa0,kCFRunLoopBeforeWaiting | kCFRunLoopExit
<CFRunLoopObserver 0x60000012ef60 [0x1135c2bb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10eee6276)}
首先会创建一个Observer并监听kCFRunLoopEntry消息
,时机是在进入Runloop前,此Observer的优先级设置为-2147483647的最高优先级,以保证回调发生在Runloop其他事件前。
然后创建另一个Observer,并监听kCFRunLoopBeforeWaiting和kCFRunLoopExit消息
,时机分别在进入Runloop休眠和退出Runloop时,将Observer的优先级设置为2147483647,以保证回调发生在Runloop其他事件之后。
两个Observer都有相同的回调函数_wrapRunLoopWithAutoreleasePoolHandler
,在第一次回调时会在内部调用_objc_autoreleasePoolPush
函数,创建自动释放池
。
在kCFRunLoopBeforeWaiting
将要进入休眠前,调用_objc_autoreleasePoolPop
函数释放自动释放池中的对象,并调用_objc_autoreleasePoolPush
函数创建一个新的释放池。在kCFRunLoopExit
将要退出Runloop时调用_objc_autoreleasePoolPop
函数,释放自动释放池中的对象。