ios转载待读清单扩展眼界

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

2016-04-08  本文已影响2163人  game3108

前言

本文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?

上一篇下一篇

猜你喜欢

热点阅读