autoreleasePool实现原理详解

在我们平时开发中,经常会涉及到对象的使用,但我们往往都不知道我们所开辟的对象是在何时被回收的,有没有及时的被释放,在苹果引入ARC之后,平时书写OC代码的时候都很少对内存释放做过处理,但是有一些场景还是需要我们手动去管理内存的释放;比如大量的for循环创建对象时候,比如子线程中存在持续性任务没办法及时关闭,这个时候也要我们手动管理临时变量的释放;

概念:对象执行 autorelease 方法或者直接在 autoreleasePool 中创建对象,会将对象添加到 autoreleasePool 中,当自动释放池销毁的时候,会对所有对象做 release 操作;

创建autoreleasePool

MRC环境创建:

   // 1、生成一个NSAutoreleasePool对象
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // 2、调用autorelease方法
    id object = [[NSObject alloc] init];
    [object autorelease];
    // 3、对象销毁
    [pool drain];

ARC环境创建:

@autoreleasepool {
     //LLVM
     id object = [NSArray array];
}

MRC环境下,我们创建的对象需要调用autorelease添加到自动释放池中,而ARC环境下,我们只要通过@autoreleasepool就可以;


通过查找源码会发现,ARC内部其实已经帮我们自动实现了autorelease方法,帮我们添加到autoreleasePool中;

#ifndef AUTORELEASE
/**
 *  Basic autorelease operation ... calls [NSObject-autorelease]<br />
 *  Does nothing when ARC is in use.
 */
#define AUTORELEASE(object) [(id)(object) autorelease]
#endif

接下来就是分析autoReleasePool内部具体的工作原理;
因为main入口函数就为我们提供了一个现成的autoReleasePool

所以可以直接通过clang得到编译后的内容,找到autoReleasePool

clang main.m -rewrite-objc

会发现@autoreleasepool本质上就是帮我们生成一个__AtAutoreleasePool对象


通过检索__AtAutoreleasePool会发现,它其实就是一个结构体,里面包含了两个函数,一个构造函数,一个析构函数;当autoreleasepool初始化的时候就会调用__AtAutoreleasePool()构造函数,当作用域离开的时候就会调用析构函数~__AtAutoreleasePool();

构造函数就是通过objc_autoreleasePoolPush()生成atautoreleasepoolobj对象,析构函数就是调用objc_autoreleasePoolPop函数,把atautoreleasepoolobj传递进去,这时候我们发现,我们应该重点分析的方法是objc_autoreleasePoolPush及objc_autoreleasePoolPop;

这个时候就得查看官方源码了;

objc_autoreleasePoolPush

在NSObject.mm定位到objc_autoreleasePoolPush,内部是直接调用了AutoreleasePoolPage的push方法;


到这里又引出了新的类-->AutoreleasePoolPage,进一步观察会发现它是继承自AutoreleasePoolPageData;


进入AutoreleasePoolPageData,会发现AutoreleasePoolPage其实是个双向链表,里面包含parent指针跟child指针,指向前后两个page节点;

接下来我们就来探索一下push方法的实现;点击去看会发现push其实就是静态内联函数,



push方法里面判断自动释放池AutoreleasePoolPage是否初始化过了,初始化过了就调用autoreleaseFast,否则就调用autoreleaseNewPage初始化一个新的page,观察发现这两个方法里面都传入了一个POOL_BOUNDARY参数,这个又是什么呢??
#   define POOL_BOUNDARY nil

点击去看会发现POOL_BOUNDARY是一个nil,这个就有点懵了,传进去nil有何意义???

POOL_BOUNDARY

POOL_BOUNDARY其实是一个哨兵对象也可以看成一个边界,用来分割不同的@autoreleasepool;

我们有时候会为了代码的可读性,或使用多层@autoreleasepool的嵌套,这些autoreleasepool会用同一个AutoreleasePoolPage对象。以下面的三个嵌套为例,在同一个page中的顺序是下图这样。不同的@autoreleasepoolPOOL_BOUNDARY做分割。

@autoreleasepool {
    NSObject *p1 = [[NSObject alloc] init];
    NSObject *p2 = [[NSObject alloc] init];
    @autoreleasepool {
        NSObject *p3 = [[NSObject alloc] init];
        @autoreleasepool {
            NSObject *p4 = [[NSObject alloc] init];
        }
    }
}

autoreleaseFast

接下来分析一下autoreleaseFast方法,在函数内部,通过hotPage获取当前的page,

1、如果page存在并且page有存储空间,就调用add方法将对象存到page中;

