AutoreleasePool是OC中的一种自动回收机制,在ARC的模式下已经很少能看到autorelease了,它可以延迟变量release的时机。在OC的main.m中就有一个autoreleasepool,本篇结合runtime研究一下autoreleasepool的底层是如何实现的。
问答模式
问:什么时候需要使用自动释放池?
官方解释:基本分为如下三点
1、当我们需要创建大量的临时变量的时候,可以通过@autoreleasepool 来减少内存峰值。
2、创建了新的线程执行Cocoa调用。
3、如果您的应用程序或线程是长期存在的,并且可能会生成大量自动释放的对象,那么您应该定期清空并创建自动释放池(就像UIKit在主线程上所做的那样);否则,自动释放的对象会累积,内存占用也会增加。但是,如果创建的线程不进行Cocoa调用,则不需要创建自动释放池。
问:为什么会减少内存峰值?
答:借用YYImage的代码打个比方。
比如业务需要在一个代码块中需要创建大量临时变量,或临时变量足够大,占用了很多内存,可以在临时变量使用完以后就立即释放掉,在ARC的环境下只能通过自动释放池实现。
if ([UIDevice currentDevice].isSimulator) {
@autoreleasepool {
NSString *outPath = [NSString stringWithFormat:@"%@ermilio.gif.png",IMAGE_OUTPUT_DIR];
NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:gif]);
[outData writeToFile:outPath atomically:YES];
[gif writeToFile:[NSString stringWithFormat:@"%@ermilio.gif",IMAGE_OUTPUT_DIR] atomically:YES];
}
@autoreleasepool {
NSString *outPath = [NSString stringWithFormat:@"%@ermilio.apng.png",IMAGE_OUTPUT_DIR];
NSData *outData = UIImagePNGRepresentation([UIImage imageWithData:apng]);
[outData writeToFile:outPath atomically:YES];
[apng writeToFile:[NSString stringWithFormat:@"%@ermilio.png",IMAGE_OUTPUT_DIR] atomically:YES];
}
@autoreleasepool {
NSString *outPath = [NSString stringWithFormat:@"%@ermilio_q85.webp.png",IMAGE_OUTPUT_DIR];
NSData *outData = UIImagePNGRepresentation([YYImageDecoder decodeImage:webp_q85 scale:1]);
[outData writeToFile:outPath atomically:YES];
[webp_q85 writeToFile:[NSString stringWithFormat:@"%@ermilio_q85.webp",IMAGE_OUTPUT_DIR] atomically:YES];
}
}
再比如在循环的场景下,如果创建大量的临时变量,会使内存峰值持续增加,加入自动释放池以后,在每次循环结束时,超出自动释放池的作用域,使得内部的大量临时变量被释放,从而大大降低了内存的使用。
for (int i = 0; i < count; i++) {
@autoreleasepool {
id imageSrc = _images[i];
NSDictionary *frameProperty = NULL;
if (_type == YYImageTypeGIF && count > 1) {
frameProperty = @{(NSString *)kCGImagePropertyGIFDictionary : @{(NSString *) kCGImagePropertyGIFDelayTime:_durations[i]}};
} else {
frameProperty = @{(id)kCGImageDestinationLossyCompressionQuality : @(_quality)};
}
}
上述这几种情况如果没必要就别这么写,毕竟创建自动释放池也需要耗费内存。
自动释放池的实现原理
在开始之前先看一下自动释放池的大致结构图
自动释放池结构图.png上图就是自动释放池的结构图,可能现在看不懂,这里先有个概况继续往下看就明白了,不太会画图,反正意思表达出来了。
查看main.cpp
我们先在终端clang一下main.m,变成C++实现
clang -rewrite-objc main.m -o main.cpp
我们会得到一个main.cpp文件,打开这个文件翻到最底部会看到这个代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_vg_bngxds5x5q90wwst5gl1jq140000gn_T_main_1b100d_mi_0);
}
return 0;
}
发现原来autoreleasepool也是一个对象,我们在这个cpp文件中查找__AtAutoreleasePool
,找到如下的结构体
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
结构体只提供了一个构造函数和一个析构函数,里面分别调用了objc_autoreleasePoolPush
和objc_autoreleasePoolPop
,这个objc前缀告诉我们,是不是能到runtime里面搜索一下,在rumtime源码中全局搜索objc_autoreleasePoolPush
,找到这个函数
void * objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
我们发现了正主,是一个类AutoreleasePoolPage
AutoreleasePoolPage
class AutoreleasePoolPage
{
···
//当自动释放池为空时的一个占位符
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
//边界符,用来区别每个AutoreleasePoolPage的边界
# define POOL_BOUNDARY nil
//线程的key,通过key值寻找线程下的AutoreleasePoolPage
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
//4096个字节,表示每个page的大小,因为虚拟内存每个扇区4096个字节
PAGE_MAX_SIZE;
//一个page里面的对象数量
static size_t const COUNT = SIZE / sizeof(id);
//共需要占用56个字节
magic_t const magic; // 16字节,校验完整性的变量
id *next; // 8字节,指向下一个对象的指针
pthread_t const thread; // 8字节,所属线程,page和thread是一一对应关系
AutoreleasePoolPage * const parent; // 8字节,父节点,指向上一个page
AutoreleasePoolPage *child; // 8字节,子节点,指向下一个page
uint32_t const depth; // 4字节,表示链表一共有多少个节点
uint32_t hiwat; // 4字节,high water marks表示自动释放池中最多能存放的对象个数
···
}
从这个类中我们得到了以下内容
- EMPTY_POOL_PLACEHOLDER:
当自动释放池为空时的一个占位符,就是在第一次push
时,先用这个字段把AutoreleasePoolPage的位置占上。
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
- POOL_BOUNDARY :
边界符,用来区别每个AutoreleasePoolPage的边界,我们从创建page的时候可以得知
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);//设置活跃页
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
- key:
线程的key,通过key值寻找线程下的AutoreleasePoolPage - PAGE_MAX_SIZE:
4096个字节,表示每个page的大小,因为虚拟内存每个扇区4096个字节 - COUNT:
一个page里面的对象数量 - magic:
校验完整性的变量,占用16字节 - next:
指向下一个对象的指针,占用8字节 - thread:
所属线程,page和thread是一一对应关系,占用8字节 - parent:
父节点,指向上一个page,占用8字节,看到这里我们发现这个自动释放池其实是个双向链表,不过是以栈的形式存取的 - child:
子节点,指向下一个page,占用8字节 - depth:
表示链表一共有多少个节点,占用4字节 - hiwat:
high water marks表示自动释放池中最多能存放的对象个数,占用4字节
从上面的分析我们可以得知,page本身占用了56个字节,而一个AutoreleasePoolPage一共4096个字节,也就是说我们还剩下4040个字节可以用来放对象。接下来看看它的push和pop的过程。
1、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);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
这里我们不看Debug,直接找autoreleaseFast函数
static inline id *autoreleaseFast(id obj)
{
//获取当前活跃的page
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()
函数;当对象满了时,调用了autoreleaseFullPage()
函数;当没获取到page时,调用autoreleaseNoPage()
函数。接下来我们看看这几个函数都做了什么
1.1、hotPage()
static inline AutoreleasePoolPage *hotPage()
{
//从一个键值对中获取当前page
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
从代码中我们得知hotPage函数是从一个键值对中获取当前活跃的page,而这个key就是上面我们看到的
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
1.2、page->add(obj)
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
从源码中我们得知,add()
是向链表中增加一个对象,简单的改变了指针的指向,这不必细说。
1.3、autoreleaseFullPage(obj, page)
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);
}
从源码中我们得知,这个page满了以后,循环遍历自动释放池中的page,直到找到一个page不满时,我们把对象添加进去。
1.4、autoreleaseNoPage(obj)
id *autoreleaseNoPage(id obj)
{
bool pushExtraBoundary = false;
//判断是否有空池占位符
if (haveEmptyPoolPlaceholder()) {
pushExtraBoundary = true;
} else if (obj != POOL_BOUNDARY && DebugMissingPools) {
//没有可用pool
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
//当前page还没有空池占位符,先加上占位符
return setEmptyPoolPlaceholder();
}
//如果执行到这里,表示目前没有可有page,要新建一个
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);//设置激活页
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
//如果有占位符,则这里加上边界符
page->add(POOL_BOUNDARY);
}
//把autorelease对象添加进来
return page->add(obj);
}
1.5、Push总结
AutoreleasePoolPage-push.png流程基本如上图所示
2、Pop
在Pop时,会传入当前的token,token就是
static inline void pop(void *token)
{
//token就是边界符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
//找到最上边的page,即当前的page
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
//讲道理,如果token不等于POOL_BOUNDARY,pageForPointer()计算过后,理论上是一定会进入这里的
} else {
//走这就出问题了
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
//更新当前自动释放池最大存储数
if (PrintPoolHiwat) printHiwat();
//清空token之前的autorelease对象
page->releaseUntil(stop);
//清空操作
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
从源码中我们看到了Pop的过程分成了三步,
1、判断token
是否等于EMPTY_POOL_PLACEHOLDER
首先我们要知道token
实际上是个边界符,通常情况下等于POOL_BOUNDARY
,其次我们要记得上面说过自动释放池其实是个双向链表,不过是以栈的形式存取的,所以当执行这个判断条件时,实际上就是Pop到了最后一步了。
2、当token
不等于POOL_BOUNDARY时
这一步一般是不会进来的,只有在没有自动释放池且调用了autorelease时才会出现。但生活还是要继续的...
在做接下来的操作前,先获取最新的page,即当前page
page = pageForPointer(token);
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;//size就是4096,每个page最大size
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
下面这个操作知识为了确保这个token拿到的page没问题。
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
//讲道理,如果token不等于POOL_BOUNDARY,pageForPointer()计算过后,理论上是一定会进入这里的
// 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);
}
}
3、最后一步释放page里面的对象。
在释放操作之前,更新当前自动释放池最大存储数。
if (PrintPoolHiwat) printHiwat();
static void printHiwat()
{
AutoreleasePoolPage *p = hotPage();
uint32_t mark = p->depth*COUNT + (uint32_t)(p->next - p->begin());
if (mark > p->hiwat && mark > 256) {
for( ; p; p = p->parent) {
p->unprotect();
p->hiwat = mark;
p->protect();
}
这一步操作释放token之前的autorelease对象。
//释放token之前的autorelease对象
page->releaseUntil(stop);
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) {
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);
}
}
kill操作,如果当前page小于当前page的一半时,则把当前页的所有子节点都kill掉,否则从子节点的子节点开始kill。
//清空操作
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
到目前为止,我们明白了autorelease对象的释放是在autoreleasePool释放之前。
参考资料
autorelease和autoreleasePoolPage--你真的了解么?
OC源码 —— autoreleasepool
官方runtime源码
网友评论