iOS 内存管理之AutoReleasePool

背景

自从苹果推出了ARC管理内存后,对于iOS开发这而言,内存管理就变得so easy了,只要正确使用相关规则,再也不用担心double release,野指针的等问题了,而ARC的背后,除了强大的编译器之外,还要得益于运行时起作用的AutoReleasePool。

研究AutoReleasePool

iOS的项目中,除了特别需求外,整个项目就一个地方明确写了autoReleasePool的代码了,就是main函数:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

autoreleasepool做了什么?

我们知道oc代码在编译期间都会转化为c/c++代码,然后转化为汇编,最终转化为对应的架构的二进制文件;也可以这么说,oc的底层实现就是c/c++,既然这样,我们把他转化为对应的c/c++代码应该就可以窥探到其中的密码了:
转化为c/c++代码,Xcode有自带的工具,打开命令行,输入一下命令就可以:

xcrun -sdk iphoneos   clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp

为了减少代码量,重新建了一个macOS命令行项目:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}

转化为cpp文件,看下对应的代码:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_k5_h0x40m15075dxn_z956dk2500000gn_T_main_6e2ecd_mi_0);
    }
    return 0;
}

从上面的C++源码可以发现@autoreleasepool {}最后变成了:

{ __AtAutoreleasePool __autoreleasepool;//定义了一个__AtAutoreleasePool结构体变量
。。。
 }

我们分析下流程:
1、进入大括号,定义了一个__AtAutoreleasePool的结构体局部变量;
2、定义这个结构体变量的时候,会走结构体的构造方法,间接的会调用objc_autoreleasePoolPush函数:

__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}

3、当走出大括号是,局部变量__autoreleasepool,会被销毁,因此会走结构体的析构函数,间接就会调用objc_autoreleasePoolPop函数:

  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}

从上面的分析,我们发现了两个重要的函数objc_autoreleasePoolPush和objc_autoreleasePoolPop,这两个函数是全局函数,而且是以objc开头的,应该是在objc的源码中,下载objc源码的地址,macOS 最新系统下面的objc4。

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

他们调用的是AutoreleasePoolPage类对应的push跟pop两个静态函数,那么我们就要研究下AutoreleasePoolPage这个类了。

研究AutoreleasePoolPage

class AutoreleasePoolPage :
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
...
}

从AutoreleasePoolPage的成员变量可以分析出,AutoreleasePoolPage是一个双向链表的结构,每个实例都会存在一个parent实例指针,跟一个child实例指针。其他成员变量暂时不知道表示什么意思,只能继续研究AutoreleasePoolPage的实现逻辑了,还是从push跟pop函数入手:

AutoreleasePoolPage 的push函数:

  static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page .debug模式新建一个page对象,实际不需要关注
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {//实际走这里
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

追根溯源autoreleaseFast:

static inline id *autoreleaseFast(id obj)
    {
        AutoreleasePoolPage *page = hotPage();
//拿到当前正在被使用的page,因为每个page都是有对象obj数量限
//制的,当page放满了,就会创建一个child page来继续放。
        if (page && !page->full()) {//没满,直接添加到当前的page上
            return page->add(obj);//添加obj
        } else if (page) {//full,满了
            return autoreleaseFullPage(obj, page);//将obj添加到对应的未满的child page里面,并将其设置为hot page
        } else {//没有page
            return autoreleaseNoPage(obj);
        }
    }
 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;
    }
 static __attribute__((noinline))
    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;//若有child page,将当前的指针指向child page
            else page = new AutoreleasePoolPage(page);//new一个新的page,并将其赋值给child page;
        } while (page->full());//page是否满了,没满,跳出

        setHotPage(page);//将page设置为当前hotpage
        return page->add(obj);//添加obj到page
    }
    id *autoreleaseNoPage(id obj)
    {
        // "No page" could mean no pool has been pushed
        // or an empty placeholder pool has been pushed and has no contents yet
        assert(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            // We are pushing a second pool over the empty placeholder pool
            // or pushing the first object into the empty placeholder pool.
            // Before doing that, push a pool boundary on behalf of the pool 
            // that is currently represented by the empty placeholder.
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         pthread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();//设置一个占位的空的page
        }

        // We are pushing an object or a non-placeholder'd pool.

        // Install the first page.第一次创建一个page
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);//并将它设置为hotpage
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);//添加哨兵对象
        }
        
        // Push the requested object or pool.
        return page->add(obj);//添加obj
    }
   id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));//page的起始地址+成员变量的大小
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);///一个page的大小是size,4096字节
    }

    bool empty() {
        return next == begin();
    }

    bool full() { 
        return next == end();
    }
  id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;//将obj的指针赋值给next所在的位置,然后将next指向下一个位置
        protect();
        return ret;//返回前一个obj
    }

