iOS 性能优化-自动释放池

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_autoreleasePoolPushobjc_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源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容