运用

运用场景

ARC下,AutoreleasePool主要应用在大量创立临时目标的场景,经过AutoreleasePool操控内存峰值,是一个很好的挑选。

NSAutoreleasePool

MRC能够调用NSAutoreleasePool使目标延迟开释,在ARC下这个API现已被禁用。

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
// ...
[pool release];

@autoreleasepool

除了NSAutoreleasePool还能够运用@autoreleasepool,而且苹果推荐运用@autoreleasepool,由于这个API功能更好,在ARC下仍然能够运用@autoreleasepool

无论是MRC还是ARCautorelease最大的作用,是在大量创立目标的同时,经过润饰让内存得到提前开释,从而下降内存峰值。

@autoreleasepool {
    NSMutableArray *channelItemsJSONArray = [NSMutableArray arrayWithContentsOfFile:[self cachedChannelItemsFile]];
    NSArray *items = [self channelItemsJSONArray];
    if (![items writeToFile:[self cachedChannelItemsFile] atomically:YES]) {
        [channelItemsJSONArray writeToFile:[self cachedChannelItemsFile] atomically:YES];
    }
    items = nil;
}

__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函数。在这两个函数内部,会调用AutoreleasePoolPagepushpop函数。

AutoreleasePoolPage

在运转时代码中,objc_autoreleasePoolPopobjc_autoreleasePoolPush,都调用了AutoreleasePoolPage类的实现。

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

AutoreleasePoolPage的界说中,能够看到有parentchild的界说,当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++的类,每个page4096个字节,也便是16进制的0x1000,也便是4kb的空间。这些空间中,其本身的成员变量只占56个字节,也便是下面七个成员变量,每个占8字节,一共56个字节。其他的四千多个字节,都用来存放被autorelease润饰的目标内存地址。

POOL_BOUNDARY

探秘AutoreleasePool实现原理

POOL_BOUNDARY的作用是,区别不同的主动开释池,也便是不同的@autoreleasepool。调用push时,会传入POOL_BOUNDARY并返回一个地址例如0x10380x1038是不存储@autorelease目标的地址的,起到一个标识作用,用来切割不同的@autoreleasepool

调用pop时,会传入end的地址,并从后到前调用目标的release办法,直到POOL_BOUNDARY停止。假如存在多个page,会从childpage的最结尾开端调用,直到POOL_BOUNDARYpage的结构是一个栈结构,开释的时分也是从栈顶开端开释。

next指针指向栈顶,是栈里边很常见的一个设计。AutoreleasePoolPagePOOL_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中的顺序是下图这样。不同的@autoreleasepoolPOOL_BOUNDARY做切割。

探秘AutoreleasePool实现原理

push

创立一个autoreleasePool之后,就会调用push函数。在push函数中会判别是否调试模式下,假如调试模式会每次生成一个新的pagedebug环境代码能够直接疏忽,只保存autoreleaseFast函数。

static inline void *push()
{
    id *dest;
    if (DebugPoolAllocation) {
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    return dest;
}

autoreleaseFast

在函数内部,会经过hotPage获取当时的pagehotPage函数内部本质上是一个pagekey的映射。

  1. 假如page不为空而且有空间,则调用pageadd函数将目标添加到page中,并将POOL_BOUNDARY添加在当时的方位。
  2. 假如page现已被创立但没有空间,会调用autoreleaseFullPage函数创立新的page,而且将链表的结尾指向新创立的page
  3. 假如没有创立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

  1. autoreleaseFullPage函数中,会从page的链表中,从前往后找到结尾的节点。
  2. 创立一个新的page,在创立函数AutoreleasePoolPage中会处理parentchild指针的问题,返回的page能够直接用。
  3. 调用setHotPagepage设置到哈希表中,而且调用pageadd函数将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函数时,有三步处理。

  1. 判别autoreleasepool是否为空,经过EMPTY_POOL_PLACEHOLDER占位符判别,为空则清空这个page
  2. 传入的stop是否不等于POOL_BOUNDARY标识,假如不等于则可能是一个有问题的page
  3. 调用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

  1. popPage函数中心代码便是调用releaseUntil函数,在最开端会调用releaseUntil函数去完结开释操作。
  2. 依照page达到一半就扩容的原则,后边的if语句会判别履行poppage链表的状况。
    1. 假如少于半满,就将子节点删去。
    2. 假如大于半满,则保存子节点,并删去后边的节点。
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润饰的目标。

  1. 获取当时的hotPage
  2. 判别page是否为空,假如为空则表明里边的目标被开释完,则将page的父节点page设置为hotPage
  3. 获得上一个节点,->的管用优先级比--要高,所以是先经过next获取当时节点地址,这是一个为空的待存入节点,随后履行--操作获取上一个目标地址。
  4. 经过memset将上一个节点开释。
  5. 判别上一个节点是否占位符号POOL_BOUNDARY,假如不是则调用objc_release开释目标。
  6. 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_BOUNDARYpage中也占了一个方位。

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润饰的目标,开释机遇有两种。

  1. 假如经过代码添加一个autoreleasepool,在作用域完毕时,随着pool的开释,就会开释pool中的目标。这种情况是及时开释的,并不依赖于runloop
  2. 另一种便是由体系主动进行开释,体系会在runloop开端的时分创立一个pool,完毕的时分会对pool中的目标履行release操作。

runloop

假如是体系创立的pool,需求手动开启runloop,主线程默认现已开启并运转,子线程需求调用currentRunLoop办法开启并运转runloop,子线程中体系创立pool的流程才会正常工作。

探秘AutoreleasePool实现原理

包括主线程在内的每个线程,假如在线程中运用到了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实现原理