全文速览
- 引子
- activities与order的含义
- _wrapRunLoopWithAutoreleasePoolHandler反汇编分析
- autorelease在runloop中的调用时机
- NSPushAutoreleasePool与NSPopAutoreleasePool的调用情况
- AutoreleasePoolPage详解
- hotPage与codePage
- objc_autoreleasePoolPush
- objc_autoreleasePoolPop
- @autoreleasepool
引子
先对autorelease的调用情况有个基本认知,新建iOS工程,直接在viewDidLoad
中打上断点,如图:
运行程序后在控制台执行po [NSRunLoop mainRunLoop]
:
可见在iOS中,系统给mainRunLoop添加了6个observer,其中2个observer对应的callback都是_wrapRunLoopWithAutoreleasePoolHandler
函数,并且两者函数地址相同,所以这两个callback是同一函数。
activities与order的含义
这两个observer的activities分别为0x1
与0xa0
,order分别为-2147483647
与2147483647
。
从文档中的这句话可知,order越小优先级越高,所以activities为0x1
回调的优先级大于activities为0xa0
的回调
activities是一组用来标识runloop状态的标识符,可通过CFRunLoopActivity
得到:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
typedef unsigned long CFOptionFlags;
对照上文,0x1
就是kCFRunLoopEntry
,而0xa0
则是kCFRunLoopBeforeWaiting | kCFRunLoopExit
两者组合。
所以_wrapRunLoopWithAutoreleasePoolHandler
会检测3种runloop状态,当runloop状态为kCFRunLoopEntry 时会最先回调,而kCFRunLoopBeforeWaiting 与kCFRunLoopExit 状态则最后回调。
对callback函数地址下断点,发现他在UIKitCore
中。如果通过image lookup
来查找这个函数还可以看到,除UIKitCore外,BaseBoard与MapKit中也有同名函数。
_wrapRunLoopWithAutoreleasePoolHandler反汇编分析
打上符号断点后,重新运行程序:
可以看到,这里会对activities的值进行检测,检测值分别为0x80
、0x20
与0x1
,对照CFRunLoopActivity ,分别为kCFRunLoopExit 、kCFRunLoopBeforeWaiting 与kCFRunLoopEntry ,这与上文相互验证。
如果activities为0x80,跳转到
0x7fff47848ced
处,后续会调用NSPopAutoreleasePool,后面还有个与0x20的判等比较,由于不等所以走后续return流程如果activities为0x20,同样跳转到
0x7fff47848ced
处,后续调用NSPopAutoreleasePool,由于0x20判等比较,此时相等,跳转到0x7fff47848cd1
处,接着执行NSPushAutoreleasePool。由于开头将rsp
指向rbp
,后面执行popq %rbp
会跳出当前函数如果activities不是0x1,会跳转到
0x7fff47848d2e
处,后续结束当前函数。因为当前函数的activities只可能是0x80、0x20、0x1这三个数,前面判等0x80与0x20后仍然没有跳转,那么当前activities一定是0x1,此时同样调用NSPushAutoreleasePool后,通过popq %rbp
跳出当前函数
autorelease在runloop中的调用时机
通过刚才的反汇编分析,可以得到以下结论:
当前runloop状态为kCFRunLoopExit(退出runloop) 时,会调用NSPopAutoreleasePool,由于优先级最低,此处可确保在其他回调完成后释放缓存池
当前runloop状态为kCFRunLoopBeforeWaiting(runloop即将休眠)时,会先调用NSPopAutoreleasePool,再调用NSPushAutoreleasePool。对应着释放旧池并创建新池,由于优先级最低,这一操作也在其他回调之后
当前runloop状态为kCFRunLoopEntry(进入runloop) 时,会调用NSPushAutoreleasePool,由于此时优先级最高,可以确保创建缓存池在其他回调之前
NSPushAutoreleasePool与NSPopAutoreleasePool的调用情况
可见,NSPushAutoreleasePool 会调用objc_autoreleasePoolPush,而NSPopAutoreleasePool 会调用objc_autoreleasePoolPop。
void *objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
两者都通过AutoreleasePoolPage
类调用了对应的push和pop函数
AutoreleasePoolPage详解
本文采用当前最新objc4-756.2
源码,在NSObject.mm
中可以看到AutoreleasePoolPage定义。去除内部函数后,AutoreleasePoolPage可简化为如下所示:
class AutoreleasePoolPage
{
// 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.
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
显然,AutoreleasePoolPage是个双向链表
- EMPTY_POOL_PLACEHOLDER
从注释可知,EMPTY_POOL_PLACEHOLDER 用于标识一个空的自动释放池,一些系统方法中的block,如NSArray的实例方法:
- (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
对应文档中有这么句话:
This method executes synchronously. Values allocated within the block are deallocated after the block is executed.
显然,block内部添加了自动释放池,但如果block内部并无其他autorelease对象,且block的参数并未被使用,这个自动释放池会被标识为EMPTY_POOL_PLACEHOLDER
POOL_BOUNDARY
自动释放池的边界标识,又称哨兵对象。push时会向池中添加哨兵对象,pop时会对哨兵对象之前的对象发送release消息key
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
#define AUTORELEASE_POOL_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
#define __PTK_FRAMEWORK_OBJC_KEY3 43
key是一个静态常量43,用来设置/取出当前page
SCRIBBLE
在releaseUntil函数中有这么句代码memset((void*)page->next, SCRIBBLE, sizeof(*page->next))
,可见SCRIBBLE只是个填充字节SIZE
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
#define PROTECT_AUTORELEASEPOOL 0
#define PAGE_MAX_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096 /* bytes per 80386 page */
每个AutoreleasePoolPage对象大小为4096
COUNT
AutoreleasePoolPage可存储的对象个数,因为SIZE等于4096个字节,所以COUNT等于512个,由于AutoreleasePoolPage本身成员变量占用56个字节(下文会说为什么占56个字节),所以一个page实际能存储的autorelease对象为505个magic
struct magic_t {
...
uint32_t m[4];
...
}
magic_t const magic;
magic是结构体magic_t的实例,用于校验AutoreleasePoolPage的完整性,共占16个字节
next
next用于指向当前page中下一个要存储autorelease的位置,这是个栈结构,源码中用*next++ = obj
来存储autorelease对象,用id obj = *--page->next
来取出autorelease对象进行release,next是个指针,共占8字节thread
pthread_t const thread;
typedef __darwin_pthread_t pthread_t;
typedef struct _opaque_pthread_t *__darwin_pthread_t;
thread为当前page所属线程,是个指针,共占8字节
parent与child
parent与child分别指向当前page的父节点与子节点,两者都是指针,都占8字节depth与hiwat
depth为链表深度,hiwat用于记录链表中存储autorelease对象的最大个数,两者各站4字节page在未添加autorelease对象时占用的字节数
16 magic + 8 next + 8 thread + 8 parent + 8 child + 4 depth + 4 hiwat
,即56字节
hotPage与codePage
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
hotPage会从TLS
中取出当前page并返回,而codePage则是找到这个双向链表的头部然后返回
objc_autoreleasePoolPush
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
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);
}
}
# define POOL_BOUNDARY nil
- 当前页未满,直接添加obj
- 当前页已满,通过autoreleaseFast 找到page的子节点,直到子节点的page未满,将obj添加到未满的page上
- 没有page,通过autoreleaseNoPage创建page添加obj
由于push中调用的autoreleaseFast的参数为POOL_BOUNDARY,而这个宏是nil,所以objc_autoreleasePoolPush其实是向page中新增一个nil,以此来作为边界区分,这也就是所谓的哨兵对象,而push函数会返回这个哨兵对象的地址。
objc_autoreleasePoolPop
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
if (hotPage()) {
pop(coldPage()->begin());
} else {
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
if (DebugPoolAllocation && page->empty()) {
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
page->kill();
setHotPage(nil);
}
else if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
先对通过token判断是否为空page,如果不是则通过token找到所在page,取出toekn存储的对象,如果取出的对象不是哨兵对象,判断这个page是否为表头,若是则表示当前对象已被释放,否则通过badPop函数抛异常。
通过page调用releaseUntil
函数进行release,这是pop函数的精华所在:
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;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
通过id obj = *--page->next
取出autorelease对象,若不是哨兵对象,则调用objc_release
函数进行release操作
最后来看一下kill函数:
void kill()
{
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
page = page->parent;
if (page) {
page->unprotect();
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
他其实是kill调用page及之后的所有子page
所以pop后半部分kill的逻辑就是:
- 如果可debug且当前page为空,则kill掉当前page及之后的所有子page,并设置父page为当前page
- 如果是表头,则kill表头及之后所有的子page,并设置当前page为nil
- 如果当前page有子page,并且当且page使用率未过半,则kill当前page子page及之后所有的子page;如果当前page有子page,并且当前page使用率过半,则判断子page的子page,如果存在,则kill掉
page->child->child
及之后的所有子page
@autoreleasepool
简单测试一下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
通过一下命令生成cpp文件:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
可见还是调用objc_autoreleasePoolPush 与objc_autoreleasePoolPop这两个函数
Have fun!