【iOS】autoreleasePool

autoreleasePool自动释放池

简介

自动释放池是OC中的一种内存回收机制,可以将加入autoreleasePool中的变量release时机延迟。当创建一个对象,在正常情况下,变量会在超出其作用域的时立即release。如果将对象加入到了自动释放池中,这个对象并不会立即释放,会等到runloop休眠/超出autoreleasepool作用域{}之后才会被释放

程序从启动到加载完成,主线程对应的runloop会处于休眠状态,等待用户交互唤醒runloop。runloop在监听到交互事件之后,就会创建自动释放池,并将所有延迟释放的对象添加到自动释放池中,在一次完整的runloop结束之前会向自动释放池中的所有对象发送release消息,然后销毁释放池。

自动释放池本质是也是一个对象:

objc 复制代码
@autoreleasePool{
}
//等价于
{
  __AtAutoreleasePool __autoreleasepool;
}
objc 复制代码
struct __AtAutoreleasePool {
    //构造函数
    __AtAutoreleasePool() {
            atautoreleasepoolobj = objc_autoreleasePoolPush();//压入一个自动释放池边界
    }
    //析构函数
    ~__AtAutoreleasePool() {
            objc_autoreleasePoolPop(atautoreleasepoolobj);//弹出自动释放池,并释放池中登记的autorelease对象
     }
    void * atautoreleasepoolobj;//保存当前自动释放池的标记(一个池边界标记)
};

可以看到这个结构体有构造函数和析构函数,结构体定义的对象在作用域结束之后,会自动调用析构函数

可以看出,在ZLPerson创建的时候,自动调用了构造函数,在出了{}之后,自动调用了析构函数.

  • autoreleasepool本质就是一个结构体对象,核心结构是AutoreleasePoolPage,@autoreleasepool 底层由一个或多个 AutoreleasePoolPage 组成,每个 Page 用来存放需要延迟释放的对象指针。页内从底地址到高地址依次存延迟释放的对象指针。
  • 页的栈底是一个56字节的空间用于存储页头信息,用于运行时管理,一页总大小为4096字节(64位环境下)
  • 只有第一页有哨兵对象,最多存储504个对象,从第二页开始最多存储505个对象
  • autoreleasepool在加入要释放的对象时,底层调用的是objc_autoreleasePoolPush方法
  • autoreleasepool在调用析构函数释放时,内部调用objc_autoreleasePoolPop方法

我们先简单了解一下@autoreleasePool编译后的样子:

objc 复制代码
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
}
///编译后:
void *token = objc_autoreleasePoolPush();
Person *p = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(token);
  • 自动释放池里存的不是对象本身,而是对象指针,整体是一个指针栈。
  • 栈中的每个指针,要么是需要释放的对象,要么是 POOL_BOUNDARY,表示一个自动释放池边界。
  • pool token 是指向当前池边界标记的指针。
  • 当池子被 pop 时,所有比哨兵标记更新的对象都会被释放。
  • 自动释放池这个指针栈被分成一页一页的内存块,每一页是一个 AutoreleasePoolPage,多个 Page 之间通过双向链表连接。
  • 线程本地存储 TLS 指向 hot page,新加入的 autorelease 对象会存到这个 hot page 中。

autoreleasePoolPage

objc 复制代码
void *
_objc_autoreleasePoolPush(void)
{
    return objc_autoreleasePoolPush();
}

void
_objc_autoreleasePoolPop(void *ctxt)
{
    objc_autoreleasePoolPop(ctxt);
}

可以看出底层调用的是autoreleasePoolPage的push和pop方法,下面是page定义:可以看出,自动释放池是一个页,大小为4096字节

objc 复制代码
//************宏定义************
#define PAGE_MIN_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096            /* bytes per 80386 page */

//************类定义************
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    friend struct thread_data_t;

public:
    //页的大小
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MIN_SIZE;  // size and alignment, power of 2
#endif

private:
    
    ...
    
    //构造函数
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//开始存储的位置
                                objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
                                newParent ? newParent->hiwat : 0)
    {...}
//...省略一堆的操作页的代码

可以看出autoreleasePoolPage是继承与AutoreleasePoolPageData,下面我们看一下这个AutoreleasePoolPageData定义:

objc 复制代码
class AutoreleasePoolPage;
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字节

    //初始化
    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)
    {
    }
};

autoreleasepoolPageData是autoreleasePoolPage的数据基类,主要保存每一页自动释放池的页头信息,包括magic、next、thread、parent、child、depth等信息,autoreleasepoolpage通过private继承autoreleasePoolPageDat复用这些字段,并在此基础上实现自动释放池的核心逻辑。因此,AutoreleasePoolPageData 可以理解为数据层,AutoreleasePoolPage 可以理解为真正执行管理逻辑的 Page 对象。一个完整的 AutoreleasePoolPage 内存布局中,前面是 AutoreleasePoolPageData 页头,后面才是用来存放 POOL_BOUNDARY 和 autorelease 对象指针的栈空间。

objc_autoreleasePoolPush

源码如下:

