iOS底层原理探索 -- 内存管理 之 @autoreleasepool

2,283 阅读7分钟
写在前面: iOS底层原理探究是本人在平时的开发和学习中不断积累的一段进阶之
路的。 记录我的不断探索之旅,希望能有帮助到各位读者朋友。

目录如下:

  1. iOS 底层原理探索 之 alloc
  2. iOS 底层原理探索 之 结构体内存对齐
  3. iOS 底层原理探索 之 对象的本质 & isa的底层实现
  4. iOS 底层原理探索 之 isa - 类的底层原理结构(上)
  5. iOS 底层原理探索 之 isa - 类的底层原理结构(中)
  6. iOS 底层原理探索 之 isa - 类的底层原理结构(下)
  7. iOS 底层原理探索 之 Runtime运行时&方法的本质
  8. iOS 底层原理探索 之 objc_msgSend
  9. iOS 底层原理探索 之 Runtime运行时慢速查找流程
  10. iOS 底层原理探索 之 动态方法决议
  11. iOS 底层原理探索 之 消息转发流程
  12. iOS 底层原理探索 之 应用程序加载原理dyld (上)
  13. iOS 底层原理探索 之 应用程序加载原理dyld (下)
  14. iOS 底层原理探索 之 类的加载
  15. iOS 底层原理探索 之 分类的加载
  16. iOS 底层原理探索 之 关联对象
  17. iOS底层原理探索 之 魔法师KVC
  18. iOS底层原理探索 之 KVO原理|8月更文挑战
  19. iOS底层原理探索 之 重写KVO|8月更文挑战
  20. iOS底层原理探索 之 多线程原理|8月更文挑战
  21. iOS底层原理探索 之 GCD函数和队列
  22. iOS底层原理探索 之 GCD原理(上)
  23. iOS底层 - 关于死锁,你了解多少?
  24. iOS底层 - 单例 销毁 可否 ?
  25. iOS底层 - Dispatch Source
  26. iOS底层 - 一个栅栏函 拦住了 数
  27. iOS底层 - 不见不散 的 信号量
  28. iOS底层 GCD - 一进一出 便成 调度组
  29. iOS底层原理探索 - 锁的基本使用
  30. iOS底层 - @synchronized 流程分析
  31. iOS底层 - 锁的原理探索
  32. iOS底层 - 带你实现一个读写锁
  33. iOS底层 - 谈Objective-C block的实现(上)
  34. iOS底层 - 谈Objective-C block的实现(下)
  35. iOS底层 - Block, 全面解析!
  36. iOS底层 - 启动优化(上)
  37. iOS底层 - 启动优化(下)
  38. iOS底层原理探索 -- 内存管理 之 内存五大区
  39. iOS底层原理探索 -- 内存管理 之 Tagged Pointer Format Changes
  40. iOS底层原理探索 -- 内存管理 之 retain & release
  41. iOS底层原理探索 -- 内存管理 之 弱引用表

以上内容的总结专栏


细枝末节整理


前言

内存管理系列的文章今天我们继续来到 @autoreleasepool 的底层原理探索。话不多说,这就开始今天的内容吧。

自动释放池

自动释放池 @autoreleasepool 最常见的地方就是我们项目的 main 。我们今天来深入探索下其底层结构和实现原理。

想要了解其内部底层结构,我们先通过 xcrun 一下,看下其 编译后是什么结构:

未命名.jpg

可以看到 编译后 将 @autoreleasepool 注释掉,替换为了 __AtAutoreleasePool __autoreleasepool;

__AtAutoreleasePool 是一个结构体

struct __AtAutoreleasePool {
    // 构造函数
    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    // 析构函数
    ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    void * atautoreleasepoolobj;
};

编译后 的 __AtAutoreleasePool __autoreleasepool; 这行代码就相当于 在作用域开始的地方,调用 构造函数, 在作用域结束的时候,调用析构函数。

也就是分别调用了:

  • objc_autoreleasePoolPush();
  • objc_autoreleasePoolPop(atautoreleasepoolobj);

通过符号断点 我们定位一下它的源码所在 : 未命名.jpg

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

AutoreleasePoolPage

/***********************************************************************
   Autorelease pool implementation

   线程的自动释放池是一个指针堆栈。 
   每个指针要么是一个要释放的对象,要么是 POOL_BOUNDARY 自动释放池边界。 
   边界对象用来标记池子的边界,防止在出栈的时候操作越界。
   池令牌是指向该池的 POOL_BOUNDARY 的指针。
   当 池被弹出,每个 比 哨兵 热的 对象 都被释放。 
   堆栈被分成一个 双向链表。 页面添加 并在必要时删除。 
   线程本地存储指向新自动释放的热页 对象存储。
**********************************************************************/

BREAKPOINT_FUNCTION(void objc_autoreleaseNoPool(id obj));
BREAKPOINT_FUNCTION(void objc_autoreleasePoolInvalid(const void *token));

