iOS-内存管理(三)-autoreleasepool

1,075 阅读9分钟

本质

@autoreleasepool,即自动释放池,是自动内存管理的核心。官方文档给出的解释如下:

Autorelease pool implementation

A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary. A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released. The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. Thread-local storage points to the hot page, where newly autoreleased objects are stored.

解释如下: autoreleasepool的实现

  • 线程的自动释放池是指针的栈,即自动释放池是栈结构。
  • 每个指针要么是要释放的对象,要么是自动释放池边界POOL_BOUNDARY
  • POOL_BOUNDARY对自动释放池来说就是一个token指针。当释放池开始进行pop操作时,对象都在POOL_BOUNDARY之前释放。
  • 释放池是一个双向链表页,页面并根据需要来添加或者删除。
  • tls指针会指向最新页面的最新存储的对象。

从代码可以看出iOS程序入口就是在自动释放池内的。

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

使用clang指令将mian.m文件编译成mian.cpp,可以看出

@autoreleasepool {
    
}

变成了如下代码:

{ __AtAutoreleasePool __autoreleasepool; 

}

__AtAutoreleasePool则是一个结构体:

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

说明@autoreleasepool其实是调用了objc_autoreleasePoolPushobjc_autoreleasePoolPop这2个方法,其中objc_autoreleasePoolPush是构造函数,而objc_autoreleasePoolPop是析构函数。当然,也可以通过查看汇编来验证这个结论。

@autoreleasepool出设置一个断点运行程序,进入汇编也可以看到,程序的入口处是objc_autoreleasePoolPush,出口处是objc_autoreleasePoolPop

原理

结构

可以看出@autoreleasepool是属于objc的,那我们直接去源码里一探究竟。

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

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

class AutoreleasePoolPage : private AutoreleasePoolPageData
{ 
    
}

AutoreleasePoolPage 继承自AutoreleasePoolPageData,而AutoreleasePoolPageData的结构如下:

class AutoreleasePoolPage;
struct AutoreleasePoolPageData {
    magic_t const magic;                // 16
    __unsafe_unretained id *next;       // 8
    pthread_t const thread;             // 8
    AutoreleasePoolPage * const parent; // 8
    AutoreleasePoolPage *child;         // 8
    uint32_t const depth;               // 4
    uint32_t hiwat;                     // 4

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
		: magic(), next(_next), thread(_thread),
		  parent(_parent), child(nil),
		  depth(_depth), hiwat(_hiwat)
    {
    }
};

其中的参数意思如下:

  • magic_t const magic:用来校验自动释放池结构的完整性
  • __unsafe_unretained id *next:指向最新添加到自动释放池的对象的下一个位置(对象的末尾位置),初始化的时候指向begin位置
  • pthread_t const thread:指向当前线程,说明autoreleasepool 是与线程紧密相关的,每一个autoreleasepool只对应一个线程,而一个线程可以对应多个autoreleasepool
  • AutoreleasePoolPage * const parent:指向父page,第一页的parent为nil
  • AutoreleasePoolPage *child:指向子page,最后一页的parent为nil
  • uint32_t const depth:表示深度,即第几页,从0开始
  • uint32_t hiwat: 暂时没明白这个属性的含义

可以得出,自动释放池里面存放着一个双向链表,链表的每一个元素就是一页AutoreleasePoolPage,我们需要自动释放的对象都存放在AutoreleasePoolPage中。那么一页AutoreleasePoolPage能存放多少对象呢?

@autoreleasepool {
    for (int i = 0; i < 5; i++) {
        TPerson *person = [[TPerson alloc] autorelease];
        NSLog(@"==%@==", person);
    }
    _objc_autoreleasePoolPrint();
}

运行程序,控制台输出:

可以看出释放了6个对象,我们明明只创建了5个对象,为什么释放了6个对象,其实有一个是我们前面提到的POOL_BOUNDARY。再把i改为1000,运行程序,可以得出,一页能存放505个对象,只是第一页有一个特殊的对象,POOL_BOUNDARY。这个我们也可以从源码中得到验证:

#define I386_PGBYTES            4096           
#define PAGE_SIZE               I386_PGBYTES

