AutoreleasePoolPage 源码分析

说明:本文的源码均摘自 objc4-818.2 这个版本。AutoreleasePoolPage 的源码位于 objc4NSObject.mmarrow-up-right 中。

目录

简介

在声明 AutoreleasePoolPage 类的前面有一段关于它的注释:

Autorelease pool implementation

  • A thread's autorelease pool is a stack of pointers.

  • Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary.

  • A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released.

  • The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary.

  • Thread-local storage points to the hot page, where newly autoreleased objects are stored.

翻译(内容稍作了充实):

  • 自动释放池是以 AutoreleasePoolPage 为节点的双链表结构,其中 AutoreleasePoolPage存储指针的栈 (a stack of pointers) ,其存储的指针有两种类型:

    • 一种是代表自动释放池的边界的指针,这些指针指向 POOL_BOUNDARY(值为 nil ),也就是我们常说的哨兵,它是在 push() 方法中调用 autoreleaseFast(POOL_BOUNDARY) 的时候加入到 page 中的。当自动释放池执行 pop() 方法时,那些在哨兵 (sentinel) 之后添加的(对象指针指向的)对象都会被释放。

    • 另一种指针是需要自动释放的对象的指针,这些对象的指针是在调用 -autorelease 方法时加入到 page 中的。

  • 线程与自动释放池是一一对应的。

  • pool token 是指向自动释放池的 POOL_BOUNDARY 的指针(也就是哨兵)。

  • hot page 的指针存储在线程局部存储 (Thread-local storage) 中,它是当前存储自动释放对象的指针的 page

基础结构

AutoreleasePoolPageData

AutoreleasePoolPageData 位于 NSObject-internal.harrow-up-right 中,它封装了重要的成员变量:

字段解读:

  • magic :用于对当前 AutoreleasePoolPage 完整性的校验;

  • thread :当前的自动释放池对应的线程;

  • next :指向最新添加的 autorelease 对象的下一个位置,初始化时指向 begin()

  • parent :指向上一个 page ,第一个结点的 parent 值为 nil

  • child :指向下一个 page ,最后一个结点的 child 值为 nil

  • depth :代表当前 page 在双链表中的深度,从 0 开始,往后递增 1

  • hiwat :代表 high water mark 。❓

AutoreleasePoolPage

AutoreleasePoolPage 私有继承自 AutoreleasePoolPageData ,因此包含前面介绍的那些成员变量。除此之外还定义了一些独有的成员变量:

友元

thread_data_t

❓ 这个友元的作用待研究。。。

AutoreleasePoolPage 中的声明:

struct thread_data_t 的定义是:

public 成员变量

SIZE

SIZE 表示 AutoreleasePoolPage 的大小。在定义 SIZE 的地方用了 PROTECT_AUTORELEASEPOOL 宏做了控制:

在源码里搜索这个宏定义,可以看到其值是 0

因此,SIZE 的值是 PAGE_MIN_SIZE ,再来看 PAGE_MIN_SIZE 的定义:

1 左移了 12 位,其值是 2^12 ,也就是 4 * 1024 Byte = 4096 Byte4 KB)。因此可以得出结论一般情况下 AutoreleasePoolPage 的大小是 4 KB

如果将 PROTECT_AUTORELEASEPOOL 的值改为 1 ,那么 SIZE 的值是 PAGE_MAX_SIZE ,其定义是:

同理,值是 2^14 ,也就是 16 * 1024 Byte = 16384 Byte16 KB)。

private 成员变量

key

成员变量 key 的作用:通过此 key 从当前线程的局部存储中(TLS)取出 hot page 。

1、key 的类型:pthread_key_t

pthread_key_t 实际是一个 unsigned long 类型,它在系统头文件中的定义是:

2、key 的值:AUTORELEASE_POOL_KEY

__PTK_FRAMEWORK_OBJC_KEY3 定义在 tsd_private.h 文件中:

说明:

  • 可以在 Xcode 中使用 cmd + shift + o 搜索 __PTK_FRAMEWORK_OBJC_KEY3 这个宏;

  • tsd : 线程的私有数据 (thread specific data)

SCRIBBLE

