资讯专栏INFORMATION COLUMN

Autoreleasepool自动释放池-源码

voidking / 1153人阅读

摘要:可以看出,我们整个应用都是包含在一个自动释放池中。而且在现在的环境中,自动释放池的用法就是这样子。参考文章自动释放池的前世今生黑幕背后的

Autoreleasepool相关的内容是在面试中比较容易被问到的。之前呢,谈到Autoreleasepool只能粗浅的了解到自动释放池与内存的管理有关,具体是怎么样来管理和释放对象,并没有深入的学习,本文是笔者在深入学习Autoreleasepool之后的总结和心得,希望对大家有帮助。

main函数

首先我们从main函数开始,main函数是我们应用的入口。

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

可以看出,我们整个iOS应用都是包含在一个自动释放池中。
而且在现在的ARC环境中,自动释放池的用法就是这样子 @autoreleasepool {}

@autoreleasepool

这个@autoreleasepool{}大家都会用,我们的代码直接写在这个大括号内即可。我们代码中的对象是怎样加到自动释放池中的,最后又是怎么样被释放的呢 ?

我们要先知道这个@autoreleasepool到底是什么。

从网上的一些博客中可以学到的,在命令行使用clang -rewrite-objc main.m 让编译器重新改写main函数所在的这个文件。当然了这一步我并没有操作,直接“盗用”了大家的结果。

从上图中可以看出,@autoreleasepool被转换为了一个__AtAutoreleasePool结构体。然后通过在main.cpp中查找找到这个结构体的定义。

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

这个结构体在初始化是调用objc_autoreleasePoolPush(),在析构时调用objc_autoreleasePoolPop()

经过整理,可以把main函数中实际的代码应该是类似这样的。

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do whatever you want
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

下面就是正式的通过源码来学习Autoreleasepool了

Autoreleasepool源码

上面我们提到了mian函数中的@autoreleasepool其实最终转成了objc_autoreleasePoolPush()objc_autoreleasePoolPop()这两个方法的调用,我们去源码中搜一下这两个函数。

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

有源码可以看出,这两个函数就是对AutoreleasePoolPage类的pish和pop方法的封装,所以我们来着重看AutoreleasePoolPage类。

AutoreleasePoolPage

简化一下代码,先看类的部分属性

class AutoreleasePoolPage {

