美文网首页
带有问题看原始文件----子线程AutoRelease对象何时释

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

作者: halobear | 来源:发表于2020-02-29 11:26 被阅读0次

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

    ##参考

    深入了解runloop

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

    流程

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/puwuhhtx.html