objc 复制代码
//入栈
static inline void *push() 
{
    id *dest;//表示这次压栈的位置
    if (slowpath(DebugPoolAllocation)) {//开启一个优化,决定师傅重新开始一页
        // Each autorelease pool starts on a new pool page.自动释放池从新池页面开始
        //如果没有,则创建
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        //压栈一个POOL_BOUNDARY,即压栈哨兵
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

判断是否有pool,如果没有,则通过autoreleaseNewPage方法创建。如果没有,则通过autoreleaseFast压栈哨兵对象。

创建页autoreleasenNewPage

objc 复制代码
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
    AutoreleasePoolPage *page = hotPage();//寻找当前是否有正在使用的自动释放池页

    if (page) return autoreleaseFullPage(obj, page);//如果找到了执行压栈
    else return autoreleaseNoPage(obj);//没找到就首次创建
}

//从线程的局部存储中查找
static inline AutoreleasePoolPage *hotPage()
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
        tls_get_direct(key);

    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;//判断取出的是否是一个特殊的占位符(不是真的autoreleasePoolPage),只是表示当前线程已经push了一个自动释放池,但是池中没有任何对象,暂时不分配page
    if (result) result->fastcheck();//快速校验当前page是否有效
    return result;
}

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    ASSERT(!hotPage());//当前线程没有任何可用的page

    bool pushExtraBoundary = false;//决定真正创建page之后是否需要往额外补充一个哨兵

    if (haveEmptyPoolPlaceholder()) {//如果当前已经push过一个自动释放池,但是没有真正创建page时,需要额外压栈一个哨兵。
        pushExtraBoundary = true;
    }
    else if (obj != POOL_BOUNDARY && DebugMissingPools) {//没有池,但是autorelease了对象
        _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug",
                     objc_thread_self(), (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }
    else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {//第一次push自动释放池,但不马上创建page
        return setEmptyPoolPlaceholder();//在TLS中放一个EMPTY_POOL_PLACEHOLDER,节省内存
    }

    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }

    return page->add(obj);
}

autoreleasepoolPage构造方法

objc 复制代码
//**********AutoreleasePoolPage构造方法**********
    AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
        AutoreleasePoolPageData(begin(),//开始存储的位置
                                objc_thread_self(),//传的是当前线程,当前线程时通过tls获取的
                                newParent,
                                newParent ? 1+newParent->depth : 0,//如果是第一页深度为0,往后是前一个的深度+1
                                newParent ? newParent->hiwat : 0)
{ 
    if (parent) {
        parent->check();
        ASSERT(!parent->child);
        parent->unprotect();
        //this 表示 新建页面,将当前页面的子节点 赋值为新建页面
        parent->child = this;
        parent->protect();
    }
    protect();
}
  • begin()表示压栈的位置,其具体位置等于页首地址+56,这个56就是结构体AutoreleasePoolPageData的内存大小
  • objc_thread_self() 表示的是当前线程,而当前线程时通过tls获取的
objc 复制代码
__attribute__((const))
static inline pthread_t objc_thread_self()
{
    //通过tls获取当前线程
    return (pthread_t)tls_get_direct(_PTHREAD_TSD_SLOT_PTHREAD_SELF);
}
  • newParent表示父节点

接下来我们查看一下释放池内存结构

首先我们先在Build Settings -> Objectice-C Automatic Reference Counting切换为MRC模式:

然后测试如下:

这里只输出了四个对象是因为编译器/runtime 对 autorelease 返回值做了优化,也就是 autorelease return value optimization 。它可能让某些 autorelease 不真正入池,而是通过运行时的快速返回值机制交接对象。objc runtime 里确实有 objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValueobjc_unsafeClaimAutoreleasedReturnValue 这一套优化入口

autoreleaseFast压栈对象

objc 复制代码
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

如果当前页已经存储满了,则通过do-while循环查找子节点对应的页,如果不存在就新建页,并压入对象。

objc 复制代码
//添加自动释放对象,当页满的时候调用这个方法
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    ASSERT(page == hotPage());
    ASSERT(page->full()  ||  DebugPoolAllocation);
    //do-while遍历循环查找界面是否满了
    do {
        //如果子页面存在,则将页面替换为子页面
        if (page->child) page = page->child;
        //如果子页面不存在,则新建页面
        else page = new AutoreleasePoolPage(page);
    } while (page->full());
    //设置为当前操作页面
    setHotPage(page);
    //对象压栈
    return page->add(obj);
}

add方法

objc 复制代码
id *add(id obj)
{
    ASSERT(!full());
    unprotect();//自动释放池的一套调试保护机制,当页处于保护状态时,可能会通过内存白虎、防御性检查等方式防止外部非法修改。所以这里需要先解除保护才能修改
    //传入对象存储的位置
    id *ret = next;  // faster than `return next-1` because of aliasing
    //将obj压栈到next指针位置,然后next进行++,即下一个对象存储的位置
    *next++ = obj;
    protect();
    return ret;
}

通过next指针存储释放对象,并将next指针递增来添加释放对象

autorelease底层分析

