探秘AutoreleasePool实现原理
本文字数:10673字
预计阅读时间:27分钟
使用
使用场景
在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() {
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、coldPage
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
函数,释放自动释放池中的对象。
也许你还想看
(▼点击文章标题或封面查看)
视频下载加速,2.5倍优化
2022-04-21
探秘OC消息发送机制
2022-03-17
KVO原理分析介绍
2021-11-18
干货:探秘WKWebView
2021-10-21
RocketMQ中台化建设
2021-01-14