前言

Hi Coder,我是 CoderStar!

在 MRC 时代,我们可能会经常用到AutoreleasePool来帮助我们管理内存,在 ARC 时代,一些内存管理的操作被编译器替代了,不用再去手动的release链表和数组的区别autorelease等操作了,但是AutoreleasePool仍然在背后默默发挥着作用,并且有些场景下我们还是需要显式用到它,今天我们就来聊一聊Autoswift是什么意思啊rele嵌套函数怎么操作asePool

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

使用形ios15

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

基本原理

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

// 这个poolSentinelObj其实就是哨兵对象
void *poolSentinelObj = objc_autoreleasePoolPush();
// 自动释放池作用域 {} 中的代码
objc_autoreleasepoolPop(poolSentinelObj);

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

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

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

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

AutoreleasePoolPage结构

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

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

注意查看注释

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

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

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

AutoreleasePool

大致流程

  • 当进入@autor链表不具有的特点是eleasepool作用域时,objc_autoreleasePoolPuSwiftsh 方法被调用, runtime 会向当前的 Autorelease链表的定义PoolPage 中添加一个 nil 对象作为哨兵对象,并返回该哨兵对象的地址;
  • 对象调用autorelease方法,会被加入到对应的的AutoreleasePoolPSwiftage中去,next指针类似一个游标,不断变化,记函数调用可以作为一个函数的形参录位置。如果加入的对象超出一页的大小,便会自动加一个新页。ios鲁多多app
  • 当离开@auto嵌套是什么意思releasepool作用域时,obj嵌套c_autoreleasePoolPop(哨兵对象地址)方法被调用,其会从当前 pag嵌套函数怎么操作e 的 next 指标的上一个元素开始查找, 直到最近一个哨兵对象, 依函数调用关系图次向这个范围中的对象发送release消息;

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

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

源码流程分析

pu链表的定义sh 函数

// 哨兵对象定义
#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 函数

// 查看源码发现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);
}

autoreleasios越狱e 函数

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 自动加入的Autios14.4.1更新了什么oreleasePool
  • 手动添加AutoreleasePool

Runloop 自动加入

主线程 Runloop 中注册了嵌套函数两个 Observer,回调都是 _wr链表的定义apRunLoopWithAutoreleasePoolHandler(swiftcode代码查询)。两个 Observer 如下:

  • 监测 Entry 事件,回调里自动创建自动释放池,order-214748364, 优先级最高,保证创建链表的定义释放池发生在其他所有回调之前;
  • 监测 BeforeWaitingExios12it 事件;
    • BeforeWaiting函数调用语句时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池。
    • Exit时调用 _objc_autoreleasePo链表和数组的区别olPop() 来释放自动释放池。这个 Observer 的 order214748364嵌套函数7,优先级swift系统最低,保证其释放池子发生在其他所有回调之后。

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

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

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

    [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        // 这里被一个局部 @autoreleasepool 包围着
    }];
    
  • 使用 N嵌套SThreaddetachNewThreadSelector:toTarget:withObject:方法创函数调用方法建新线程时,新线程自动带有 AutoreleasePool

手动添加

如果手动加了autoreleaswift国际结算系统sswiftkeyepool链表和数组的区别则在作用域大括号结束时释放;

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

CLI 程序

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

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

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

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类型的呢?

  • 编译器会检查方法名是swift翻译否以swift翻译alloc, nswiftcode代码查询ew, copy, mutableCopy 开始,swifter如果不是嵌套循环则自动将返回值链表反转的对象注册到 AutoreleasePool 中,比如一些类方法;

    这个地方会有个点,如果你自定函数调用义的方法是用这几个关键单词开头的,clang 在编译的时候就就不会走“逻辑,我们可以链表和数组的区别利用clang attribute去处理,示例:- (id)allocObject __attribute__((objc_method_family(none))),其会将函数调用方法allocObject这个方法当做普通对象看待。

  • iOS 5 及之前的编译器,swift代码关键字 __weak 修饰swiftly的对象,会自动加入AutoreleasePool。iOS 5 及之后的编译ios12器,则直接调用的 relswift系统ease,不会加入 Autorswift翻译eleasePool
  • id 指针 (id *) 和swift国际结算系统对象指针(NSError *),会自动加上关键字 __autorealeasingswift国际结算系统加入 AutoreleasePool

我们其实可以通过objc_autoreleaseRios鲁多多appeturnValue函数来标识一个对象是否加入到AutoreleasePool中去。同时该方法还附带了优化效果链表objc_autswift翻译oreleaseReturnValue函数会检查使用该函数的方法或函数调用方的执行命ios鲁多多app令列表,如果方法或函数的调用方在调用了方法或函数后紧接着调用obios越狱jc_retainAutoreleasedReturnValue()函数,那么就不将返回的对象注册ios是什么意思AutoreleasePool,而是直接传递到方法或函数的调用方。

aios下载rc-runtime-objc-autoreleasereturnvalue

常驻线程

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

答案当然是会,其实根函数调用后必须带回返回值吗据上面的源码分析,我们就可以知道,当子线程如果没有创建 AutoreleasePool函数调用语句 ,但是产生了 Autorelease 对象,就会调用 a链表是否能随机访问元素utoreleaseswiftlyNoPage 方法。在这个方法中,会自动帮你创建一个 hotpag嵌套原理e,也就是默认生链表逆置成一个 Autor函数调用的三种方式eleasePoolPage 来添加 Autorelease 对象。AutoreleasePool中的对象会等到线程销毁后得到释放。说到这里,我们就需要链表和数组的区别注意常驻线程了链表数据结构。如果是常驻线程,就容易导致线链表适用于什么查找程中所有的Autorelease对象都迟迟得不到释放,所以需要手动添加AutoreleasePool,让相关对象可以得到及时释放。

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函数中的 @autoswiftkeyreleas链表和数组的区别epool 只是负责管理它的作用域中的 autorelease 对象。

在 Xcode11 之前,是将整个应用程序运行放在 @autoreleasep链表oolswiftly,由于 RunLoop 的存在,理iOS论上这里的@autoreleasepool有点像摆设函数调用关系图,根本没有发挥出作用。

Xcode 11 前ios越狱

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

Xcode 11

在 Xcode 11 后,触发主线程 RunLoopios15 UIApplicat链表是否能随机访问元素ionMain 函数放在了 @autoreleasepool 外面,这可以保证 @autoreleasepool 中的 autorelease 对象在程序启动后立即释放。正如新版本的 @autoreleasepool 中的注释所写 “Setup code that might create autoreleased objects goes here.函数调用后必须带回返回值吗“,这里的autoreleasepoolios系统是为了处理进入UIApplicationMain之前可能会产生的autorelease对象swiftly

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涉及的点简单摸了一遍,希望小链表c语言伙伴能对其有一个更全面的认识。

要更加努力呀!

Let’s be CoderStar!

  • 自动ios启动器释放池的前世今生 —- 深入解析 autoreleasepool
  • iOS autoreleasePool 原理总结链表和数组的区别
  • 黑幕背后的 Autorelease
  • autoreleasepool 探究
  • Transitioning to ARC Release Notes
  • iOS – 聊聊 autorelease 和 @autoreleasepool