swift AutoreleasePool 原理
刘小壮 人气:0使用场景
在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
最大的作用,是在大量创建对象的同时,通过修饰让内存得到提前释放,从而降低内存峰值。
@autoreleasepool { NSMutableArray *channelItemsJSONArray = [NSMutableArray arrayWithContentsOfFile:[self cachedChannelItemsFile]]; NSArray *items = [self channelItemsJSONArray]; if (![items writeToFile:[self cachedChannelItemsFile] atomically:YES]) { [channelItemsJSONArray writeToFile:[self cachedChannelItemsFile] atomically:YES]; } items = nil; }
__autoreleasing
在ARC
下,需要被自动释放的对象,可以用__autoreleasing
修饰,让对象延迟释放。
+ (NSArray *)parseString:(NSString *)originalM3U8Str m3u8Host:(NSString *)m3u8url error:(NSError *__autoreleasing *)errorPtr;
源码分析
__AtAutoreleasePool结构体
struct __AtAutoreleasePool { __AtAutoreleasePool() { atautoreleasepoolobj = objc_autoreleasePoolPush(); } ~__AtAutoreleasePool() { 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
的定义中,可以看到有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); }
autoreleaseNoPage
autoreleaseNoPage
函数的核心代码比较简单,就是创建一个新的page
,随后设置POOL_BOUNDARY
标志,并且把对象添加进去。在函数中需要留意POOL_BOUNDARY
标志,很多地方都用来做page
是否为空的判断。
static __attribute__((noinline)) id *autoreleaseNoPage(id obj) { AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } return page->add(obj); }
add
add
函数比较简单,核心逻辑就是将obj
放入next
指针的位置,并且对next
指针进行++
,指向下一个位置。*next++
表示先用后加,先将obj
存入next的地址,随后+1
。
id *add(id obj) { ASSERT(!full()); unprotect(); id *ret = next; *next++ = obj; protect(); return ret; }
pop
调用pop
函数时,有三步处理。
- 判断
autoreleasepool
是否为空,通过EMPTY_POOL_PLACEHOLDER
占位符判断,为空则清空这个page
。 - 传入的
stop
是否不等于POOL_BOUNDARY
标识,如果不等于则可能是一个有问题的page
。 - 调用
popPage
方法,释放对象。
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); }
popPage
popPage
函数核心代码就是调用releaseUntil
函数,在最开始会调用releaseUntil
函数去完成释放操作。
按照page
达到一半就扩容的原则,后面的if
语句会判断执行pop
后page
链表的状态。
如果少于半满,就将子节点删除。
如果大于半满,则保留子节点,并删除后面的节点。
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(); } } }
releaseUntil
在releaseUntil
函数内部,核心逻辑是从当前page
,从后到前调用objc_release
,释放被autorelease
修饰的对象。
- 获取当前的
hotPage
。 - 判断
page
是否为空,如果为空则表示里面的对象被释放完,则将page
的父节点page
设置为hotPage
。 - 获得上一个节点,
->
的算数优先级比--
要高,所以是先通过next
获取当前节点地址,这是一个为空的待存入节点,随后执行--
操作获取上一个对象地址。 - 通过
memset
将上一个节点释放。 - 判断上一个节点是否占位符号
POOL_BOUNDARY
,如果不是则调用objc_release
释放对象。 - 在
while
循环结束后,将当前page
设置为hotPage
。
void releaseUntil(id *stop) { while (this->next != stop) { AutoreleasePoolPage *page = hotPage(); 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); }
autorelease
对象调用autorelease
方法会被编译器转换为objc_autoreleaseReturnValue
方法,并且经过多层调用,会来到底层的autorelease
函数。
在这个函数中会判断传入的对象是否tagged pointer
,因为tagged pointer
没有引用计数的概念。随后会调用autoreleaseFast
函数,函数内部调用add
函数将obj
对象加入到page
中,并且会判断是否需要创建新的page
。
static inline id autorelease(id obj) { assert(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); return obj; }
hotPage
hotPage
可以被理解为,page
链表的末尾,也就是调用push
函数被插入的位置。执行hotPage
函数获取,以及调用setHotPage
设置,都是操作的链表的末尾page
。
AutoreleasePoolPage
对象和线程一一对应,并且都被存储在tls
的哈希表中。通过tls_get_direct
函数并传入key
可以获取到对应的自动释放池。
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
为空,也就是从未存储过任何对象。是一个标志位,下面是标志位的定义。
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
coldPage
coldPage
只有获取函数,没有设置函数。这是因为coldPage
函数本质上,就是寻找page
链表的根节点,从源码中的while
循环可以看到。
static inline AutoreleasePoolPage *coldPage() { AutoreleasePoolPage *result = hotPage(); if (result) { while (result->parent) { result = result->parent; result->fastcheck(); } } return result; }
调试
_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
函数,释放自动释放池中的对象。
加载全部内容