    static size_t const SIZE = 
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif

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

熟悉链表的朋友,看到这个parent和child就差不多能猜出来了,每一个自动释放池其实是一个双向链表,链表的每一个结点就是这个AutoreleasePoolPage,每个AutoreleasePoolPage的大小为4096字节

#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES
自动释放池中的栈(转载)

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

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

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

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

从图片中我们可以看到在AutoreleasePoolPage的栈中出现了一个POOL_SENTINEL,我们称之为哨兵对象。

#define POOL_SENTINEL nil

其实哨兵对象只是nil的别名,他有啥作用呢 ?

每个自动释放池初始化在调用objc_autoreleasePoolPush的时候,都会把一个POOL_SENTINELpush到自动释放池的栈顶,并且返回这个POOL_SENTINEL的地址。

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        
        // do whatever you want
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

上面这个atautoreleasepoolobj就是一个POOL_SENTINEL
可以看到在调用objc_autoreleasePoolPop时,会传进去这个地址:

根据传入的哨兵对象地址找到哨兵对象所处的page

在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

因为自动释放池是一个双向链表,而且每一个page的空间有限,所以会存在当前page已满的情况,也就出现了一个自动释放池跨越几个page的情况,所以在release的时候,也要顺着链表全部清理掉。

objc_autoreleasePoolPush
    static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

经查阅,DebugPoolAllocation是来区别调试模式的,我们主要看autoreleaseFast这个函数。

    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( )我们可以理解为获取当前的AutoreleasePoolPage,获取到当前page之后又根据page是否已满来区别处理。

有 hotPage 并且当前 page 不满

调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

有 hotPage 并且当前 page 已满

调用 autoreleaseFullPage 初始化一个新的页

调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

无 hotPage

调用 autoreleaseNoPage 创建一个 hotPage

调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中

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

上面代码中的函数是在page已满的时候调用,从源码中可以看出通过传入的page遍历链表,直到找到一个未满的page,如果遍历到最后一个结点也没有未满的,就新建一个new AutoreleasePoolPage(page);。并且要把找到的满足条件的这个page设置为hotPage。

objc_autoreleasePoolPop

我们看一下pop的源码,里面内容很多,我们精简了一下。

static inline void pop(void *token) {
    AutoreleasePoolPage *page = pageForPointer(token);
    id *stop = (id *)token;

    page->releaseUntil(stop);
    
    if (page->child) {
    // 不清楚为什么要用下面这个if分类
        if (page->lessThanHalfFull()) {
            page->child->kill();
        } else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

通过token调用pageForPointer()方法获取到当前的AutoreleasePoolPage,然后调用releaseUntil()释放page中的对象,直到stop,child节点调用kill()方法。

void kill() {
   AutoreleasePoolPage *page = this;
   //通过循环先找到最后一个节点
   while (page->child) page = page->child;

   AutoreleasePoolPage *deathptr;
   //通过do-while循环,依次从后往前置为nil
   do {
       deathptr = page;
       page = page->parent;
       if (page) {
           page->unprotect();
           page->child = nil;
           page->protect();
       }
       delete deathptr;
   } while (deathptr != this);
}

pageForPointer()主要是通过内存地址的操作,获取当前指针所在页的首地址, releaseUntil()也是通过一个循环来释放所有的对象,具体的源码大家可以自己看一下。

Autorelease对象什么时候释放

在没有手动加AutoreleasePool的情况下,Autorelease对象都是在当前的runloop迭代结束时释放的,因为系统在每个runloop迭代中都加入了自动释放池Push和Pop。

这个问题又要跟runloop联系到一起了,等我们研究过runloop的源码,对这个问题应该就有更深刻的认识了。

参考文章

自动释放池的前世今生
黑幕背后的Autorelease

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/17232.html

相关文章

  • 自动释放的前世今生

    摘要:需要注意的是整个的应用都是包含在一个自动释放池中的。在每个自动释放池初始化调用的时候,都会把一个到自动释放池的栈顶,并且返回这个哨兵对象。最后,将添加到自动释放池中。不过在这个方法中传入其它的指针也是可行的,会将自动释放池释放到相应的位置。 关注仓库,及时获得更新:iOS-Source-Code-Analyze Follow: Draveness · Github 由于 Object...

    zhjx922 评论0 收藏0
  • Objective-C的内存管理(2)——从MRC到ARC

    摘要:在里,每个继承自的对象都会记录自身的引用计数,一番加加减减之后,变成就会释放掉。引用计数减少的操作就是了。要知道,返回的对象引用计数是有的,而并不是。 罗里吧嗦颠三倒四,单纯的个人笔记。 MRC 引用计数上一篇已经有大概讲过。在Objective-C里,每个继承自NSObject的对象都会记录自身的引用计数,一番加加减减之后,变成0就会释放掉。MRC是Mannul Reference ...

    luck 评论0 收藏0
  • 理解 ARC 实现原理

    摘要:是中管理引用计数的技术,帮助实现垃圾自动回收,具体实现的原理是由编译器进行管理的,同时运行时库协助编译器辅助完成。本文主要内容由修饰符拓展开,分别延伸出引用计数弱引用表自动释放池等实现原理。判断使用了优化处理则返回对象,否则引用计数。ARC 是 iOS 中管理引用计数的技术,帮助 iOS 实现垃圾自动回收,具体实现的原理是由编译器进行管理的,同时运行时库协助编译器辅助完成。主要涉及到 Cla...

    lavor 评论0 收藏0
  • iOS内存管理

    摘要:内存块头部管理引用计数的好处少量代码皆可完成能够统一管理引用计数内存块与对象内存块。中的或者等,还是需要自己手动进行内存管理。必须遵守内存管理的方法命名规则。但是附有修饰的变量不属于编译器的内存管理对象,可能造成内存泄露或者崩溃。 1.引用计数式内存管理的思考方式 自己生成的对象,自己持有 非自己生成的对象,自己也能持有 不在需要自己持有的对象时释放 非自己持有的对象无法释放 2....

    adam1q84 评论0 收藏0
  • IOS学习总结(1)——关于并发

    摘要:我就个人感受而言,总结一下并发编程以及相关的一些知识。实际上,所有的并发编程都是构建于线程之上的包括和操作队列。当方法执行结束后,运行时会特地清理并弃掉线程。 本文仅作为个人学习总结记录使用!能力有限,难免会有疏漏和错误,还望指出。共同进步。 独白 工作学习IOS已经有半年了,一直都想抽出时间记录一下自己的学习和工作经验,但都写到一半,就没有然后了。关于并发编程,其实国外国内已经有...

    summerpxy 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<