首先是一个常规问题,autorelease对象何时释放?答:在AutoreleasePoolPage pop的时候释放,在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool,详情可以看YY的深入理解runloop。那么子线程呢?子线程的runloop都需要手动开启,那么子线程中使用autorelease对象会内存泄漏吗,如果不会又是什么时候释放呢。
带着这个问题,我们看一看runloop的原始码中答案的答案。
在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_DESTRUCTOR_ITERATIONS; 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_DESTRUCTOR_ITERATIONS; 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。
##参考
网友评论