PAGE_SIZE是4096,4096减去AutoreleasePoolPage其他变量的内存大小,即(4096-56)/8 = 505。

push

熟悉了autoreleasepool的结构,再来看看它是怎么进行初始化构造的。先解释几个名词:

  • hotPage:新入池子的对象都会放到hotPage
  • page->full():当前页已经满了
static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // 是DEBUG环境
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        // 调用@autoreleasepool的时候传入的参数是POOL_BOUNDARY
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    
    return dest;
}

static inline id *autoreleaseFast(id obj)
{
    // 获取hotPage
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        // 入栈
        return page->add(obj);
    } else if (page) {
        // page满了
        return autoreleaseFullPage(obj, page);
    } else {
        // 没有获取到page
        return autoreleaseNoPage(obj);
    }
}

id *add(id obj)
{
    unprotect();
    id *ret = next;  
    // 把obj放入next指针地址内,然后next指针地址++
    *next++ = obj;
    protect();
    return ret;
}

static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // hotPage满了,遍历hotPage的子面,
    // 如果有没满的页就把该页设置为hotPage
    // 找不到没满的子页,就新创建一页
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    // 添加对象
    return page->add(obj);
}

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // 创建一个新页,设置为hotPage
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    // 添加`POOL_BOUNDARY`对象
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }
    
    // 对象入页
    return page->add(obj);
}

初始化自动释放池的步骤如下:

  • 获取hotPage
  • 如果存在hotPage
    • hotPage没有满,把对象放入next指针地址,然后next指针地址++
    • hotPage满了,遍历hotPage的子页
      • 如果有没满的页,就把该页设置为hotPage,存入对象
      • 找不到没满的子页,就新创建一页,设置为hotPage,存入对象
  • 如果不存在hotPage,新创建一页,设置为hotPage,如果是个空池子则放入边界对象POOL_BOUNDARY,存入对象。

需要注意的是,初始化自动释放池,存入的其实是边界对象POOL_BOUNDARY

pop

自动释放池的析构函数即为AutoreleasePoolPage::pop(ctxt),此时传入的参数即为POOL_BOUNDARY,调用该方法会把整个释放池里面的对象全部释放。

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) {
            // 自动释放池是空的,删除占位对象
            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) {
            
        } else {
            return badPop(token);
        }
    }

    return popPage<false>(token, page, stop);
}

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();
        }
    }
}

void releaseUntil(id *stop) 
    {
        // 循环遍历 清除自动释放池里的所有对象 并释放
        while (this->next != stop) {
            AutoreleasePoolPage *page = hotPage();

            while (page->empty()) {
                // 如果当前页是空的,就把其父页设置为hotPage
                page = page->parent;
                setHotPage(page);
            }

            // 修改next指针的指向
            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                // 非边界对象的释放需要调用objc_release
                objc_release(obj);
            }
        }

        setHotPage(this);
    }

自动释放池析构的步骤如下:

    1. 获取边界对象POOL_BOUNDARY所在的page
    1. 判断page是不是第一页的第一个节点,如果不是就抛出异常
    1. 遍历释放池里的所有对象,修改next指针的指向,并且释放非POOL_BOUNDARY的对象
    1. 删除当前页的子页

autoreleasepool的嵌套

下面我们来看一个例子

@autoreleasepool {
    NSObject *obj1 = [[NSObject alloc] autorelease];
    NSLog(@"obj1 = %@",obj1);
    NSLog(@"-----------00000-----------");
    @autoreleasepool {
        NSObject *obj2 = [[NSObject alloc] autorelease];
        NSLog(@"obj2 = %@",obj2);
        NSObject *obj3 = [[NSObject alloc] autorelease];
        NSLog(@"obj3 = %@",obj3);
        _objc_autoreleasePoolPrint();
        NSLog(@"-----------11111-----------");
        
        @autoreleasepool {
            NSObject *obj4 = [[NSObject alloc] autorelease];
            NSLog(@"obj4 = %@",obj4);
            _objc_autoreleasePoolPrint();
            NSLog(@"---------22222-------------");
        }
    }

    NSLog(@"---------33333-------------");
    _objc_autoreleasePoolPrint();
}
    
