带有问题看原始文件----子线程AutoRelease对象何时释放

首先是一个常规问题,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

黑幕背后的自动释放堆栈溢出

流程

你可能感兴趣的:(带有问题看原始文件----子线程AutoRelease对象何时释放)