探索子线程autorelease对象的释放时机

前言

本文csdn地址:http://blog.csdn.net/game3108/article/details/51147949
今天在给同事讲autorelease对象释放时机:

1.手动添加的autoreleasepool,pool release或者drain的时候释放
MRC下

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init;
// xxxxx
[pool drain];

ARC下

@autoreleasepool {
}

2.runloop循环释放
runloop循环中创建新的autoreleasepool并在循环结束中释放

autorelease原理在黑幕背后的Autorelease有详细的介绍
runloop原理可以参考深入理解RunLoop

但一个子线程,默认是没有runloop的(第一次获得runloop对象时候创建),如果子线程上的autorelease对象何时释放?

猜测

  • 子线程执行完任务被释放

测试:一个网上很多很多的例子(自己也写了一遍)

//
//  ViewController.m
//  Test
//
//  Created by game3108 on 16/3/14.
//  Copyright © 2016年 game3108. All rights reserved.
//

#import "ViewController.h"
#import "TestObject.h"

@interface ViewController (){
    __weak id _testObject;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    TestObject *testObject = [TestObject instanceWithNumber:10];
    _testObject = testObject;
    
    UIButton *test = [[UIButton alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
    test.backgroundColor = [UIColor blackColor];
    [self.view addSubview:test];
    [test addTarget:self action:@selector(testButton) forControlEvents:UIControlEventTouchUpInside];
    // Do any additional setup after loading the view, typically from a nib.
}

- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear : %@", _testObject);
}

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear : %@", _testObject);
}

- (void)testButton{
    NSLog(@"testButton : %@", _testObject);
}
//
//  TestObject.h
//  Test
//
//  Created by game3108 on 16/4/5.
//  Copyright © 2016年 game3108. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface TestObject : NSObject

+ (instancetype) instanceWithNumber:(NSInteger)number;

@end
//
//  TestObject.m
//  Test
//
//  Created by game3108 on 16/4/5.
//  Copyright © 2016年 game3108. All rights reserved.
//

#import "TestObject.h"

@implementation TestObject{
    NSInteger _number;
}

+ (instancetype) instanceWithNumber:(NSInteger)number{
    //不加__autoreleasing 会因为arc返回值优化而不是一个autorelease对象,可以去掉试试
    __autoreleasing TestObject *object = [[TestObject alloc]init];
    object->_number = number;
    return object;
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"%d", self->_number];
}

-(void)dealloc{
    NSLog(@"Test Object dealloc");
}

@end

输出结果也和网上的结果差不多:

2016-04-05 17:10:02.718 Test[978:534893] viewWillAppear : 10
2016-04-05 17:10:59.730 Test[978:534893] Test Object dealloc
2016-04-05 17:10:59.738 Test[978:534893] viewDidAppear : (null)
2016-04-05 17:11:10.564 Test[978:534893] testButton : (null)

给dealloc加一个断点

dealloc断点图

嗯?怎么只有5行。。我当时看这个dealloc堆栈信息内心是有点崩溃的,好像系统调用都被隐藏了。也不知道那里去设置把他完整显示出来,最后捣鼓了半天,也只找到这个:(如果有人知道如何设置,请告诉我一下)

捣鼓出来的dealloc图

总算看到了,AutoreleasePoolPage:pop(void*)一应俱全,也验证了相关文章中的信息:
runloop循环中,呼叫了相应的observer,进行autoreleasepool的创建和释放。

测试一下子线程的autorelease对象释放:

//
//  ViewController.m
//  Test
//
//  Created by game3108 on 16/3/14.
//  Copyright © 2016年 game3108. All rights reserved.
//

#import "ViewController.h"
#import "TestObject.h"

@interface ViewController (){
    __weak id _testObject;
    NSThread *_testThread;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    _testThread = [[NSThread alloc] initWithTarget:self
                                                   selector:@selector(testBuild)
                                                     object:nil];
    [_testThread start];
    
    UIButton *test = [[UIButton alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
    test.backgroundColor = [UIColor blackColor];
    [self.view addSubview:test];
    [test addTarget:self action:@selector(testButton) forControlEvents:UIControlEventTouchUpInside];
    // Do any additional setup after loading the view, typically from a nib.
    
    [self performSelector:@selector(testAlert) onThread:_testThread withObject:nil waitUntilDone:NO];
}

- (void)testBuild{
    TestObject *testObject = [TestObject instanceWithNumber:10];
    _testObject = testObject;
}

- (void)testAlert{
    NSLog(@"test alert");
    NSLog(@"test alert");
    NSLog(@"test alert");
}

- (void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear : %@", _testObject);
}

- (void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear : %@", _testObject);
}

- (void)testButton{
    NSLog(@"testButton : %@", _testObject);
}

@end

代码完成,同样的dealloc断点看一下:

dealloc截图

可以看到,当pthread_exit退出时,触发了_pthread_tsd_cleanup,触发AutoreleasePoolPage的tls_dealloc(void*),然后回收autorelease对象

使用lldbwatchpoint命令,给_testObject = testObject;增加断电,并且在命令行中watchpoint set v _testObject看一下,结果也是和刚才的差不多:

watchpoint截图

那这边基本可以下一个结论:
每一个线程创建的时候就会有一个autorelease pool的创建,并且在线程退出的时候,清空整个autorelease pool。(ps:如果在子线程中设置一个循环,autorelease对象确实无法释放)

总结:

每个线程创建的时候就会创建一个autorelease pool,并且在线程退出的时候,清空autorelease pool。所以子线程的autorelease对象,要么在子线程中设置runloop清楚


我下载了runtime的objc4-680.tar.gz,和libthread的libpthread-138.10.4.tar.gz,试图去理解一下整个过程(原谅我的渣C水平,要买几本书好好学习一下了。):

NSObject.mm中class AutoreleasePoolPage的代码
tls_dealloc

void kill() 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;
        } while (deathptr != this);
    }

static void tls_dealloc(void *p) 
    {
        // reinstate TLS value while we work
        setHotPage((AutoreleasePoolPage *)p);

        if (AutoreleasePoolPage *page = coldPage()) {
            if (!page->empty()) pop(page->begin());  // pop all of the pools
            if (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);
    }

简单来说就是清空所有page
不停网上找调用点,会找到

NSObject.mm:

static void init()
    {
        int r __unused = pthread_key_init_np(AutoreleasePoolPage::key, 
                                             AutoreleasePoolPage::tls_dealloc);
        assert(r == 0);
    }

就会调用到pthread的方法,简单来讲就是会构造一个

static struct {
 uintptr_t destructor;
} _pthread_keys[_INTERNAL_POSIX_THREAD_KEYS_END];

将destructor方法保存,并且在thread退出的相应的时机调用。因为C水平有限,也只能懂个大概,也就不详细展开了。

2017.2.13更新

我从stackoverflow does NSThread create autoreleasepool automaticly now?看到了一个更不错的答案,翻译一下,写在这里。

这没有被存档,但这个答案显然是:是的。在OS X 10.9+余iOS 7+上。

Objective-c的runtime对你来说是开源的,所以你可以读源代码看如何运行。最新版本的runtime(646,用在OS X 10.10和IOS 8)确实添加了一个pool如果你仔没有pool的情况下运行autorelease。

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    // No pool in place.
    assert(!hotPage());

    if (obj != POOL_SENTINEL  &&  DebugMissingPools) {
        // We are pushing an object with no pool in place, 
        // and no-pool debugging was requested by environment.
        _objc_inform("MISSING POOLS: Object %p of class %s "
                     "autoreleased with no pool in place - "
                     "just leaking - break on "
                     "objc_autoreleaseNoPool() to debug", 
                     (void*)obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return nil;
    }

    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    // Push an autorelease pool boundary if it wasn't already requested.
    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    // Push the requested object.
    return page->add(obj);
}

这个函数的调用时在你第一次压入pool(事实上push的是POOL_SENTIAL),或者你没有pool下autorelease。当第一个pool被压入,它建立autorelease的栈。但当你从code来看,因为DebugMissingPools的环境变量没有设置(不会被设为默认)。当autolrease没有pool下结束了,它同样会建立autorelease栈,然后压入一个pool(压入一个POOL_SENTINEL)

同样的,(如果没有看其他代码,有点难理解,但这个是相关部分)。当thread被销毁(同样线程本地的存储也被销毁),它释放了每一个在autorelease栈中的对象(使用pop(0);)因此它不建立在用户去弹出最后一个pool。

static void tls_dealloc(void *p) 
{
    // reinstate TLS value while we work
    setHotPage((AutoreleasePoolPage *)p);
    pop(0);
    setHotPage(nil);
}

这个当前版本的runtime(551.1,来自于OS X 10.9与iOS 7)也同样做了这个,所以你可以看NSObject.mm

static __attribute__((noinline))
id *autoreleaseSlow(id obj)
{
    AutoreleasePoolPage *page;
    page = hotPage();

    // The code below assumes some cases are handled by autoreleaseFast()
    assert(!page || page->full());

    if (!page) {
        // No pool. Silently push one.
        assert(obj != POOL_SENTINEL);

        if (DebugMissingPools) {
            _objc_inform("MISSING POOLS: Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }

        push();
        page = hotPage();
    }

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

但之前的版本(532.2,来自于OS X 10.8与 iOS 6)没有做。

static __attribute__((noinline))
id *autoreleaseSlow(id obj)
{
    AutoreleasePoolPage *page;
    page = hotPage();

    // The code below assumes some cases are handled by autoreleaseFast()
    assert(!page || page->full());

    if (!page) {
        assert(obj != POOL_SENTINEL);
        _objc_inform("Object %p of class %s autoreleased "
                     "with no pool in place - just leaking - "
                     "break on objc_autoreleaseNoPool() to debug", 
                     obj, object_getClassName(obj));
        objc_autoreleaseNoPool(obj);
        return NULL;
    }

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

注意上面的代码为任何pthread执行,不仅仅是NSThread

基本上,如果你运行在OS X 10.9+或者iOS 7+,不用pool去autorelease在一个线程上不回引起内存泄漏。这个没有被文档记录并且是一个内部的实现细节,因此小心依赖这个因为Apple可能会在之后的系统修改它。然而,我看不到任何他们会移除这个功能的理由,因为这个简单,并且只有益处而没有坏处,除非他们重写autorelease pool的工作或者其他内容。

参考资料

1.黑幕背后的Autorelease
2.autorelease使用注意事项
3.深入理解RunLoop
4.Objective-C Autorelease Pool 的实现原理
5.runtime源代码
6.libpthread
7.does NSThread create autoreleasepool automaticly now?

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容