前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >AutoreleasePool

AutoreleasePool

作者头像
CoderStar
发布2022-09-23 08:54:26
4250
发布2022-09-23 08:54:26
举报
文章被收录于专栏:CoderStarCoderStar

前言

Hi Coder,我是 CoderStar!

在 MRC 时代,我们可能会经常用到AutoreleasePool来帮助我们管理内存,在 ARC 时代,一些内存管理的操作被编译器替代了,不用再去手动的release以及autorelease等操作了,但是AutoreleasePool仍然在背后默默发挥着作用,并且有些场景下我们还是需要显式用到它,今天我们就来聊一聊AutoreleasePool

下列源码为Runtime objc内代码,版本之间可能会有差异,但是大致原理应该是一致的。

使用形式

代码语言:javascript
复制
// OC
@autoreleasepool {
    // 生成自动释放对象
}

// swift
autoreleasepool {
    // 生成自动释放对象
}

基本原理

@autoreleasepool 包裹的相关代码在编译时,编译器会自动把它编译为如下形式。

代码语言:javascript
复制
// 这个poolSentinelObj其实就是哨兵对象
void *poolSentinelObj = objc_autoreleasePoolPush();

// 自动释放池作用域 {} 中的代码

objc_autoreleasepoolPop(poolSentinelObj);

其实如果研究的更细一点,其中还会有一个__AtAutoreleasePool的结构存在,其仅仅是对上述两个方法更高一层的封装而已;

继而查看objc_autoreleasePoolPush以及objc_autoreleasepoolPop这两个函数的源码实现,如下:

代码语言:javascript
复制
void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

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

上面我们可以看到一个核心类,AutoreleasePoolPage

AutoreleasePoolPage结构

AutoreleasePoolPage 是一个 C++ 中的类,在 NSObject.mm 中的定义是这样的:

代码语言:javascript
复制
class AutoreleasePoolPage {
    // 对当前AutoreleasePoolPage 完整性的校验
    magic_t const magic;

    // 指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满
    id *next;

    // 当前线程,表明与线程有对应关系
    pthread_t const thread;

    // 指向父节点,第一个节点的 parent 值为 nil;
    AutoreleasePoolPage * const parent;

    // 指向子节点,最后一个节点的 child 值为 nil;
    AutoreleasePoolPage *child;

    // 代表深度,第一个page的depth为0,往后每递增一个page,depth会加1;
    uint32_t const depth;

    // 表示high water mark(最高水位标记)
    uint32_t hiwat;
};

注意查看注释

从上述的结构可以知道,其实每一个AutoreleasePool都是以AutoreleasePoolPage为节点用双向链表的形式连接起来的。

每个 AutoreleasePoolPage 对象有 4096 字节的存储空间, 除了存放它自己的成员变量(56 个字节,每个占 8 个字节)外, 剩下的空间用来存储后面加入的 autorelease 对象。

为什么每个 AutoreleasePoolPage 的大小设置成 4096 个字节呢?因为 4096 是虚拟内存一页的大小。

AutoreleasePool结构

大致流程

  • 当进入@autoreleasepool作用域时,objc_autoreleasePoolPush 方法被调用, runtime 会向当前的 AutoreleasePoolPage 中添加一个 nil 对象作为哨兵对象,并返回该哨兵对象的地址;
  • 对象调用autorelease方法,会被加入到对应的的AutoreleasePoolPage中去,next指针类似一个游标,不断变化,记录位置。如果加入的对象超出一页的大小,便会自动加一个新页。
  • 当离开@autoreleasepool作用域时,objc_autoreleasePoolPop(哨兵对象地址)方法被调用,其会从当前 page 的 next 指标的上一个元素开始查找, 直到最近一个哨兵对象, 依次向这个范围中的对象发送release消息;

因为哨兵对象的存在,自动释放池的嵌套也是满足的,不管是嵌套还是被嵌套的自动释放池,找自己对应的哨兵对象就行了。

下面看下具体源码流程分析。

源码流程分析

push 函数
代码语言:javascript
复制
// 哨兵对象定义
#define POOL_BOUNDARY nil

static inline void *push()
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        // 添加一个哨兵对象到自动释放池
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ...
    return dest;
}

//向自动释放池中添加对象
static inline id *autoreleaseFast(id obj)
{
    // 获取hotPage: 当前正在使用的Page
    AutoreleasePoolPage *page = hotPage();
    // 如果有page 并且 page没有被占满
    if (page && !page->full()) {
        // 添加一个对象
        return page->add(obj);
    } else if (page) {
        // 添加一个对象
        return autoreleaseFullPage(obj, page);
    } else {
        // 如果没有page,则创建一个page
        return autoreleaseNoPage(obj);
    }
}

// 创建一个新的page,并将当前page->child指向新的page,将对象添加进去
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    ...
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

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

// 创建一个新的page
id *autoreleaseNoPage(id obj)
{
    ...
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);
    ...
    // Push the requested object or pool.
    return page->add(obj);
}
pop 函数
代码语言:javascript
复制
// 查看源码发现pop函数最终会调用 releaseUntil
// 调用顺序为pop->popPage->releaseUntil

// stop 的值即为最初push时返回的哨兵对象的地址.
void releaseUntil(id *stop)
{
    // 循环依次向autorelease对象发送release消息
    while (this->next != stop) {
        // AutoreleasePoolPage 有cold和hot之分.hot是当前正在使用的,cold是没有使用的
        //获取当前正在使用的
        AutoreleasePoolPage *page = hotPage();

        // 如果为空,通过parent指针指向它的父节点,并将父节点置为当前使用的page
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        // 获取当前Page next指针的上一个元素
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        // 从next的上一个元素开始,向上查找只要不是哨兵对象,就向其发送release消息
        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}
autorelease 函数
代码语言:javascript
复制
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;
}

执行类型

AutoreleasePool一般会包括两种执行类型:

  • RunLoop 自动加入的AutoreleasePool
  • 手动添加AutoreleasePool

Runloop 自动加入

主线程 Runloop 中注册了两个 Observer,回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。两个 Observer 如下:

  • 监测 Entry 事件,回调里自动创建自动释放池,order-214748364, 优先级最高,保证创建释放池发生在其他所有回调之前;
  • 监测 BeforeWaitingExit 事件;
    • BeforeWaiting时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池。
    • Exit时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

系统的自动释放池也并不总是在 BeforeWaitingExit 才释放,在处理完 TimerSource 事件之后, 也可能会进行释放操作。

当然系统部分方法内部也自动添加了AutoreleasePool,比如:

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

代码语言:javascript
复制
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部 @autoreleasepool 包围着
}];

使用 NSThreaddetachNewThreadSelector:toTarget:withObject:方法创建新线程时,新线程自动带有 AutoreleasePool

...

手动添加

如果手动加了autoreleasepool,则在作用域大括号结束时释放;

那我们一般会在什么场景下手动添加呢?

CLI 程序

因为 GUI 程序拥有 RunLoop 机制的原因,每个周期都会进行释放,我们可能不用太过关注AutoreleasePool的使用,但是 CLI 程序可能我们就需要对其更关注一些了。

遍历中生成大量Autorelease局部变量

在遍历过程中生成大量Autorelease局部变量,会导致内存峰值比较高,我们手动加入AutoreleasePool可以降低内存使用峰值;

代码语言:javascript
复制
func loadBigData() {
    if let path = NSBundle.mainBundle().pathForResource("big", ofType: "jpg") {
        for i in 1...10000 {
            autoreleasepool {
                let data = NSData.dataWithContentsOfFile(path, options: nil, error: nil)
                NSThread.sleepForTimeInterval(0.5)
            }
        }
    }
}

这个地方稍微扩展一下,不是所有方式生成的对象都可以用这种方式去降低内存峰值,因为我们可以明确的是只有Autorelease类型的对象才会交给AutoreleasePool去管理,如果不是这类对象;

那什么样的对象才是Autorelease类型的呢?

  • 编译器会检查方法名是否以alloc, new, copy, mutableCopy 开始,如果不是则自动将返回值的对象注册到 AutoreleasePool 中,比如一些类方法;这个地方会有个点,如果你自定义的方法是用这几个关键单词开头的,clang 在编译的时候就就不会走``逻辑,我们可以利用clang attribute去处理,示例:- (id)allocObject __attribute__((objc_method_family(none))),其会将allocObject这个方法当做普通对象看待。
  • iOS 5 及之前的编译器,关键字 __weak 修饰的对象,会自动加入AutoreleasePool。iOS 5 及之后的编译器,则直接调用的 release,不会加入 AutoreleasePool
  • id 指针 (id *) 和对象指针(NSError *),会自动加上关键字 __autorealeasing,加入 AutoreleasePool

我们其实可以通过objc_autoreleaseReturnValue函数来标识一个对象是否加入到AutoreleasePool中去。同时该方法还附带了优化效果,objc_autoreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用objc_retainAutoreleasedReturnValue()函数,那么就不将返回的对象注册到AutoreleasePool,而是直接传递到方法或函数的调用方。

arc-runtime-objc-autoreleasereturnvalue[1]

常驻线程

子线程默认不会开启 Runloop,可能这时候会有小伙伴有疑问,那还会自动创建AutoreleasePool吗?

答案当然是会,其实根据上面的源码分析,我们就可以知道,当子线程如果没有创建 AutoreleasePool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage,也就是默认生成一个 AutoreleasePoolPage 来添加 Autorelease 对象。AutoreleasePool中的对象会等到线程销毁后得到释放。说到这里,我们就需要注意常驻线程了。如果是常驻线程,就容易导致线程中所有的Autorelease对象都迟迟得不到释放,所以需要手动添加AutoreleasePool,让相关对象可以得到及时释放。

代码语言:javascript
复制
class KeepAliveThreadManager {
    private init() {}
    static let shared = KeepAliveThreadManager()

    private(set) var thread: Thread?

    /// 开启常驻线程
    public func start() {
        if thread != nil, thread!.isExecuting {
            return
        }
        thread = Thread {
            autoreleasepool {
                let currentRunLoop = RunLoop.current

                // 如果想要加对该RunLoop的状态观察,需要在获取后添加,而不是等到启动之后再添加,

                currentRunLoop.add(Port(), forMode: .common)
                currentRunLoop.run()
            }
        }
        thread?.start()
    }

    /// 关闭常驻线程
    public func end() {
        thread?.cancel()
        thread = nil
    }
}

class Test: NSObject {
    func test() {
        if let thread = KeepAliveThreadManager.shared.thread {
            perform(#selector(task), on: thread, with: nil, waitUntilDone: false)
        }
    }

    @objc
    func task() {
        /// 在任务外加一层 autoreleasepool
        autoreleasepool {

        }
    }
}

main.m文件中的它

main函数中的 @autoreleasepool 只是负责管理它的作用域中的 autorelease 对象。

在 Xcode11 之前,是将整个应用程序运行放在 @autoreleasepool 内,由于 RunLoop 的存在,理论上这里的@autoreleasepool有点像摆设,根本没有发挥出作用。

Xcode 11 前

代码语言:javascript
复制
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

Xcode 11

在 Xcode 11 后,触发主线程 RunLoopUIApplicationMain 函数放在了 @autoreleasepool 外面,这可以保证 @autoreleasepool 中的 autorelease 对象在程序启动后立即释放。正如新版本的 @autoreleasepool 中的注释所写 "Setup code that might create autoreleased objects goes here.",这里的autoreleasepool是为了处理进入UIApplicationMain之前可能会产生的autorelease对象。

代码语言:javascript
复制
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

最后

大致把AutoreleasePool涉及的点简单摸了一遍,希望小伙伴能对其有一个更全面的认识。

要更加努力呀!

Let's be CoderStar!

  • 自动释放池的前世今生 ---- 深入解析 autoreleasepool[2]
  • iOS autoreleasePool 原理总结[3]
  • 黑幕背后的 Autorelease[4]
  • autoreleasepool 探究[5]
  • Transitioning to ARC Release Notes[6]
  • iOS - 聊聊 autorelease 和 @autoreleasepool[7]

参考资料

[1]arc-runtime-objc-autoreleasereturnvalue: https://clang.llvm.org/docs/AutomaticReferenceCounting.html#arc-runtime-objc-autoreleasereturnvalue

[2]自动释放池的前世今生 ---- 深入解析 autoreleasepool: https://draveness.me/autoreleasepool/

[3]iOS autoreleasePool 原理总结: https://www.jianshu.com/p/20496cbb6dc3

[4]黑幕背后的 Autorelease: https://blog.sunnyxx.com/2014/10/15/behind-autorelease/

[5]autoreleasepool 探究: https://github.com/Yuan91/autoreleasepool

[6]Transitioning to ARC Release Notes: https://developer.apple.com/library/archive/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011226

[7]iOS - 聊聊 autorelease 和 @autoreleasepool: https://juejin.cn/post/6844904094503567368

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-02-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 CoderStar 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 使用形式
  • 基本原理
    • AutoreleasePoolPage结构
      • 大致流程
        • 源码流程分析
          • push 函数
          • pop 函数
          • autorelease 函数
      • 执行类型
        • 主 Runloop 自动加入
          • 手动添加
            • CLI 程序
            • 遍历中生成大量Autorelease局部变量
            • 常驻线程
        • main.m文件中的它
        • 最后
          • 参考资料
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档