releaseUntil 函数中,将 release 后空出的位置使用 SCRIBBLE 填充:

COUNT

可保存的对象指针的数量是 4096 / 8 = 512(指针的大小是 8 字节),实际可用容量要减去 page 自身的成员变量占用的 56 字节,也就是 7 个对象指针的大小 ,因此实际可存储 505 个对象指针。

宏(内部定义的)

EMPTY_POOL_PLACEHOLDER

注释:

EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is pushed and it has never contained any objects. This saves memory when the top level (i.e. libdispatch) pushes and pops pools but never uses them.

翻译:

当一个自动释放池被 push() 且它从未包含任何对象指针时,就将 EMPTY_POOL_PLACEHOLDER 存储在 TLS 中。当上层(如 libdispatchpush()pop() 自动释放池但从不使用它们时,这将节省内存。

POOL_BOUNDARY

可以看到 POOL_BOUNDARY 就是 nil ,也就是说哨兵指针都是指向 nil 的。

宏(外部定义的)

SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS

注释:Define SUPPORT_AUTORELEASEPOOL_DEDDUP_PTRS to combine consecutive pointers to the same object in autorelease pools

翻译:定义 SUPPORT_AUTORELEASEPOOL_DEDDUP_PTRS 来组合自动释放池中指向同一对象的连续指针。

这个宏在 64 位设备上的值是 1 ,在其他设备上的值是 0

PROTECT_AUTORELEASEPOOL

这个宏的值是 0 。将其设为 1 可以对自动释放池的内容执行 mprotect()

由于宏的默认值为 0 ,因此 protect()unprotect() 这两个函数实际上什么也没做:

构造方法 & 析构方法

private 方法

begin()

end()

empty()

full()

lessThanHalfFull()

add(id obj)

将一个对象指针添加到自动释放池。

简化版:

完整版:

SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS 在 64 位设备上是 1 ,里面的逻辑较多,可以暂时不看宏所包裹的内容。

releaseAll()

releaseUntil(id *stop)

kill()

将当前页面以及子页面全部删除。

从当前的 page 开始,一直根据 child 链向前走直到 child 为空,把经过的 page 全部执行 delete 操作(包括当前 page )。

kill() 被调用到的地方:

1) tls_dealloc(void *p) 方法中:

2) popPage() 方法中:

kill() 方法说明

  • 删除 page 使用的是 delete 关键字;

  • 循环使用的是 do...while ,所以会至少进行一次 delete

  • kill() 方法中的this 是调用它的 page 。比如调用 page->child->kill() ,此时 kill() 方法中的 this 就是指代 page->child

tls_dealloc(void *p)

tls_dealloc(void *p)线程局部存储 (Thread Local Stroge, TLS) 的析构函数,它是在 AutoreleasePoolPageinit() 方法中作为函数指针传入给 pthread_key_init_np() 函数的第二个参数的:

tls_dealloc(void *p) 中,要对自动释放池内的所有自动释放对象执行 release() 操作,然后调用 kill() 来释放所有的 page

pageForPointer(const void *p) & pageForPointer(uintptr_t p)

通过内存地址的计算,获取 p 指针所在的 page 的首地址。将指针与 page 的大小,也就是 4096 取模,得到当前指针的 offset ,再通过 (p - offset) 就能获取到 p 所在的 page 的起始地址:

而最后调用的方法 fastCheck() 用来检查当前的 result 是不是一个 AutoreleasePoolPage

通过检查 magic_t 结构体中的某个成员是否为 0xA1A1A1A1。

其中,uintptr_t 实际上就是 unsigned long

haveEmptyPoolPlaceholder() & setEmptyPoolPlaceholder()

判断 tls 中的 keyAUTORELEASE_POOL_KEY)对应的值是否是 EMPTY_POOL_PLACEHOLDER

tls 中将 keyAUTORELEASE_POOL_KEY)的值设置为 EMPTY_POOL_PLACEHOLDER

hotPage() & setHotPage(AutoreleasePoolPage *page)

tls 中获取 hotPage

hotPage 存入 tls

coldPage()

coldPage 是双链表中的第一个 page

autoreleaseFast(id obj)

