iOS AutoreleasePool

AutoreleasePool是OC中的一种内存自动回收机制,它可以推迟AutoReleasePool中的变量release的时机。在通常情况下,创建的变量超出作用域后就会被释放,但是加入到AutoreleasePool后,变量就会延迟释放。

源码

在终端中使用xcrun -sdk iphonesimulator clang -rewrite-objc main.m命令将OC的main函数转译成C++实现:
main.cpp

extern "C" __declspec(dllimport) void *objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
    __AtAutoreleasePool() {atautoreleaseobj = objc_autoreleasePoolPush();}
    ~__AtAutoreleasePoll() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    void *atautoreleasepoolobj;
};

int main(int argc, char *argv[]) {
    /*@autoreleasepool*/{__AtAutoreleasePool __autoreleasepool;
    return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

结构体初始化时调用objc_autoreleasePoolPush()方法,会在析构时调用objc_autoreleasePoolPop方法。
这表明,main函数的实际工作时是这样的:

int main(int argc, const char *argv[]) {
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    
        // 实现内容
    
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

objc_autoreleasepoopPush和objc_autoreleasePoolPop都是对AutoreleasePoolPage的封装。

结构

每个自动释放池都是由一系列的AutoreleasePoolPage组成的,并且每一个AutoreleasePoolPage 的大小都是4096字节

class AutoreleasePoolPage {
    magic_t const magic;                        /// 用于对当前AutoreleasePoolPage完整性校验
    id *next;   // 指向下一个新增加的位置
    pthread_t const thread;                     /// thread保存了当前页所在的线程
    AutoreleasePoolPage *const parent; //前节点
    AutoreleasePoolPage *child; //后节点
    uint32_t const depth;
    uint32_t hiwat;
}

自动释放池中的AutoreleasePoolPage是以双向链表的形式连接起来的,parent和child就是用来构建双向链表的指针。


AutoreleasePoolPage父子节点.png

parent和child就是用来构建双向链表的指针。parent指向前一个page,child指向下一个page。一个AutoreleasePoolPage的空间被沾满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入。

自动释放池中的栈

如果我们的一个AutoreleasePoolPage被初始化在内存的0x100816000 ~ 0x100817000中,它在内存中的结构如下:


AutoreleasePoolPage内存结构.png

其中有56bit用于存储AutoreleasePoolPage的成员变量,剩下的0x100816038 ~ 0x100817000都是用来存储加入到自动释放池中的对象。

begin()和end()这两个类的实例方法帮助我们快速获取0x100816038 ~ 0x100817000这一范围的边界地址。

next指向了下一个为空的内存地址,如果next指向的地址加入一个object,它就会如下图所示移动到下一个为空的内存地址空间中:


AutoreleasePoolPage内存结构(有对象).png

创建

App启动后主线程的RunLoop注册两个observer:_warpRunLoopWithAutoreleasePoolHandle()

第一个observer监听事件Entry,回调调用_objc_autoreleasePoolPush()创建自动释放池,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个observer监听两个事件:
1.Before waiting时调用_objc_autoreleasePoolPop和push清空自动释放池。
2.Exit时调用_objc_autoreleasePoolPop释放清空。
优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调被RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool了。

POOL_SENTINEL (哨兵对象,最新的objc4为POOL_BOUNDARY)

POOL_SENTINEL只是nil的别名

#define POOL_SENTINEL nil (最新的objc4为 #define POOL_BOUNDARY nil)

在每个自动释放池初始化调用objc_autoreleasePoolPush的时候,都会把一个POOL_SENTINEL push到自动释放池的栈顶,并返回这个POOL_SENINEL哨兵对象。

int main(int argc, const char *argv[]) {
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    
        // 作用方法
    
        objc_autoreleasePoolPop(autoreleasepoolobj);
    }
}   

上面的atautoreleasepoolobj就是一个POOL_SENTINEL。
而当方法objc_autoreleasePoolPop调用时,就会向自动释放池中的对象发送release消息,直到第一个POOL_SENTINEL。

objc_autoreleasePoolPush方法

了解了POOL_SENTINEL,我们来重新回顾一下objc_autoreleasePoolPush方法:

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

它调用AutoreleasePoolPush的类方法push:

static inline void *push() {
    return autoreleaseFast(POOL_SENTINEL);
}

在这里会进入比较关键的方法autoreleaseFast,并传入哨兵对象POOL_SENTINEL:

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);
    }
}
  • 有hotPage并当前page不满,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中。
  • 有hotPage并当前page已满,调用autoreleaseFullPage初始化一个新的页,调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中。
  • 无hotPage,调用autoreleaseNoPage创建一个hotPage调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中。
    最后的都会调用page->add(obj)将对象添加到自动释放池中。

hotPage为当前正在使用的AutoreleasePoolPage。

page->add添加对象(当前page不满)

id *add(id obj)将对象添加到自动释放池页中:

    id *add(id obj) {
        assert(!full());
        unprotect();
        id *ret = next; // faster than ‘return next-1’ because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

这个方法就是一个压栈操作,将对象加入AutoreleasePoolPage然后移动栈顶的指针。

autoreleaseFullPage

autoreleaseFullPage会在当前的hotPage已满的时候调用:

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开始遍历整个双向链表,知道查找到一个未满的AutoreleasePoolPage使用构造器传入parent创建一个新的AutoreleasePoolPage,在查找到一个可以使用的AutoreleasePoolPage之后,会将该页面标记成hotPage,然后调动上面分析过的page->add方法添加对象。

autoreleaseNoPage

如果当前内存中不存在hotPage,就会调用autoreleaseNoPage方法初始化一个AutoreleasePoolPage:

static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }
    
    return page->add(obj);
}

既然当前内存中不存在AutoreleasePoolPage,就要从头开始构建这个自动释放池的双向链表,也就是新的AutoreleasePoolPage是没有parent指针的。

初始化之后,将当前页标记为hotPage,然后会先向这个page中添加一个POOL_SENTINEL对象,来确保在pop调用的时候,不会出现异常。

最后,将obj添加到自动释放池中。

objc_autoreleasePoolPop方法

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

这个方法中传入一个是哨兵对象POOL_SENTINEL。

AutoreleasePoolPage::pop方法调用

static inline void pop(void *token) {
    AutoreleasePoolPage *page = pageForPointer(token);
    id *stop = (id *)token;
    
    page->releaseUntil(stop);
    
    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

作用:使用pageForPointer获取当前token所在的AutoreleasePoolPage,
调用releaseUntil方法释放栈中的对象,知道stop
调用child的kill方法

pageForPointer获取AutoreleasePoolPage

pageForPointer方法主要是通过内存地址的操作,获取当前指针所在的页的首地址:

static AutoreleasePoolPage *pageForPointer(const void *p) {
    return pageForPointer((uintptr_t)p);
}

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

将指针与页面大小,也就是4096取模,得到当前指针的偏移量,因为所有的Autoreleas在呢欧村中都是对齐的:
p = 0x100816048
p % SIZE = 0X48;
result = 0x100816000
而最后调用的方法fastCheck()用来检查当前的result是不是一个AutoreleasePoolPage。

releaseUntil释放对象

releaseUntil方法的实现如下:

void releaseUntil(id *stop) {
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();
        
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }
        
        page->unprotect();
        if objc = *--page->next;
        memset((void *)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();
        
        if (obj != POOL_SENTINEL) {
            objc_release(obj);
        }
    }
    
    setHotPage(this);
}

用一个while循环持续释放AutoreleasePoolPage中的内容,直到next指向了stop。

使用memset将内存的内容设置成SCRIBBLE,然后使用objc_release释放对象。

kill()

到这里,没有分析的方法只剩下kill了,而它会将当前页面以及子页面全部删除:

void kill() {
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;
    
    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    }while (deathptr != this);
}

autorelease方法

方法调用栈:

- [NSObject autorelease]
    id objc_object::rootAutorelease()
        id objc_object::rootAutorelease2()
            static id AutoreleasePoolPage::autorelease(id obj)
                static id AutoreleasePoolPage::autoreleaseFast(id obj)
                    id *add(id obj)
                    static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
                        AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                        id *add(id obj)
                    static id *autoreleaseNoPage(id obj)
                        AutoreleasePoolPage(AutoreleasePoolPage *newParent)
                        id *add(id obj)

在autorelease方法的调用栈中,最终都会调用上面提到的autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。

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()) {
    return AutoreleasePoolPage::autorelease((id)this);
}

static inline id autorelease(id obj) {
    id *dest __unused = autoreleaseFast(obj);
    return obj;
}
  • 自动释放池是由AutoreleasePoolPage以双向链表的方式实现的。
  • 当对象调用autorelease方法时,会将对象加入AutoreleasePoolPage的栈中。
  • 调用AutoreleasePoolPage::pop方法会向栈中的对象发送release消息

Autorelease返回值的快速释放机制:
值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略。
比如一个工厂方法:

+ (instancetype)createSark {
    return [self new];
}
// caller
Sark *sark = [Sark createSark];

秉着谁创建谁释放的原则,返回值需要是一个autorelease对象才能配合调用方法管理内存,于是乎编译器改写成了形如下面的代码:

+ (instancetype)createSark {
    id tmp = [self new];
    return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease
}
id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain
Sark *sark= tmp;
objc_storeStrong(&sark, nil); // 相当于我们调用了release

使用

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)] {
    // 这里被局部@autoreleasepool包围着
}];

在普通for循环和for in循环中没有,所以还是新版的block枚举器更加节省内存空间。for循环中遍历产生大量的autorelease变量时,就需要手动添加布局AutoreleasePool

for (int i = 0; i < 5; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"%d", i];
    }
}

子线程autorelease对象的释放时机

@property (weak) id obj;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondaryThread) toTarget:self withObject:nil];
}

- (void)createAndConfigObserverInSecondaryThread {
    __autoreleasing id test = [NSObject new];
    NSLog(@"obj = %@", test);
    _obj = test;
    [[NSThread currentThread] setName:@"test runloop thread"];
    NSLog(@"thread ending");
}

_obj = test;代码断定
watchpoint set variable _obj (监听_obj的销毁)
没有runloop

static void tls_dealloc(void *p) {
    if ((p == void *)EMPTY_POOL_PLACEHOLDER) {
        // No objects or pool pages to clean up here.
        return;
    }
    
    // reinstate TLS value while we work
    setHotPage((AutoreleasePoolPage *)p);
    
    if (AutoreleasePoolPage *page = coldPage()) {
        if (!page->empty()) pop(page->begin()); // pop all of the pools
        if (DebugMissingPools || DebugPoolAllocation) {
            // pop() killed the pages already
        } else {
            page->kill(); // free all of the pages
        }
    }
    
    // clear TLS value so TLS destruction doesn't loop
    setHotPage(nil);
}

在这找到了if(!page->empty()) pop(page->begin());这句关键代码。在_pthread_exit时会执行下面的函数:

void _pthread_tsd_cleanup(pthread_t self) {
    #if !VARIANT_DYLD
    int j;
    // clean up dynamic keys first
    for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
        pthread_key_t k;
        for (k == __pthread_tsd_start; k <= self->max_tsd_key; k++) {
            _pthread_tsd_cleanup_key(self, k);
        }
    }
    
    self->max_tsd_key = 0;
    
    // clean up static keys
    for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
        pthread_key_t k;
        for (k = __pthread_tsd_first; k <= __pthread_tsd_max; k++) {
            _pthread_tsd_cleanup_key(self, k);
        }
    }
#endif
}

也就是说thread在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool,在tls_deplloc中,执行pop操作。
所以线程在销毁时会清空autoreleasepool。

在GNU的实现中,target执行相应的action操作是在[self acceptInputForMode:modebeforeDate:d];中,可以看到在runMode:(NSString)mode beforeDate:(NSDate)date方法中,其实是包裹了一个autoreleasepool的,也就是arp,如果再深入一些函数里面,发现其实很多地方都有autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。

  • 子线程在使用autorelease对象时,如果没有autoreleasepoool就会在autoreleaseNoPage中懒加载一个出来。
  • 在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统就是在很多地方都插入了autorelease管理操作。
  • 就算插入没有pop也米有关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里会清空autoreleasepool。

你可能感兴趣的:(iOS AutoreleasePool)