首先是一个常规问题,autorelease对象何时释放?答:在AutoreleasePoolPage pop的时候释放,在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool,详情可以看YY的深入理解runloop。那么子线程呢?子线程的runloop都需要手动开启,那么子线程中使用autorelease对象会内存泄漏吗,如果不会又是什么时候释放呢。
Runloop原始码
带着这个问题,我们看一看runloop的原始码中答案的答案。
autoreleasepool
在MRC下,使用__autoreleasing修饰符等同于MRC下调用自动发布方法,所以在NSObject子系统中找到-(id)autorelese方法开始看。
1
2
3
4
-(id)autorelease
{
return _objc_rootAutorelease(self);
}
可以看到这个方法里只是简单的调了一下_objc_rootAutorelease(),继续跟进。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
id
_objc_rootAutorelease(id obj)
{
assert(obj);
返回obj-> rootAutorelease();
}
...
//基本自动发布实现,忽略替代。
内联ID
objc_object :: rootAutorelease()
{
如果(isTaggedPointer())返回(id)
如果(prepareOptimizedReturn(ReturnAtPlus1))返回(id)
返回rootAutorelease2();
}
...
__attribute __((noinline,used))
id
objc_object :: rootAutorelease2()
{
assert(!isTaggedPointer());
返回AutoreleasePoolPage :: autorelease((id)this);
}
检查是否AutoreleasePoolPage :: autorelease是标签指针和是否要做不加入autoreleasepool的优化,然后rootAutorelease2()。最后走入了AutoreleasePoolPage::autorelease()。
接下来看看AutoreleasePoolPage这个类,有关这个类的说明,可以看看sunny的黑幕背后的Autorelease。现在来看看AutoreleasePoolPage中的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public:
静态内联ID autorelease(id obj)
{
assert(obj);
assert(!obj-> isTaggedPointer());
id * dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || * dest == obj);
返回obj;
}
...
静态内联ID * autoreleaseFast(id obj)
{
AutoreleasePoolPage * page = hotPage();
if(page &&!page-> full()){
返回页面-> add(obj);
} else if(page){
返回autoreleaseFullPage(obj,page);
} else {
return autoreleaseNoPage(obj);
}
}
...
静态__attribute __((noinline))
id * autoreleaseNoPage(id obj)
{
//“无页面”可能意味着没有推送池
//或推送了一个空的占位符池且尚无内容
assert(!hotPage ());
bool pushExtraBoundary = false;
if(haveEmptyPoolPlaceholder()){
//我们将第二个池推入空的占位符池
//或将第一个对象推入空的占位符池。
//在此之前,代表
当前由空占位符表示的池// 推入池边界。
pushExtraBoundary = true;
}
否则if(obj!= POOL_BOUNDARY && DebugMissingPools){
//我们正在推送一个没有池的对象,
//环境请求了无池调试。
_objc_inform(“ MISSING POOLS:(%p)类%s的对象%p”
“自动释放,没有池-”
“只是泄漏-中断”
“要调试的objc_autoreleaseNoPool()”,
pthread_self(),(void *)obj,object_getClassName(obj));
objc_autoreleaseNoPool(obj);
返回零;
}
否则,如果(obj == POOL_BOUNDARY &&!DebugPoolAllocation){
//我们在没有池的情况下推送一个池,
//并且不请求每个池分配调试。
//安装并返回空池占位符。
返回setEmptyPoolPlaceholder();
}
//我们正在推送对象或非占位符的池。
//安装首页。
AutoreleasePoolPage * page =新的AutoreleasePoolPage(nil);
setHotPage(page); //代表先前占位符的池推边界。 如果(pushExtraBoundary){ page-> add(POOL_BOUNDARY); } //推送请求的对象或池。 返回页面->添加(obj); }
这里我们找到了我们想看的代码,如果当前线程没有AutorelesepoolPage的话,代码执行顺序为autorelease-> autoreleaseFast-> autoreleaseNoPage。
在autoreleaseNoPage方法中,会创建一个hotPage,然后调用page-> add(obj)。也就是说甚至这个线程没有AutorelesepoolPage,使用了autorelease对象时也会new一个AutoreleasepoolPage出来管理autorelese对象,不用担心内存泄漏。
何时释放
明确了何时创建autoreleasepool之后就自然而然的有下一个问题,这个autoreleasepool何时清空?
对于这个问题,这里使用watchpoint set variable命令来观察。
首先是一个最简单的场景,创建一个子线程。
1
2
3
4
__weak id obj;
...
[NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondaryThread)toTarget:self withObject:nil];
使用一个弱指针观察子线程中的自动释放对象,子线程中执行的任务。
1
2
3
4
5
6
7
-(void)createAndConfigObserverInSecondaryThread {
__autoreleasing id test = [NSObject new];
NSLog(@“ obj =%@”,测试);
obj =测试;
[[NSThread currentThread] setName:@“ test runloop thread”];
NSLog(@“线程结束”);
}
在该obj =测试位置设置断点使用watchpoint set variable obj命令观察obj,可以看到obj在释放时的方法调用栈是这样的。通过这个调用栈可以看到释放的时机在_pthread_exit。这个方法如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
静态无效tls_dealloc(void * p)
{
if(p ==(void *)EMPTY_POOL_PLACEHOLDER){
//这里没有要清理的对象或池页面。
返回;
}
//恢复工作时的TLS值
setHotPage((AutoreleasePoolPage *)p);
如果(AutoreleasePoolPage * page = coldPage()){
if(!page-> empty())pop(page-> begin()); //
如果(DebugMissingPools || DebugPoolAllocation){
// pop()已杀死所有页面,则弹出所有池
} else {
page-> kill(); //释放所有页面
}
} //清除TLS值,以便TLS销毁不会循环 setHotPage(nil); }
在这找到了if (!page->empty()) pop(page->begin());这句关键代码。再往上看一点,在_pthread_exit时会执行下面这个函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void
_pthread_tsd_cleanup(pthread_t self)
{
#if!VARIANT_DYLD
int j;
//首先
为(j = 0; j pthread_key_t k; for(k = __pthread_tsd_start; k <= self-> max_tsd_key; k ++){_ pthread_tsd_cleanup_key(self,k); } } self-> max_tsd_key = 0; //清理 (j = 0; j pthread_key_t k; for(k = __pthread_tsd_first; k <= __pthread_tsd_max ; k ++){_ pthread_tsd_cleanup_key(self,k); } } #endif //!VARIANT_DYLD } 该线程在退出时会释放自身资源,这个操作就包含了销毁自动释放池,在tls_delloc中,执行了pop操作。 这个实验本该到此就结束了,对于文章开始的问题在这里也已经有了答案,线程在销毁时会清空autoreleasepool。但是上述这个示例中的线程并没有加入runloop,只是一个一次性的线程。现在给这个线程加入runloop来看看效果会是怎么样的。 运行循环源和自动释放池 对于runloop,我们知道runloop一定要有源能力保证run起来以后不立即结束,而source有三种,自定义源,端口源,计时器。 先加一个计时器试试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 -(void)createAndConfigObserverInSecondaryThread { [[NSThread currentThread] setName:@“ test runloop thread”]; NSRunLoop * loop = [NSRunLoop currentRunLoop]; CFRunLoopObserverRef观察器; 观察者= CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, true,//重复 0xFFFFFF,// CATransaction(2000000)之后 YYRunLoopObserverCallBack,NULL); CFRunLoopRef cfrunloop = [循环getCFRunLoop]; 如果(观察者){ CFRunLoopAddObserver(cfrunloop,观察者,kCFRunLoopCommonModes); CFRelease(观察者); } [NSTimer scheduleTimerWithTimeInterval:5目标:自我选择器:@selector(testAction)userInfo:无重复:是]; [循环运行]; NSLog(@“线程结束”); } -(void)testAction { __autoreleasing id test = [NSObject new]; obj =测试; NSLog(@“ obj =%@”,obj); } 这里的oberserver没有什么,就是从YYKit里复制出来的一段观察者代码,用于监视runloop的状态。发现的可以看看。 在testAction()中加上watchpoint断点,观察obj的释放时机。可以看到释放的时机在CFRunloopRunSpecific中,也就是runloop切换状态的时候,继续往上看发现这个替代。这个函数中的实现如下 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() 1 2 3 4 5 6 静态void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION __(CFRunLoopTimerCallBack func,CFRunLoopTimerRef timer,void * info){ if(func){ func(timer,info); } asm __volatile __(“”); //阻止尾部调用优化 } 这个所谓func的callback是timer的一个属性,根据这个调用栈看到,释放autoreleasepool的操作应该是在这个callback中。这里猜测一下timer,应该是在自己的回调函数里插入了释放autorelesepool的代码。 然后用自己实现的source试一试, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 -(void)createAndConfigObserverInSecondaryThread { __autoreleasing id test = [NSObject new]; NSLog(@“ obj =%@”,测试); obj =测试; [[NSThread currentThread] setName:@“ test runloop thread”]; NSRunLoop * loop = [NSRunLoop currentRunLoop]; CFRunLoopObserverRef观察器; 观察者= CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, true,//重复 0xFFFFFF,// CATransaction(2000000)之后 YYRunLoopObserverCallBack,NULL); CFRunLoopRef cfrunloop = [循环getCFRunLoop]; 如果(观察者){ CFRunLoopAddObserver(cfrunloop,观察者,kCFRunLoopCommonModes); CFRelease(观察者); } CFRunLoopSourceRef源; CFRunLoopSourceContext sourceContext = {0,(__bridge void *)(self),NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,&runLoopSourcePerformRoutine}; 源= CFRunLoopSourceCreate(NULL,0,&sourceContext); CFRunLoopAddSource(cfrunloop,source,kCFRunLoopDefaultMode); runLoopSource =源; runLoop = cfrunloop; [循环运行]; NSLog(@“线程结束”); } -(void)wakeupSource { //通知输入源 CFRunLoopSourceSignal(runLoopSource); //唤醒runLoop CFRunLoopWakeUp(runLoop); } ... void runLoopSourcePerformRoutine(void * info) { __autoreleasing id test = [NSObject new]; obj =测试; // NSLog(@“ obj is%@”,obj); // //如果不对obj赋值,obj会一直持有createAndConfigObserverInSecondaryThread函数入口的那个对象,那个对象不在这里面的自动释放池影响。 NSLog(@“”方法%@“,[NSThread currentThread]); } 这里wakeupSource()是一个按钮的点击事件,用于唤醒runloop。runloop唤醒之后将执行runLoopSourcePerformRoutine函数,在runLoopSourcePerformRoutine()中观察对象的释放时机,发现是在[NSRunloop run:beforeDate:]中,查看GNU的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 -(BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)date { NSAutoreleasePool * arp = [NSAutoreleasePool new]; NSString * savedMode = _currentMode; GSRunLoopCtxt *上下文; NSDate * d; NSAssert(mode!= nil,NSInvalidArgumentException); / *处理所有待处理的通知。 * / GSPrivateNotifyASAP(mode); / *并处理循环中安排的所有执行者(例如,来自 另一个线程的东西。 * / _currentMode =模式; context = NSMapGet(_contextMap,mode); [self _checkPerformers:context]; _currentMode = savedMode; / *找出我们可以在第一个限制日期之前等待多长时间。 *如果没有输入源或计时器,请立即返回。 * / d = [self limitDateForMode:模式]; if(nil == d) { [树桩排水]; 返回否; } / *使用我们拥有的两个日期中的较早日期(零日期就像遥远的过去)。 * / if(nil == date) { [self acceptInputForMode:模式beforeDate:nil]; } else { / *保留日期,以防计时器(或其他事件)触发 *。 * / d = [[d较早的日期:日期]副本]; [self acceptInputForMode:模式beforeDate:d]; 发布(d); } [排泄口]; 返回是; } 在GNU的实现中,targer执行相应的动作操作是在[self acceptInputForMode: mode beforeDate: d];中,可以看到在runMode: (NSString*)mode beforeDate: (NSDate*)date方法中,其实是包裹了一个autoreleasepool的,也就是arp,如果在深入一些函数里面,发现实际上很多地方都有autoreleasepool的函数,因此即使是我们自定义的源,执行函数中没有释放autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。 文章到此就告一段落了,还有一种端口源,也就是source1,这种source我没有去看,好奇的同学可以去看一看如果有什么不对我们一起讨论。 总结 1. 在runloop的运行中:beforeDate,以及一些源的回调中,有autoreleasepool的push和pop操作,总结就是系统在1.子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。很多地方都差不多自动释放的管理操作。 3.就算插入没有弹出也没关系,在线程退出的时候会释放资源,执行AutoreleasePoolPage :: tls_dealloc,在这里面会清空autoreleasepool。 ##参考 深入了解runloop 黑幕背后的自动释放堆栈溢出 流程