AutoreleasePoolPagepush()autorelease(id obj) 方法最终都是调用的 autoreleaseFast(id obj)

在此方法中,先取出 hotPage 然后分三种情况处理:

  • page 存在且未满,则直接调用 page->add(obj)obj 存入到自动释放池中;

  • page 存在但已满,调用 autoreleaseFullPage(obj, page)

  • page 不存在,调用 autoreleaseNoPage(obj)

autoreleaseFullPage(id obj, AutoreleasePoolPage *page)

注释:

The hot page is full. Step to the next non-full page, adding a new page if necessary. Then add the object to that page.

解读:

hot page 已满,跳转到下一个未满的 page ,若不存在则添加新 page 。最后将 obj 添加到该 page 中。

如果存在未满的 page->child ,则将其设置为 hot page ;否则需要创建一个新的 page

最后执行 page->add(obj)obj 添加到自动释放池中。

autoreleaseNoPage(id obj)

注释:

"No page" could mean no pool has been pushed or an empty placeholder pool has been pushed and has no contents yet

翻译:

"No page" 指未曾执行 push() 因而还不存在 pool ,或者是执行 push() 后创建的是 empty placeholder pool 、因此里面还没有内容。

代码解读:

此方法创建了一个新的 page ,最后调用 page->add(obj)obj 添加到 page 中。

autoreleaseNewPage(id obj) (Debug only)

此方法应该只用于 debug 环境。

仅在 push() 中用到了此方法,通过 DebugPoolAllocation 进行的判断:

调用时的注释:Each autorelease pool starts on a new pool page

public 方法

autorelease(id obj)

  • 使用 obj->isTaggedPointerOrNil()Tagged Pointer 做了断言;

  • 调用了 autoreleaseFast(obj)obj 加入到自动释放池中;

  • dest 这个值只用于 ASSERT 中,因此使用 __unused 来修饰了。

push()

实际上调用了 autoreleaseFast(POOL_BOUNDARY) ,另一个autoreleaseNewPage(POOL_BOUNDARY) 仅用于 Debug 环境。

可以看到返回值 dest 有两种类型:要么是 EMPTY_POOL_PLACEHOLDER ,要么是指向 POOL_BOUNDARY 的指针。

返回值 dest 就是哨兵指针,它也是后面执行 pop(void *token) 时需要的参数 token

pop(void *token)

先调用 pageForPointer(token) 找到 token(也就是哨兵指针)所在的 page ,再调用了 popPage<false>(token, page, stop)

popPage(void *token, AutoreleasePoolPage *page, id *stop)

pop(void *token) 会调用此方法,然后此方法会调用 page->releaseUntil(stop) ,最后会调用 kill() 方法来删除 page

popPage() 方法中对 kill() 方法调用的说明:

  • 如果 page->child 存在,则调用 page->lessThanHalfFull() 方法检测“当前 page 存储的内容是否超过一半”:

    • 不超过一半,则删除 page->child 及之后的节点;

    • 超过一半,则保留 page->child ,删除 page->child->child 及之后的节点。

init()

说明:npnot portable 的缩写,代表不可移植。

这里调用 pthread_key_init_np(int, void (*)(void *)) 执行了 pthread 的初始化,其中:

  • 第一个参数:AutoreleasePoolPage::key ,值为 AUTORELEASE_POOL_KEY

  • 第二个参数:AutoreleasePoolPage::tls_dealloc ,是析构函数。

pthread_key_init_np() 的函数原型是:

注释的翻译:为静态键设置析构函数,因为它不是用 pthread_key_create() 创建的。

便捷的函数

objc_autoreleasePoolPush(void)

实际上调用了 AutoreleasePoolPage::push() 方法:

_objc_autoreleasePoolPush() 其实只是简单调用了 objc_autoreleasePoolPush()

push 的调用路径

注意:

  • push() 内调用 autoreleaseFast(id obj) 传入的参数是 POOL_BOUNDARY

  • autoreleaseFullPage(obj, page)autoreleaseNoPage(obj) 最终调会调用 add(obj) 方法。

objc_autoreleasePoolPop(void *ctxt)

实际上调用了 AutoreleasePoolPage::pop(void *token) 方法,这里传入的 ctxt 就是 token ,也就是哨兵指针:

同样,_objc_autoreleasePoolPop(void *ctxt) 也只是简单调用了 objc_autoreleasePoolPop(void *ctxt)

pop() 的调用路径

objc_autorelease(id obj)

1)objc_autorelease(id obj)

2)objc_object::autorelease()

如果 ISA()->hasCustomRR()false ,则直接调转到第 5 步,否则执行第 3 步:

3)-[NSObject autorelease]

4)_objc_rootAutorelease(id obj)

5)objc_object::rootAutorelease()

6)objc_object::rootAutorelease2()

7)AutoreleasePoolPage::autorelease(id obj)

8)autoreleaseFast(id obj)

autorelease 方法的调用栈:

参考:draveness《自动释放池的前世今生》arrow-up-right

❓ 这种分支图是用什么工具画的?

引申问题

因此可以得知,即使一个 NSTread 子线程没有使用 @autoreleasepool 包裹,对象在调用 autorelease 之后,最终会调用 autoreleaseNoPage(obj) 来创建一个自动释放池。

线程销毁时,会在 tls_dealloc(void *p) 方法中:

  • 先调用 coldPage() 方法找到双链表中的第一个 page

  • 再调用 objc_autoreleasePoolPop(page->begin()) 来释放所有 autorelease 对象;

  • 最后调用 page->kill() 来释放所有的 page 。

tls_dealloc(void *p) 方法中的核心代码:

结论

  • 如果某个 NSTread 子线程只需要执行一次任务就销毁,则autorelease 对象会在线程销毁时释放,不会引起内存泄漏;

  • 如果某个 NSTread 子线程是常驻子线程,却没有使用 @autoreleasepool 包裹,那么 autorelease 对象会因为没有释放而占用大量的内存,造成内存泄漏。(需要再研究一下 AFN2 的常驻线程)

因此常驻子线程的回调方法一定要使用 @autoreleasepool 包裹,以保障每次执行完回调后,产生的 autorelease 对象能得到及时释放。

objc_autoreleaseReturnValue

对 autorelease 的优化

SUPPORT_RETURN_AUTORELEASE

Fast handling of return through Cocoa's +0 autoreleasing convention.

The caller and callee cooperate to keep the returned object out of the autorelease pool and eliminate redundant retain/release pairs.

An optimized callee looks at the caller's instructions following the return. If the caller's instructions are also optimized then the callee skips all retain count operations: no autorelease, no retain/autorelease.

Instead it saves the result's current retain count (+0 or +1) in thread-local storage. If the caller does not look optimized then the callee performs autorelease or retain/autorelease as usual.

An optimized caller looks at the thread-local storage. If the result is set then it performs any retain or release needed to change the result from the retain count left by the callee to the retain count desired by the caller. Otherwise the caller assumes the result is currently at +0 from an unoptimized callee and performs any retain needed for that case.

  • There are two optimized callees:

    • objc_autoreleaseReturnValue: result is currently +1. The unoptimized path autoreleases it. objc_retainAutoreleaseReturnValue: result is currently +0. The unoptimized path retains and autoreleases it.

  • There are two optimized callers:

    • objc_retainAutoreleasedReturnValue: caller wants the value at +1. The unoptimized path retains it.

    • objc_unsafeClaimAutoreleasedReturnValue: caller wants the value at +0 unsafely. The unoptimized path does nothing.

Example:

Callee sees the optimized caller, sets TLS, and leaves the result at +1. Caller sees the TLS, clears it, and accepts the result at +1 as-is.

The callee's recognition of the optimized caller is architecture-dependent.

  • x86_64: Callee looks for mov rax, rdi followed by a call or jump instruction to objc_retainAutoreleasedReturnValue or objc_unsafeClaimAutoreleasedReturnValue.

  • i386: Callee looks for a magic nop movl %ebp, %ebp (frame pointer register)

  • armv7: Callee looks for a magic nop mov r7, r7 (frame pointer register).

  • arm64: Callee looks for a magic nop mov x29, x29 (frame pointer register).

Tagged pointer objects do participate in the optimized return scheme, because it saves message sends. They are not entered in the autorelease pool in the unoptimized case.

参考

Last updated