sleep(5);
return 0;

运行程序,控制台输出:

可以看出,在第一次打印的地方,page里面有5个对象,obj1obj2obj3还有2个POOL也就是边界对象;第二个打印的地方有7个对象,obj1obj2obj3obj4还有3个边界对象;而在第三次打印的时候只有2个对象了,obj1和1个边界对象,这是因为前面的对象被释放了。我们前面说了一个page里面可以存放505个对象,所以当出现嵌套的时候,一个page里面可能存在多个释放池。而嵌套的时候,每个释放池的边界对象都是单独存在的。

如果释放池是串行的,则互不影响。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj1 = [[NSObject alloc] autorelease];
        NSLog(@"obj1 = %@",obj1);
        _objc_autoreleasePoolPrint();
    }
    
    NSLog(@"---------22222-------------");
    @autoreleasepool {
        NSObject *obj2 = [[NSObject alloc] autorelease];
        NSLog(@"obj2 = %@",obj2);
        NSObject *obj3 = [[NSObject alloc] autorelease];
        NSLog(@"obj3 = %@",obj3);
        _objc_autoreleasePoolPrint();
    }
    
    NSLog(@"---------33333-------------");
    @autoreleasepool {
        NSObject *obj4 = [[NSObject alloc] autorelease];
        NSLog(@"obj4 = %@",obj4);
        _objc_autoreleasePoolPrint();
    }
    
    sleep(5);
    return 0;
}

autoreleasepool的使用

通常我们是不必自己创建自动释放池,甚至不必查看用于创建自动释放池的代码。但是在以下3种情况,我们可能会使用到自动释放池:

  • 编写不基于UI框架的程序,例如命令行工具。
  • 编写一个创建许多临时对象的循环语句。可以在循环内使用自动释放池在下一次迭代之前处理这些对象,有助于减少应用程序的最大内存占用。
  • 使用辅助线程的时候,一旦线程开始执行,就必须创建自己的自动释放池块;否则可能出现内存泄露。
for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        TPerson *person = [[TPerson alloc] init];
        NSLog(@"==name==%@==", person.name);
    }
}

autorelease

看完了@autoreleasepool,我们再来看看autorelease

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

NEVER_INLINE id
_objc_rootAutorelease(id obj)
{
    return obj->rootAutorelease();
}

inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

static inline id autorelease(id obj)
{
    id *dest __unused = autoreleaseFast(obj);
    return obj;
}

根据代码,可以看出,对象执行autorelease方法后自身引用计数器不会改变,而且会返回对象本身。而且autoreleasepoolautorelease的区别就是传入的参数不同,autoreleasepool初始化的时候传入的是POOL_BOUNDARY,而autorelease传入的是对象本身。autorelease实际上只是把对release的调用延迟了,因为对象调用了autorelease,系统就会把该对象放入了当前的autoreleasepool中,当autoreleasepool被释放时,其中的对象才会被调用release

总结

  1. autoreleasepool是一种自动内存管理机制,一般情况下,创建的变量会在超出其作用域的时候就会release,但是如果将变量加入到autoreleasePool中,那么release操作将延迟到在释放池销毁的时候执行。其结构是一个双向链表,链表的元素是AutoreleasePoolPageARC下,自动释放池的压栈操作是系统自行处理的。

  2. autoreleasepool的初始化过程如下:

  1. autoreleasepool的释放过程如下:

  1. 向对象执行autorelease操作会将对象放入autoreleasepool,此时对象的释放时间就被延迟了,会和autoreleasepool一起释放。autorelease的流程:

    autorelease -> _objc_rootAutorelease -> rootAutorelease -> rootAutorelease2 -> AutoreleasePoolPage::autorelease -> autoreleaseFast

之后就和autoreleasepool初始化的流程类似,只是传入的对象不同,autoreleasepool传入的是POOL_BOUNDARY,而autorelease传入的是当前对象。

  1. autoreleasepool可以嵌套使用,此时边界对象POOL_BOUNDARY都会存在,互不影响。

参考文献: