根本运用

在 ARC 中,运用 AutoreleasePool 非常简单,只需形如以下办法调用即可,编译器会将块中的目标刺进相似如 [obj autorelease]; 相同的代码,在超出 AutoreleasePool 块作用域后会主动调用目标的 release 办法,这能推迟目标的开释。但一般来说,并不需求显式运用 @autoreleasepool{ },这是因为在主线程 RunLoop 的每个周期中都会主动进行主动开释池的创立和毁掉。

@autoreleasepool {
}

什么目标会归入到 AutoreleasePool 中?

除了显式参加 __autoreleasing 一切权润饰目标外,还有些目标会直接被隐式归入 AutoReleasePool 办理。

  • 非自己生成并持有的目标

编译器会检查办法名是否以 allocnewcopymutableCopy 开始,假如不是则主动将其回来值注册到 AutoreleasePool 中。ARC 经过命名约定将内存办理标准化,本来 ARC 也能够直接舍弃 autorelease 这个概念,而且规定,一切从办法中回来的目标其引用计数比预期的多 1,但这样做就破坏了向后兼容性(backward compatibility),无法与不运用 ARC 的代码兼容。

不过运用 clang attribute ,如 - (id)allocObject __attribute__((objc_method_family(none))),会将allocObject 这个办法作为一般办法回来目标看待。

在一般办法回来目标后,或许会将目标 retain 一次以进行强持有。例如以下的代码会被翻译为:

EOCPerson _myPerson = [EOCPerson personWithName: @"Bob Smith"]; // 会调用 `autorelease`
// 被翻译为
EOCPerson *tmp = [EOCPerson personWithName: @"Bob Smith"];
_myPerson = [tmp retain];

其中 autoreleaseretain 结对出现,是多余的,为了进步性能能够将其删去。所以编译器在被调用方选用 objc_retainAutoreleaseReturnValue 办法取代 autorelease ,会检查行将履行的那段代码是否会履行 retain 操作,若有则会在线程部分存储(TLS,Thread Local Storage)中存储这个目标,不履行 autorelease 操作;在调用方选用 objc_retainAutoreleasedReturnValue 办法取代 retain ,会检测 TLS 是否存了这个目标,若有则直接回来这个目标,不进行 retain 操作。

  • id 的指针或目标的指针

id 的指针(id **)和目标的指针(NSError **),假如没有显式指定,会主动加上关键字 __autoreleasing,注册到 AutoreleasePool 中。

  • 关于 __weak1 润饰的目标

在 LLVM 8.0 之前的编译器,关键字 __weak 润饰的目标,会主动注册到 AutoreleasePool 中;在 LLVM 8.0 以及之后的编译器,则会直接调用 release 办法。

什么时分显式运用 @autoreleasepool?

  • CLI(Command-line interface)程序

在 Cocoa 结构中因为有 RunLoop 机制的原因,每个周期都会进行主动开释池的创立与开释,但在 CLI 中意味着不会定时清理内存,因而需求更多关注。

  • 循环中使生成很多部分变量

再循环进程中产生了很多的部分变量,会导致内存峰值过高,因而手动参加 @autoreleasepool 能够降低内存运用峰值。

虽然只要 Autorelease 目标(也即上文说到的哪些目标会归入 AutoreleasePool 办理)会归入AutoreleasePool 办理,但这能够运用块机制,让编译器将在块结尾主动刺进 release 代码。

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)
                let person = Person("nihao") //也会开释
                NSThread.sleepForTimeInterval(0.5)
            }
        }
    }
}
  • 常驻线程

主线程的 RunLoop 会在每个周期进行主动开释池的创立与开释,子线程则不会,一起子线程也不一定会有 RunLoop。但只要是 Autorelease 目标,就会主动归入 AutoreleasePool 办理,每个线程都会主动创立并办理自己的主动开释池,比及线程毁掉的时分开释。但常驻线程中的目标因线程无法毁掉迟迟得不到开释,这就需求手动添加 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 {
        }
    }
}

与 RunLoop 的关系

主线程在 RunLoop 中注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler

  • 第一个 Observer 监测 Entry 事情(行将进入 RunLoop)

回调内部会调用 _objc_autoreleasePoolPush 创立主动开释池,其 order = -214748364,优先级最高,确保创立主动开释池在其他一切回调之前。

  • 第二个 Observer 监测 BeforeWaitingExit 事情

BeforeWaiting 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 开释旧的主动开释池并创立新的主动开释池。

Exit 时调用 _objc_autoreleasePoolPop 来开释主动开释池,其 order = 2147483647,优先级最低,确保其其它回调都在开释主动开释池之前。

AutoReleasePool 源码

形如 _objc_autoreleasePoolPush_objc_autoreleasePoolPushobjc_autorelease 其内部都是调用 AutoreleasePoolPage 的相关静态办法。因而其源码首要是对 AutoreleasePoolPage 的探究。

以下参阅的源码为 objc4-838。

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
} 
void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}
__attribute__((noinline,used))
id 
objc_object::rootAutorelease2()
{
    ASSERT(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}

AutoreleasePoolPage 的数据结构

AutoreleasePoolPage 继承自 AutoreleasePoolPageDataAutoreleasePoolPageData 存储了主动开释池实例目标的信息,而 AutoreleasePoolPage 里则存储了全局一切的主动开释池的所需信息,因而其特点类型也都是 static const

struct AutoreleasePoolPageData
{
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    // 用来优化同一目标屡次参加 AutoreleasePoolPage,只需记载其地址与数量,无需重复递增,节省空间
    struct AutoreleasePoolEntry {
        uintptr_t ptr: 48;
        uintptr_t count: 16;
        static const uintptr_t maxCount = 65535; // 2^16 - 1
    };
#endif
  // 对当时 AutoreleasePool 完整性校验
	magic_t const magic;
  // 指向下一个行将产生的 autorelease 目标的方位
	__unsafe_unretained id *next;
  // 相关的线程
	pthread_t const thread;
  // 指向父节点
	AutoreleasePoolPage * const parent;
  // 指向字节点
	AutoreleasePoolPage *child;
  // 链表的深度
	uint32_t const depth;
  // 水位线(DEBUG 运用,用作判别上次和这次的目标添加数量)
	uint32_t hiwat;
};

nextparentchild 的结构来看,构成了以栈作为节点的双向链表,每个 AutorleasePoolPage 的巨细为 4096 个字节。

iOS中的内存管理|AutoReleasePool

值的留意的是,引入了 AutoreleasePoolEntry 结构,用作将同一目标屡次进行 autorelease 操作时的优化,这时不会将 next 递增,而是将 AutoreleasePoolEntrycount 递增,得以优化内存空间。这里将 ptrcount 指定存储巨细,其总巨细为 64 字节,与 id 类型指针巨细相同,使得 AutoreleasePoolEntry 和 一般的 id 类型能够互操作。

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
public:
  // 每个 Page 的巨细,为 4096 字节(虚拟内存一页的巨细)
	static size_t const SIZE = PAGE_MIN_SIZE;
private:
  // 关于 AutoreleasePool 的 Key,用来查找存储在 TLS 中线程的 HotPage
	static pthread_key_t const key = AUTORELEASE_POOL_KEY;
  // 开释目标后用 0xA3A3A3A3 占位
	static uint8_t const SCRIBBLE = 0xA3;
  // 存储目标个数
	static size_t const COUNT = SIZE / sizeof(id);
  // 最大错误数量(DEBUG 运用)
  static size_t const MAX_FAULTS = 2;
}

关于 AutoreleasePoolPage 的静态特点,其中比较重要的:

  • size 固定为 4096,刚好为虚拟内存巨细的一页。
  • key43,用作线程部分存储的 Key,存储的是线程所属的 hotPage,隔离区分其他线程的 AutoreleasePoolPage
  • SCRIBBLE0xA3A3A3A3,在用作占位开释掉的 next 指针,标识为未初始化的地址。

AutoreleasePoolPage::push()

AutoreleasePoolPage::push() 创立一个主动开释池,实际上是刺进一个 POOL_BOUNDARY (岗兵目标,指向 nil)用来表明不同的主动开释池,去除去 DEBUG 调试和一些边界条件,其首要逻辑会集在 autoreleaseFast 办法中,依据 hotPage 的状况分为三种情况:

关于 hotPage,其存储在 TLS 中,表明当时正活跃的 Page;与之相对应是 coldPage,指向的是双向链表的头节点。

// 此处的 obj 为 POOL_BOUNDARY
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 而且 hotPage 未满

这是直接调用 hotPageadd 实例办法,依据宏界说,判别是否运用 AutoreleasePoolEntry 类型优化同一目标的屡次 autorelease,否则直接参加,next 指向下一个将要参加 AutoreleasePoolPage 的地址。

  • 存在 hotPage 而且 hotPage 已满

hotPage ,遍历找一个未满的子节点,若没有则创立一个 AutoreleasePoolPage ,随后将找到或生成的 page 置为 hotPage (运用 TLS 机制),并将目标 addhotPage 中。

  • 不存在 hotPage

创立一个 AutoreleasePoolPage ,将其设置为 hotPage,并将目标参加到 hotPage 中。

AutoreleasePoolPage::pop(ctxt)

pop 办法需求传入参数,在 _objc_autoreleasePoolPush 中是传入 push 办法回来的参数,push 回来的是存储的岗兵目标的地址,因而传入的也是岗兵目标的地址。

不过该办法也或许在其他地方调用,假如是岗兵目标的地址会毁掉整个以岗兵目标开始的单个主动开释池,还有或许毁掉整个主动开释池,其办法首要逻辑如下:

static inline void
pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        page = hotPage();
        if (!page) {
            // 假如整个主动开释池为空,仅有占位符,以 nil 填充
            return setHotPage(nil);
        }
        // 从头节点开始,移除主动开释池中的一切内容
        page = coldPage();
        token = page->begin();
    } else {
        page = pageForPointer(token);
    }
    stop = (id *)token;
    return popPage<false>(token, page, stop);
}

如何经过到 token(也即传入的岗兵目标的地址) 查找到所对应的 AutoreleasePoolPage ,在 pageForPointer 办法中。

这里有个前置条件,AutoreleasePoolPage 会经过 malloc_zone_memalign 办法分配内存,因而每个 AutoreleasePoolPage 的地址都是 SIZE(4096)的倍数,也便是地址会进行对齐,在与 SIZE 进行取余操作后,得到相对于 token 所在的 AutoreleasePoolPage 的偏移,相减则就能得到其首地址。

static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;
    ASSERT(offset >= sizeof(AutoreleasePoolPage));
    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();
    return result;
}

popPage 办法中,从 hotPage 开始,一向进行出栈操作,也即会 objc_release(obj);,直到满足栈首的地址与 stop 的地址一致,之后会调用 child / child-> childkill 办法,将一切的子节点毁掉。

有意思的是,会依据子节点的状况(子节点中已存储巨细小于总巨细的一半)进行区分,更有意思的是,在进行 releaseUntil 办法时,会将每一个子节点清空,里边也会判别 ASSERT(page->empty());,因而只会调用 page->child->kill();

不过这里说到一个概念,迟滞现象(hysteresis),wiki 是这样解释的:

一系统经过某一输入路径之运作后,即使换回开始的状况时相同的输入值,状况也不能回到其初始。

估测是虽然需求将一切子节点清空,可是系统不同以往了,或许后续需求从头创立子节点,这里先不清空,为后续运用进步效率。

template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    page->releaseUntil(stop);
    if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

objc_autorelease

去除去一些优化条件,如是否是 taggedPointer 指针,是否选用 TLS 优化 autorelease 过程 (上文说到),是否是类目标等。

一般最终会指向 AutoreleasePoolPage::autorelease((id)this); ,这与 AutoreleasePoolPage::push() 的分析情况一致。

static inline id autorelease(id obj)
{
    ASSERT(!_objc_isTaggedPointerOrNil(obj));
    id *dest __unused = autoreleaseFast(obj);
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  (id)((AutoreleasePoolEntry *)dest)->ptr == obj);
#else
    ASSERT(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
#endif
    return obj;
}

本文同步发布于公众号(nihao的编程随记)和个人博客nihao,欢迎 follow 以取得更佳观看体验。

[1] Why is @autoreleasepool still needed with arc

[2] Objective-C 高档编程 iOS与OS X多线程和内存办理

[3] AutoreleasePool

[4] Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效办法

[5] Why __weak object will be added to autorelease pool?

[6] 黑暗地的Autorelease

[7] iOS AutoreleasePool

[8] nihao_objc4_838

[9] 迟滞现象