2、如果page存在但是没有存储空间,这个时候就调用autoreleaseFullPage函数创建新的page,并且将链表的末尾指向新创建的page

3、如果没有page不存在,则调用autoreleaseNoPage函数创建一个新的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);
        }
    }

add

add函数核心逻辑就是将obj放入next指针的位置,并且对next指针进行++,指向下一个位置。*next++表示先用后加,先将obj存入next的地址,随后+1;用ret保存next的地址,即上一个对象地址,并返回;

id *add(id obj) {
    ASSERT(!full());
    unprotect();
    id *ret = next;  
    *next++ = obj;
    protect();
    return ret;
}

这里又引发一个问题,next指针刚开始指向哪里呢???

通过进一步探索会发现,begin即this(即当前page的首地址)加上当前结构体的需要占用的内存空间,即自动释放池第一个对象从这里开始存储;

    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }

    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    }

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

    bool full() { 
        return next == end();
    }

简单画了一个流程粗略表示一个obj对象入栈的过程;

自动释放池push过程

那么每一个page能存储多少空间呢?

从最新的源码中可以看出,SIZE最大可以是PAGE_MAX_SIZE是1按位左移14,即end指针指向2^14+this;

autoreleaseFullPage

  1. autoreleaseFullPage函数中,会从page的链表中,从前往后找到末尾的节点child。

  2. 创建一个新的page,在创建函数AutoreleasePoolPage中会对parentchild指针做处理,返回的page可以直接用。

  3. 调用setHotPagepage设置到tls_set_direct哈希表中,tls_set_direct(key, (void *)page); 标记成当前可用的page;并且调用pageadd函数将autorelease修饰的对象,添加到page中。

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;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }

autoreleaseNoPage

autoreleaseNoPage函数的核心代码其实比较简单,就是创建一个新的page,并且也是一样,调用setHotPage设置当前可供使用的page;随后设置POOL_BOUNDARY哨兵对象,并且把对象添加进去。

static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
        // 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);
        }

        // Push the requested object or pool.
        return page->add(obj);
    }

objc_autoreleasePoolPop

分析完push的流程之后,接下来就分析一下pop的流程,即自动释放池释放的过程;

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

objc_autoreleasePoolPop函数内部其实也是用到AutoreleasePoolPage对象,只是调用pop函数;

pop

  1. 判断autoreleasepool是否为空,通过EMPTY_POOL_PLACEHOLDER占位符判断,为空则清空这个page

  2. autoreleasepool不为空,则调用pageForPointer方法,对传入的指针token取模,p % SIZE,再用p减去偏移量offset,拿到result返回给page使用,这个表示当前token在最后一个page上的位置;

  3. 传入的stop是否不等于POOL_BOUNDARY标识,如果不等于则可能是一个有问题的page

  4. 调用popPage方法,释放对象。

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

        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // 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);
            }
        }

        if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
            return popPageDebug(token, page, stop);
        }

        return popPage<false>(token, page, stop);
    }
static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;

        ASSERT(offset >= sizeof(AutoreleasePoolPage));

        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();

        return result;
    }

popPage

接下来就是探索一下popPage如何释放对象;

  1. popPage内部核心代码就是执行releaseUntil方法进行释放操作;

  2. 按照page达到一半就扩容的原则,后面的if语句会判断执行poppage链表的状态。

    1. 如果少于半满,就将子节点删除。

    2. 如果大于半满,则保留子节点,并删除后面的节点。

template<bool allowDebug> static void
    popPage(void *token, AutoreleasePoolPage *page, id *stop)
    {
        if (allowDebug && PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (allowDebug && 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) {
            // 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();
            }
        }
    }

releaseUntil

releaseUntil函数内部,核心逻辑是从当前page,从后到前调用objc_release,释放被autorelease修饰的对象。

  1. AutoreleasePoolPage *page = hotPage(),获取当前的hotPage

  2. 判断page是否为空,如果为空则表示里面的对象被释放完,则将page的父节点page设置为hotPage

  3. 获得上一个节点,->的算数优先级比--要高,所以是先通过next获取当前节点地址,这是一个为空的待存入节点,随后执行--操作获取上一个对象地址,跟前文提到的*next++ = obj有异曲同工之妙;

  4. 通过memset将上一个节点释放。

  5. 判断上一个节点是否占位符号POOL_BOUNDARY,如果不是则调用objc_release释放对象。

  6. while循环结束后,将当前page设置为hotPage

    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 = page->parent;
                setHotPage(page);
            }

            page->unprotect();
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
            AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

            // create an obj with the zeroed out top byte and release that
            id obj = (id)entry->ptr;
            int count = (int)entry->count;  // grab these before memset
#else
            id obj = *--page->next;
#endif
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                // release count+1 times since it is count of the additional
                // autoreleases beyond the first one
                for (int i = 0; i < count + 1; i++) {
                    objc_release(obj);
                }
#else
                objc_release(obj);
#endif
            }
        }

        setHotPage(this); 
    }

hotPage、coldPage概念

hotPage

hotPage可以被理解为最后一个可插入对象的page,即链表的末尾的page,当我们获取hotPage => AutoreleasePoolPage *page = hotPage() 以及setHotPage时,都是对链表末尾的page进行操作;

hotPage函数内部本质上是一个pagekey的映射。通过当前的AUTORELEASE_POOL_KEY存取;

static pthread_key_t const key = AUTORELEASE_POOL_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;
    }

通过tls_get_direct获取当前的page,这个tls其实就是thread local storage哈希表,即从当前线程的局部私有存储空间中取出该数据,只有当前线程才能访问的数据,这里就可以看出,autoreleasepool跟当前线程其实是有一定的对应关系,每个线程都有其对应的autoreleasepool,AutoreleasePoolPage对象和线程一一对应,;

coldPage

coldPage就是查询函数,通过查询链表page的根节点

static inline AutoreleasePoolPage *coldPage() 
    {
        AutoreleasePoolPage *result = hotPage();
        if (result) {
            while (result->parent) {
                result = result->parent;
                result->fastcheck();
            }
        }
        return result;
    }

page释放时机

下面这段主要参考探秘AutoreleasePool实现原理

autorelease修饰的对象,释放时机有两种。

  1. 如果通过代码添加一个autoreleasepool,在作用域结束时,随着pool的释放,就会释放pool中的对象。这种情况是及时释放的,并不依赖于runloop

  2. 另一种就是由系统自动进行释放,系统会在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,并监听kCFRunLoopBeforeWaitingkCFRunLoopExit消息,时机分别在进入Runloop休眠和退出Runloop时,将Observer的优先级设置为2147483647,以保证回调发生在Runloop其他事件之后。

两个Observer都有相同的回调函数_wrapRunLoopWithAutoreleasePoolHandler,在第一次回调时会在内部调用_objc_autoreleasePoolPush函数,创建自动释放池。

kCFRunLoopBeforeWaiting将要进入休眠前,调用_objc_autoreleasePoolPop函数释放自动释放池中的对象,并调用_objc_autoreleasePoolPush函数创建一个新的释放池。在kCFRunLoopExit将要退出Runloop时调用_objc_autoreleasePoolPop函数,释放自动释放池中的对象。

对象添加到自动释放池过程

讲完了autoreleasePool的开辟跟回收过程,以及回收时机,最后对象添加到自动释放池过程

前文已经提到了,系统LLVM会在帮我们自动的调用一个autorelease方法,我们就顺着这个方法看看,它是如何一步步的插入到自动释放池中的;

-(id) autorelease
{
    return _objc_rootAutorelease(self);
}

NEVER_INLINE id
_objc_rootAutorelease(id obj)
{
    ASSERT(obj);
    return obj->rootAutorelease();
}
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj)
    {
        ASSERT(!obj->isTaggedPointerOrNil());
        id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
        ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
#endif
        return obj;
    }

1、autorelease先调用_objc_rootAutorelease方法

2、_objc_rootAutorelease内部在调用当前对象的rootAutorelease方法

3、rootAutorelease方法内部再调用当前对象的rootAutorelease2方法

4、rootAutorelease2方法在调用AutoreleasePoolPage::autorelease方法;

5、AutoreleasePoolPage::autorelease方法最后是调用autoreleaseFast,这个时候就跟我们分析autoreleasePush中的方法遥相呼应了;

总结就是:objc_object::autorelease >> objc_object::_objc_rootAutorelease >> _objc_rootAutorelease >> objc_object::rootAutorelease >> objc_object::rootAutorelease2 >> AutoreleasePoolPage::autorelease >> AutoreleasePoolPage::autoreleaseFast

注:
ARC的规则:alloc/new/copy/mutableCopy 开头的方法返回的对象不是 autorelease 对象;在我们平时开发中,不要出现alloc/new/copy/mutableCopy 开头的方法,否则很容易会导致内存泄露;

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

推荐阅读更多精彩内容