首先是一个常规问题,autorelease对象何时释放?答:在AutoreleasePoolPage pop的时候释放,在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool,详情可以看YY的深入理解runloop。那么子线程呢?子线程的runloop都需要手动开启,那么子线程中使用autorelease对象会内存泄漏吗,如果不会又是什么时候释放呢。
Runloop源码
带着这个问题,我们看一看runloop的源码中给出的答案。
autoreleasepool创建
在MRC下,使用__autoreleasing修饰符等同于MRC下调用autorelease方法,所以在NSObject源码中找到-(id)autorelese方法开始看。
1 | -(id) autorelease |
可以看到这个方法里只是简单的调了一下_objc_rootAutorelease()
,继续跟进。
1 | id |
检查是否AutoreleasePoolPage::autorelease是标签指针和是否要做不加入autoreleasepool的优化,然后rootAutorelease2()
。最后走入了AutoreleasePoolPage::autorelease()
。
接下来看看AutoreleasePoolPage
这个类,有关这个类的说明,可以看看sunny的黑幕背后的Autorelease。现在来看看AutoreleasePoolPage中的实现。
1 | public: |
这里我们找到了我们想看的代码,如果当前线程没有AutorelesepoolPage的话,代码执行顺序为autorelease -> autoreleaseFast -> autoreleaseNoPage。
在autoreleaseNoPage方法中,会创建一个hotPage,然后调用page->add(obj)。也就是说即使这个线程没有AutorelesepoolPage,使用了autorelease对象时也会new一个AutoreleasepoolPage出来管理autorelese对象,不用担心内存泄漏。
何时释放
明确了何时创建autoreleasepool以后就自然而然的有下一个问题,这个autoreleasepool何时清空?
对于这个问题,这里使用watchpoint set variable
命令来观察。
首先是一个最简单的场景,创建一个子线程。
1 | __weak id obj; |
使用一个weak指针观察子线程中的autorelease对象,子线程中执行的任务。
1 | - (void)createAndConfigObserverInSecondaryThread{ |
在obj = test处设置断点使用watchpoint set variable obj
命令观察obj,可以看到obj在释放时的方法调用栈是这样的。
通过这个调用栈可以看到释放的时机在_pthread_exit。然后执行到AutorelepoolPage的tls_dealloc方法。这个方法如下
1 | static void tls_dealloc(void *p) |
在这找到了if (!page->empty()) pop(page->begin());
这句关键代码。再往上看一点,在_pthread_exit时会执行下面这个函数
1 | void |
也就是说thread在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool,在tls_delloc中,执行了pop操作。
这个实验本该到此就结束了,对于文章开始的问题在这里也已经有了答案,线程在销毁时会清空autoreleasepool。但是上述这个例子中的线程并没有加入runloop,只是一个一次性的线程。现在给这个线程加入runloop来看看效果会是怎么样的。
runloop source & autoreleasepool
对于runloop,我们知道runloop一定要有source才能保证run起来以后不立即结束,而source有三种,custom source,port source,timer。
先加一个timer试试
1 | - (void)createAndConfigObserverInSecondaryThread{ |
这里的oberserver没有什么,就是从YYKit里复制出来的一段observer代码,用于监控runloop的状态。感兴趣的可以看看。
在testAction()
中加上watchpoint断点,观察obj的释放时机。
可以看到释放的时机在CFRunloopRunSpecific中,也就是runloop切换状态的时候,继续往上看发现__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__()
这个回调。这个函数中的实现如下
1 | static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) { |
这个名为func
的callback是timer的一个属性,根据这个调用栈看到,释放autoreleasepool的操作应该是在这个callback中。这里猜测一下timer,应该是在自己的callback函数里插入了释放autorelesepool的代码。
然后用自己实现的source试一试,
1 | - (void)createAndConfigObserverInSecondaryThread{ |
这里wakeupSource()
是一个按钮的点击事件,用于唤醒runloop。runloop唤醒之后将执行runLoopSourcePerformRoutine
函数,在runLoopSourcePerformRoutine()
中观察obj的释放时机,发现是在[NSRunloop run:beforeDate:]中,查看GNU的实现
1 | - (BOOL) runMode: (NSString*)mode beforeDate: (NSDate*)date |
在GNU的实现中,targer执行相应的action操作是在[self acceptInputForMode: mode beforeDate: d];
中,可以看到在runMode: (NSString*)mode beforeDate: (NSDate*)date
方法中,其实是包裹了一个autoreleasepool的,也就是arp
,如果在深入一些函数里面,发现其实很多地方都有autoreleasepool的函数,所以即使是我们自定义的source,执行函数中没有释放autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。
文章到此就告一段落了,还有一种port source,也就是source1,这种source我没有去看,好奇的同学可以去看一看如果有什么不对我们一起讨论。
总结
1.子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。
2.在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统在很多地方都差不多autorelease的管理操作。
3.就算插入没有pop也没关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里面会清空autoreleasepool。