objc 复制代码
__attribute__((aligned(16), flatten, noinline))//16位对齐、对内对外内联、对外相反
id
objc_autorelease(id obj)//autorelease的C函数入口
{
    //不是对象,则直接返回
    if (!obj) return obj;
    //如果是小对象,也直接返回
    if (obj->isTaggedPointer()) return obj;
    return obj->autorelease();
}


inline id 
objc_object::autorelease()//先判断类有没有自定义的retain、release、autorelease相关方法
{
    ASSERT(!isTaggedPointer());
    //判断是否是自定义类
    if (fastpath(!ISA()->hasCustomRR())) {
        return rootAutorelease();
    }

    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(autorelease));//发现有自定义的逻辑,走消息转发
}

inline id 
objc_object::rootAutorelease()//默认autorelease实现
{
    //如果是小对象,直接返回
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;//判断是否开启了返回值优化,避免不必要的入池和引用计数操作

    return rootAutorelease2();//真正准备将对象加入自动释放池(慢路径)
}

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

static inline id autorelease(id obj)
{
    ASSERT(obj);
    ASSERT(!obj->isTaggedPointer());
    //autoreleaseFast 压栈操作
    id *dest __unused = autoreleaseFast(obj);//尝试将对象快速加入当前线程池的自动释放池
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

调用路径:objc_autorelease(obj)objc_object::autorelease()rootAutorelease()rootAutorelease2()AutoreleasePoolPage::autorelease()autoreleaseFast(obj)

无论是压栈哨兵对象还是普通对象,都会进入到autoreleaseFase方法。

autoreleasePoolPop分析

objc 复制代码
static inline void
pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;//表示本次pop要停止的位置

    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {//如果 token 是 EMPTY_POOL_PLACEHOLDER,说明 push 的时候没有真正创建 Page
        page = hotPage();//查看当前有没有真正的page
        if (!page) {
            return setHotPage(nil);//表示当前空池结束,将线程的hotPage状态清空
        }

        page = coldPage();
        token = page->begin();//如果顶层 pool 使用的是 EMPTY_POOL_PLACEHOLDER,后来又创建了 Page,那么 pop 顶层 pool 时,需要从整个 Page 链的起点开始处理
    } else {
        page = pageForPointer(token);//根据token找到它处于哪一个autoreleasePooloage中
    }
    
    stop = (id *)token;

    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {//特殊情况,就算不是哨兵,但是是最冷page的起始位置也允许
        } else {
            return badPop(token);
        }
    }

    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);//调试出栈
    }

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

EMPTY_POOL_PLACEHOLDER 是延迟创建策略,push阶段先进行占位,autorelease阶段发现时再补建。(替换)

popPage

objc 复制代码
template<bool allowDebug>
static void popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    if (allowDebug && PrintPoolHiwat) printHiwat();//输出曾经使用过的最大容量,只和调试有关

    page->releaseUntil(stop);//从当前位置一直释放到终止位置

    if (allowDebug && DebugPoolAllocation && page->empty()) {//当前页清空
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    }
    else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) {//已经清空到个最顶层池,直接删掉当前页并将hotpage置空
        page->kill();
        setHotPage(nil);
    }
    else if (page->child) {//滞后保留策略:runtime 会保留一部分空 page,供下次继续使用
        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()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();

        id obj = *--page->next;

      //弹出的槽位会被写成特殊值 SCRIBBLE,避免后续误用旧指针
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));

        page->protect();

        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);

#if DEBUG
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        ASSERT(page->empty());
    }
#endif
}
//最后的池边界标记会被弹出
  • 自动释放池的压栈(即push)操作中:
    当没有pool,即只有空占位符(存储在tls中)时,则创建页,压栈哨兵对象
    在页中压栈普通对象主要是通过next指针递增进行的,
    当页满了时,需要设置页的child对象为新建页
  • 出栈操作中
    通过next指针递减来实现一个释放
    当页空了时,需要赋值页的parent对象为当前页
相关推荐
秋雨梧桐叶落莳4 小时前
iOS——ZARA仿写项目
学习·macos·ios·objective-c·cocoa
人月神话Lee4 小时前
【图像处理】二值化与阈值——从灰度到黑白的决策
ios·ai编程·图像识别
美狐美颜SDK开放平台7 小时前
美颜SDK接入流程详解:Android、iOS、鸿蒙兼容方案解析
android·人工智能·ios·华为·harmonyos·美颜sdk·视频美颜sdk
90后的晨仔8 小时前
Combine 操作符 —— 打造强大的数据处理管道
ios
90后的晨仔8 小时前
Combine 高级操作符:掌控数据流的节奏与方向
ios
90后的晨仔8 小时前
Combine 与 SwiftUI 集成:构建响应式 UI 的黄金搭档
ios
2501_916007479 小时前
Xcode支持的编程语言、主要功能及使用指南
ide·vscode·macos·ios·个人开发·xcode·敏捷流程
MonkeyKing11 小时前
iOS 深入理解 UIView 与 CALayer:关系、渲染流程与坐标系
ios
君子木11 小时前
解决ios App的webview不支持<video>标签行内播放的问题(点击播放按钮后会直接全拼播放)
ios