总结一下:

  1. push操作从取出当前的hotpage,然后将一个哨兵对象(其实是nil)放入到page的next位置,并将next指向的位置向下+1;
  2. 取出的hotpage存在,但是hotpage是一个fullpage(一个page,PAGE_MAX_SIZE = 4096字节大小,除了存放内部成员变量的值之外,其他的都用来存放autorelease对象的地址),这时就会循环查找他的child指向的page对象,知道找到没使用完的child page,如果没有child page,则创建一个,将找到的child page并设置为hotpage,然后将一个哨兵对象添加进去;
  3. 取出的当前hotpage不存在,则通过autoreleaseNoPage创建一个新的page,并设置为hotpage,然后将然后将一个哨兵对象添加进去;
  4. 这样做的结果,除了第一层page(没有parent page),每次push返回的obj的都是哨兵对象,最开始的push返回的是第一层page最开始的位置page.begin(),后面的pop会用到这个返回值。

AutoreleasePoolPage 的pop函数:

    static inline void pop(void *token) 
    {//token就是对应push返回值,上面提到过要么是哨兵对象,要么是第一层page最开始的位置
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                //要么是第一层page最开始的位置
                // 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 {//其他情况不存在,坏的page
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(token);
            }
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);//释放存放在page的指针所指的对象,直到遇到哨兵对象或者全部释放完成

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {//将page对象释放掉
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }
    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) {//循环释放对象,直到遇到哨兵对象或者全部释放完
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {//当前page释放完,还没遇到哨兵对象,拿到parent page,并设置为hotpage,继续释放,直到遇到哨兵对象或者全部释放完
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;//拿到obj对象,并将next指针指向上一个位置
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));//清空next位置,这里是设置为0x3A
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);//释放掉对象
            }
        }

        setHotPage(this);//释放完后,将当前page设置为hotpage

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

pop函数总结几点:

  1. 会拿到最近一次push函数(pop函数与push一一对应)返回的哨兵对象,作为pop函数的入参;
  2. 遍历hotpage的next指针指向的对象,并释放,直到遇到入参的哨兵对象;
  3. 如果当前page释放完了,还没遇到哨兵对象,就会往parent page遍历,直到遇到哨兵对象,一次类推;
  4. 最后释放掉为空(empty)的page对象,但是需要注意的是:当他的parent的使用空间超过了1/2,保留它对应的child page。

从上面的分析大致了解了AutoreleasePoolPage的工作流程,在程序运行的时候是怎么样工作的呢?
我们知道,在MRC时代,需要程序员手写对象的retain和release,后面最智能的就是new一个对象的时候,我们需要带上autorelease代码:

[[[NSObject alloc] init] autorelease];//MRC手动管理内存

到了ARC,我们不需要这样写,因为编译器在编译的时候,会自动帮忙加上这些代码,所以说不管是ARC还是MRC时期,oc对象的内存管理入口都是autorelease方法:

autorelease方法的研究:

- (id)autorelease {
    return ((id)self)->rootAutorelease();
}
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;//tagPointer 不需要
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);//调用的是AutoreleasePoolPage的autorelease函数
}
static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);//调用autoreleaseFast,上面提到过
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

就是说,每个oc对象创建的时候(alloc/new/copy/mutableCopy),都是通过autorelease方法添加到page对象中的。pop那具体的调用时机又是什么呢?我们知道,项目只在main函数有一个autoreleasepool,其他的地方除非程序员自己手动添加,就不会有了,也就是说:我们暂且认为编译器帮忙添加的autorelease,那也只是添加进page,pop函数还是只有一个,而且是程序退出的时候调用,如果是这种情况的话,程序整个运行期间,内存得不停的增长,因为只有申请,没有释放。显然这种做法是行不通的 。

那么系统是怎么做的呢?直接说结论:我们知道程序运行期间是通过runloop维持的,而runloop就是不停的监听事件和timer,处理事件和timer,没有事件和timer的时候就进入休眠,系统会在runloop里面添加了两个autorelease相关的observers:
autorelease相关的observers.png
RunLoop& AutoReleasePool关系几点说明:
  1. App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
  2. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
  3. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

总结

通过上面的源码及流程的分析,我们对于autoreleasepool的工作原理及流程有了充分的了解:

  1. autoreleasepool底层的数据结构是一个autoreleasepage的双向链表,每个page的大小为4096字节,除了存储成员变量的大小,其他的位置都用来存储autorelease对象的地址,next变量永远指向下一个可以存放autorelease对象地址的地址空间,具体结构如下:
    数据结构
  2. 当一个page1存储空间用完后,会创建一个新的page2,新的page2的parent指针指向满了的page1,page1的child指针会指向page2;
  3. 程序启动就会创建一个autoreleasepool,会调用push,程序结束时,最后会调用pop,回收所有autorelease对象的内存;
  4. 程序运行期间,同过监听runloop的休眠状态,调用push/pop方法,管理autorelease对象;
  5. 每次push的时候,会往page里面添加一个哨兵对象,这个哨兵对象作为下次pop函数的入参,遇到哨兵对象,说明这次runloop循环添加到page的autorelease对象release完毕。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270