autoreleasepool
概念
autoreleasepool
本质是自动延迟对象的释放,即对象使用完之后,它不会立即释放,而是加入到释放池,等到某个合适的时刻,对释放池中的对象进行统一释放。官方文档对主线程的自动释放池有这么一段描述:
The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.
ARC
与MRC
下autoreleasepool
的区别
MRC
下需要手动管理自动释放池的创建和释放,MRC
下只需要使用@autoreleasepool
将对应的代码包含起来即可。
- (void)MRCTest { Person *person; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; person = [[Person alloc] initWithName:@"jam" age:24]; [person autorelease]; NSLog(@"before pool release person: %@", person); [pool release]; NSLog(@"after pool release person: %@", person); //crash } 输出结果: before pool release person: name:jam, age:24 crash ... - (void)ARCTest { Person *person; @autoreleasepool { person = [[Person alloc] initWithName:@"jam" age:24]; NSLog(@"before end release pool person: %@", person); } NSLog(@"after end release pool person: %@", person); } 输出结果: before end release pool person: name:jam, age:24 after end release pool person: name:jam, age:24 复制代码
根据日志输出得知:MRC下调用自动释放池release
方法后,会对在autorelease
对象进行释放,因此,此后访问的person
变量为野指针,再去访问自然会导致crash。而ARC下,@autoreleasepool
并不会立即在结束括号符后,立即释放person
变量,而是会在一个合适的时间点。具体是在什么时候,下面会讲解到。
ps:x-code下对特定文件设置使用MRC的方式:-fno-objc-arc
autoreleasepool
与runloop
的关系在断点调试中,使用
po [NSRunLoop currentLoop]
由上图可知:自动释放池在runloop
中注册了两个observer,分别都会以_wrapRunLoopWithAutoreleasePoolHandler
进行回调。不过两个observer中的activities
和order
有些不同。
a. 首先看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 }; 复制代码
第一个observer
的activities
为0x01
,即kCFRunLoopEntry
,第二个observer
的activities
为0xa0
(转换为二进制为10100000
),即kCFRunLoopBeforeWaiting | kCFRunLoopExit
。
b. 两者order
的区别,这里的order
表示的是runloop
执行事件的优先级。
order = -2147483647 order = 2147483647 int32 max: 2147483647 int32 min: -2147483648 复制代码
根据上面activities
和order
的对比,得知:
第一个observer
在runloop
监听kCFRunLoopEntry
时的优先级为-2147483647
(优先级最高),即保证该observer回调会发生在其他事件回调之前。
第二个observer
在runloop
监听kCFRunLoopBeforeWaiting | kCFRunLoopExit
时的优先级为2147483647
,即保证该observer回调会发生在其他事件回调之后
这两个observer分别在回调时对自动释放池进行了什么操作呢?我们通过一个小例子来看看
Person *p; //此处打断点 p = [[Person alloc] initWithName:@"jam" age:24]; NSLog(@"p: %@", p); 复制代码
我们先在声明临时变量p
处设置一个断点,然后使用watchpoint set variable p
命令监测变量p的变化,然后继续运行程序,会不断触发到断点,其中会在某个时刻分别显示这么两段内容:
CoreFoundation`objc_autoreleasePoolPush: -> 0x107e6a2fc <+0>: jmpq *0x1e88d6(%rip) ; (void *)0x000000010a9bd50f: objc_autoreleasePoolPush CoreFoundation`objc_autoreleasePoolPop: -> 0x107e6a2f6 <+0>: jmpq *0x1e88d4(%rip) ; (void *)0x000000010a9bd5b3: objc_autoreleasePoolPop 复制代码
很明显这两段内容是跟自动释放池相关,分别对应释放池的push
和pop
操作,而这两个操作其实就是通过上面两个observer
的回调之后的相关调用。(这两者的关联的确没有什么很好的证据证明,只能说是根据上面的例子推测而来)
因此,当runloop
进入kCFRunLoopEntry
时,自动释放池会进行push
操作,当runloop
进入kCFRunLoopBeforeWaiting | kCFRunLoopExit
状态时,自动释放池会进行pop
操作。即系统在每一个runloop迭代中都加入了自动释放池push和pop。
@autoreleasepool
的原理通过使用clang编译器对
main.m
文件进行重新改写为cpp文件来一探究竟。
clang -rewrite-objc main.m 复制代码
运行后,发现会出错,提示fatal error: 'UIKit/UIKit.h' file not found
,此时,可以通过下面的命令来解决:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m 复制代码
其实这里主要是通过-isysroot
选项指定了编译所使用的的SDK目录,即x-code下的SDK目录。
//.m 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); } //.cpp int main(int argc, char * argv[]) { NSString * appDelegateClassName; /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))); } return UIApplicationMain(argc, argv, __null, appDelegateClassName); } 复制代码
可以看到,生成后的cpp文件中,新增了一个__AtAutoreleasePool
结构体的变量
struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; }; 复制代码
根据这个结构体的定义,可以看出在初始化时,会调用objc_autoreleasePoolPush()
方法,在其析构函数,即该结构体实例销毁时,会调用objc_autoreleasePoolPop(atautoreleasepoolobj)
方法。
objc_autoreleasePoolPush
和objc_autoreleasePoolPop
的原理在上面
runloop
和@autorelesepool
的探究过程中,最后都会停留到这两个方法中,接下来,我们通过查看源码来探究下这两个方法具体做了哪些工作。(ps:可以在这里下载可编译的runtime
源码)
void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } NEVER_INLINE void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); } 复制代码
根据上面的代码,可以看到push
和pop
操作分别调用了AutoreleasePoolPage
的类方法。我们先看下AutoreleasePoolPage
的定义:
class AutoreleasePoolPage : private AutoreleasePoolPageData {...} struct AutoreleasePoolPageData { magic_t const magic; //检查完整性的校验 __unsafe_unretained id *next; pthread_t const thread; //当前线程 AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; }; 复制代码
这里比较值得关注的有:
a. parent
和child
变量构成双向链表
b. next
变量作为指向新添加autorelease
对象的下一个位置,用于以栈的形式存储
自动释放池数据结构如上所示:双链表+栈
了解完AutoreleasePoolPage
的结构后,我们来分别细看下push
和pop
操作
push
操作static inline void *push() { id *dest; if (slowpath(DebugPoolAllocation)) { //debug模式下会直接生成一个新的page // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; } #define POOL_BOUNDARY nil 复制代码
这里会根据是否为debug模式,来进行不同的处理,这里可以暂时忽略debug模式下的处理,即调用autoreleaseFast
方法,并传入一个nil
对象,最后返回dest
对象作为push
方法的返回值。
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); } } 复制代码
a. 首先它通过hotPage
方法获取到当前的page
,若page
存在且空间未满,则将obj添加到page中。
b. 若page存在但空间已经满了,则需要新建一个子page来存储obj
c. 若page不存在,则创建一个新page来存储obj
page
的获取和存储(这里的当前page指的是AutoreleasePoolPage
链表中当前所处于的节点page)//获取page 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; } //设置page static inline void setHotPage(AutoreleasePoolPage *page) { if (page) page->fastcheck(); tls_set_direct(key, (void *)page); } //AutoreleasePoolPage声明内 static pthread_key_t const key = AUTORELEASE_POOL_KEY; 复制代码
可以看到两者分别调用tls_get_direct
和tls_set_direct
方法对page分别进行读取和存储。
static inline void *tls_get_direct(tls_key_t k) { ASSERT(is_valid_direct_key(k)); if (_pthread_has_direct_tsd()) { return _pthread_getspecific_direct(k); } else { return pthread_getspecific(k); } } static inline void tls_set_direct(tls_key_t k, void *value) { ASSERT(is_valid_direct_key(k)); if (_pthread_has_direct_tsd()) { _pthread_setspecific_direct(k, value); } else { pthread_setspecific(k, value); } } 复制代码
这里使用了TLS(Thread Local Storage)
线程局部变量进行存储,也就是说使用当前线程的局部存储空间对page进行存储,这样实现了线程和自动释放池的关联,不同线程的自动释放池也是独立的,互不干扰。
static __attribute__((noinline)) id *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. ASSERT(page == hotPage()); ASSERT(page->full() || DebugPoolAllocation); do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); } 复制代码
如上,若当前page空间不足,则不断往后遍历,直到找到有空间的page,若找到最后也没有,则创建一个子page,并更新当前page节点,以便下一次可以直接添加(而不需要遍历查找)
static __attribute__((noinline)) .... // Install the first page. AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); // Push a boundary on behalf of the previously-placeholder'd pool. if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } // Push the requested object or pool. return page->add(obj); } 复制代码
如上,page不存在的情况,会创建一个新page(作为链表的头部节点),并更新到TLS中。
add
操作:不管上面哪种情况,最后都会调用add
方法将对象添加到对应的page
中id *add(id obj) { ASSERT(!full()); unprotect(); id *ret = next; // faster than `return next-1` because of aliasing *next++ = obj; protect(); return ret; } 复制代码
上面提到过*next
为新添加对象的位置,所以这里将*next
的赋值为当前对象,并移动到下一个位置。
autoreleaseFast
方法的调用a. AutoreleasePoolPage:push
方法,传入POOL_BOUNDARY(nil)
对象
当调用push方法时,都会传入一个nil对象,作为“哨兵对象”,以便标识每次
push
和pop
之间添加的对象区间,这样当执行pop
操作时,就能准确释放对应的对象(直到“哨兵”位置)。
如上,当进行pop操作时,会将obj2-5的对象进行释放。
b. AutoreleasePoolPage:autorelease
方法,传入实际的obj对象
static inline id autorelease(id obj) { ASSERT(obj); ASSERT(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); return obj; } 复制代码
在ARC下,编译器会在适当的位置插入autorelease
方法。因此,会将对象自动添加到自动释放池中。
pop
操作static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; if (token == (void*)EMPTY_POOL_PLACEHOLDER) { // Popping the top-level placeholder pool. page = hotPage(); if (!page) { // Pool was never used. Clear the placeholder. return setHotPage(nil); } // Pool was used. Pop its contents normally. // Pool pages remain allocated for re-use as usual. page = coldPage(); token = page->begin(); } else { 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 (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) { return popPageDebug(token, page, stop); } return popPage<false>(token, page, stop); } 复制代码
这里传入的参数token
为上面push
操作返回的,即push
操作后,返回的"哨兵"对象的指针。
EMPTY_POOL_PLACEHOLDER
是对只有1个pool情况下的优化,可以先不考虑该细节。
通过pageForPointer
方法获取当前到page
if (*stop != POOL_BOUNDARY)
,根据上面的第一点,可以知道,token
应该为p
操作完后,返回的“哨兵”对象,若不是,则进行异常处理。
page
static AutoreleasePoolPage *pageForPointer(const void *p) { return pageForPointer((uintptr_t)p); } 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; } 复制代码
因为每一个page
的大小是固定的,所以可以通过p % SIZE
的方法获取到偏移量,然后通过p - offset
获取到page的起始地址。
template<bool allowDebug> static void popPage(void *token, AutoreleasePoolPage *page, id *stop) { if (allowDebug && PrintPoolHiwat) printHiwat(); page->releaseUntil(stop); // memory: delete empty children if (allowDebug && DebugPoolAllocation && page->empty()) { // special case: delete everything during page-per-pool debugging AutoreleasePoolPage *parent = page->parent; page->kill(); setHotPage(parent); } else if (allowDebug && DebugMissingPools && page->empty() && !page->parent) { // special case: delete everything for pop(top) // when debugging missing autorelease pools page->kill(); setHotPage(nil); } else 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(); } } } 复制代码
这里主要通过releaseUntil
方法进行释放对象,释放后,会根据page的空间进行调整,前两个if判断都是debug模式下,可以先不用管,最后一个else if其实就是对剩余的空闲空间进行回收。
void releaseUntil(id *stop) { // Not recursive: we don't want to blow out the stack // if a thread accumulates a stupendous amount of garbage while (this->next != stop) { // Restart from hotPage() every time, in case -release // autoreleased more objects AutoreleasePoolPage *page = hotPage(); // fixme I think this `while` can be `if`, but I can't prove it 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 } 复制代码
这里用while
循环从当前page
的不断遍历,直到next
指向了stop
。
SCRIBBLE
,next
指针往前移。具体流程如下图所示:
autoreleasepool
与NSThread
的关系两者的关联主要涉及的有两个点:
a.
autoreleasepool
依赖于当前线程的TLS
,这个上面也分析过了;b.
autoreleasepool
在不同线程中的创建和释放,这里主要探讨这个问题
@autoreleasepool
创建了自动释放池,所以我们无需额外去创建和释放了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
方法进行创建和释放呢?在ARC中,我们知道编译器会在合适的位置自动插入
autorelease
方法,而我们上面分析push
操作的时候提到过autoreleaseFast
方法也会在autorelease
方法的时候调用。因此,不管我们有没手动创建自动释放池,它都会添加到autoreleasepool
中。
NSObject *obj = [[NSObject alloc] init]; //编译后: NSObject *obj = [[NSObject alloc] init]; [obj autorelease]; static inline id autorelease(id obj) { ASSERT(obj); ASSERT(!obj->isTaggedPointer()); id *dest __unused = autoreleaseFast(obj); ASSERT(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj); return obj; } 复制代码
自动释放池的创建清楚了,再来看看它的释放操作。我们知道主线程中的@autoreleasepool
会通过objc_autoreleasePoolPop
方法进行释放。而在子线程中并没有调用这样的方法,那又要如何进行释放呢?我们先看个例子:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view. self.view.backgroundColor = [UIColor whiteColor]; NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil]; [thread start]; } - (void)threadRun { Person *p = [[Person alloc] initWithName:@"jam" age:24 date:[NSDate date]]; self.person = p; //此处打断点 NSLog(@"run in %@", [NSThread currentThread]); } 复制代码
在self.person = p
的位置打断点,然后设置观察对象watchpoint set variable p
,再不断执行,直到线程执行完,找到对应线程的断点,可以看到:
点进去看,可以看到起调用过程:
这里有个_pthread_tsd_cleanup
函数的调用
void _pthread_tsd_cleanup(pthread_t self) { int i, j; void *param; for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) { for (i = 0; i < _POSIX_THREAD_KEYS_MAX; i++) { if (_pthread_keys[i].created && (param = self->tsd[i])) { self->tsd[i] = (void *)NULL; if (_pthread_keys[i].destructor) { (_pthread_keys[i].destructor)(param); } } } } } 复制代码
很明显,该函数会对当前线程的TLS的资源进行清除,遍历所有pthread_key_t
,调用其析构函数。我们知道autoreleasepool
在线程中有对应的pthread_key_t
static pthread_key_t const key = AUTORELEASE_POOL_KEY; static void init() { int r __unused = pthread_key_init_np(AutoreleasePoolPage::key, AutoreleasePoolPage::tls_dealloc); ASSERT(r == 0); } static void tls_dealloc(void *p) { if (p == (void*)EMPTY_POOL_PLACEHOLDER) { // No objects or pool pages to clean up here. return; } // reinstate TLS value while we work setHotPage((AutoreleasePoolPage *)p); if (AutoreleasePoolPage *page = coldPage()) { if (!page->empty()) objc_autoreleasePoolPop(page->begin()); // pop all of the pools if (slowpath(DebugMissingPools || DebugPoolAllocation)) { // pop() killed the pages already } else { page->kill(); // free all of the pages } } // clear TLS value so TLS destruction doesn't loop setHotPage(nil); } 复制代码
因此,子线程中自动释放池的创建和释放都无需我们进行额外的操作。当然,在某些场景下,也可以手动通过@autoreleasepool
进行创建和释放。
autoreleasepool
与enumerateObjectsUsingBlock
enumerateObjectsUsingBlock
方法会自动在内部添加一个@autoreleasepool
,以保证下一次迭代前清除临时对象,从而降低内存峰值。
int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); NSArray *arr = @[@"str1", @"str2", @"str3"]; [arr enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { id o = obj; //此处设置断点 NSLog(@"obj: %@", o); }]; } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } 复制代码
我们通过在id o = obj
位置设置断点,然后添加观察变量watchpoint set variable o
,再运行程序,会发现每次迭代结束后,都会调用自动释放池的releaseUnitl
方法:
Using Autorelease Pool
NSAutoReleasePool
autoreleasepool and nsthread
黑幕背后的Autorelease