class AutoreleasePoolPage : private AutoreleasePoolPageData
{ ... }

AutoreleasePoolPageData

struct AutoreleasePoolPageData
{

...
    // 用来校验 AutoreleasePoolPage 的结构是否完整;
    magic_t const magic; //16
    
    //指向最新添加的 autoreleased 对象的下一个位置,初始化时指向begin() ;
    __unsafe_unretained id *next; //8

    //指向当前线程;
    pthread_t const thread; //8
    
    //指向父结点,第一个结点的 parent 值为 nil ;
    AutoreleasePoolPage * const parent; //8
    
    //指向子结点,最后一个结点的 child 值为 nil ;
    AutoreleasePoolPage *child; //8
    
    //代表深度,从 0 开始,往后递增 1;
    uint32_t const depth; //4
    
    //代表 high water mark 最大入栈数量标记
    uint32_t hiwat;  //4

...

struct magic_t {
    static const uint32_t M0 = 0xA1A1A1A1;
    #   define M1 "AUTORELEASE!"
    static const size_t M1_len = 12;
    uint32_t m[4]; // 4*4

    ...
}

};

配置关闭 ARC ,我们测试下:

image.png

image.png

通过打印可以看到,哨兵对象也是一个对象。

下面来到 push() 函数:

push()

第一次

首先没有页面(autoreleaseNoPage),我们会创建第一个页面, 然后将页面设置为热页面,接着加入烧饼对象,再接着加入 其他的对象入池子。

    static inline void *push() 
    {
        id *dest;
        if (slowpath(DebugPoolAllocation)) {
            // 每个自动释放池从一个新的池页开始。
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }
    
    ...
    
    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);
        }
    }
    
    ...
    
    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
        // "No page" 可能是说泳池没有被推过
        // 或者一个空的占位符池已经被推入并且还没有内容
        ASSERT(!hotPage());

        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) { ... }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) { ... }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) { ... }

        // 我们正在推入一个对象或一个非占位符池

        // 安装第一页。
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page); // 设置为 热页面
        
        // 为先前占位符池推入一个边界。
        if (pushExtraBoundary) {
            // 加哨兵对象
            page->add(POOL_BOUNDARY); 
        }
        
        // 推入请求的对象或池。
        return page->add(obj); 
    }

AutoreleasePoolPage

    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
		AutoreleasePoolPageData(begin(), 
                objc_thread_self(),
                newParent,
                newParent ? 1+newParent->depth : 0,
                newParent ? newParent->hiwat : 0)
    {
        if (objc::PageCountWarning != -1) {
            checkTooMuchAutorelease();
        }
        // 这里是双向链表的设置
        if (parent) {
            parent->check();
            ASSERT(!parent->child);
            parent->unprotect();
            parent->child = this;
            parent->protect();
        }
        protect();
    }
    
    ...
    
    id * begin() {
        // begin 页面从自动释放池的成员(自动释放池也需要56字节内存空间)  之后的内存开始
        return (id *) ((uint8_t *)this+sizeof(*this)); //56
    }

image.png

其大小为 56 字节。

image.png

在这里,我们也可以验证 - begin 页面从自动释放池的成员(自动释放池也需要56字节内存空间) 之后的内存开始。 且自动释放池第一个加进来的对象是哨兵对象;之后才是我们入栈进入的对象。

满了 page->full()

 static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // 热点页面已满
        // 步骤跳转到下一个非完整页面,如有必要,添加新页面。
        // 然后将对象添加到该页面。
        ASSERT(page == hotPage());
        ASSERT(page->full()  ||  DebugPoolAllocation);

        do {
            //  满了之后, 找到最后一个子页面,才添加一个page
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

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

内存分页管理:

  • 空间换时间的常规操作

在这里不使用连续内存,是为了不断的压栈出栈的话对内存的压力以及操作性能的问题,以及全部的数据存在一个页面上,管理上会十分的繁琐,一旦出问题,全部都完蛋;所以采用分页可以有效的隔离降低互相直降的影响,提升性能;分页之后,内存可以不连续,再次提升性能。

自动释放池.001.jpeg

自动释放池大小

测试我们发现,每个页面可以管理504个对象:

image.png image.png

为什么是504个对象一页呢?

504*8+56+POOL_BOUNDARY = 4096 = 2^12

这一大小在源码中也有体现:

image.png image.png

入栈流程: 通过next指针不断内存平移加入对象后,指向下一个位置;

出栈流程: 找到hot页面,找到parent页面设置为hot页面,将当前页面删掉。通过哨兵对象来判断是否已经完成。

扩展

  • 自动释放池可以嵌套使用, 并且互不影响。
  • 没有 autorelease 并不会交给 @autoreleasepool 来管理(ARC环境下 编译器会自动 添加 autorelease)。
  • 以 alloc、 new、 copy、 mutablecopy 为前缀 对象开头的 不会交给 @autoreleasepool 来管理。