autoreleasepool
概念
autoreleasepool
本质是自动延迟对象的释放,即对象使用完以后,它不会当即释放,而是加入到释放池,等到某个合适的时刻,对释放池中的对象进行统一释放。html官方文档对主线程的自动释放池有这么一段描述:c++
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.git
ARC
与MRC
下autoreleasepool
的区别
MRC
下须要手动管理自动释放池的建立和释放,ARC
下只须要使用@autoreleasepool
将对应的代码包含起来便可。github
- (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
变量,而是会在一个合适的时间点。具体是在何时,下面会讲解到。objective-c
ps:x-code下对特定文件设置使用MRC的方式:-fno-objc-arc bash
autoreleasepool
与runloop
的关系在断点调试中,使用
po [NSRunLoop currentLoop]
数据结构
由上图可知:自动释放池在runloop
中注册了两个observer,分别都会以_wrapRunLoopWithAutoreleasePoolHandler
进行回调。不过两个observer中的activities
和order
有些不一样。app
a. 首先看activities
的区别:less
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
方法: