[关闭]
@qidiandasheng 2020-07-22T13:39:51.000000Z 字数 5589 阅读 1912

内存管理Autorelease

iOS理论


介绍

MRC中,调用[obj autorelease]来延迟内存的释放。

ARC下就是系统自动给你加了autorelease和自动释放池,来延迟内存的释放。

为什么要延迟内存的释放呢?我觉得是有弱引用的问题,弱引用不持有对象。如果对象一没强引用持有马上释放了,那弱引用再去调用对象的时候就会出问题。

Autorelease对象什么时候释放?

子线程AutoRelease对象何时释放这篇文章根据源码介绍了子线程中AutoRelease对象的释放

示例

我们来看下面一段代码:

  1. - (void)viewDidLoad{
  2. [super viewDidLoad];
  3. NSString *string = [NSString stringWithFormat:@"齐滇大圣"];
  4. }

这里[NSString stringWithFormat:@"齐滇大圣"];创建对象时这个对象的引用计数为 1 。当使用局部变量 string 指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。而当 viewDidLoad 方法返回时,局部变量 string 被回收,指向了 nil 。因此,其所指向对象的引用计数 -1 ,变成了 1 。

那么问题来了还有一个引用计数怎么办呢?其实我们的这个对象被系统自动添加到了当前的 autoreleasepool 中,当autoreleasepool drain的时候引用计数就会-1,对象就被释放了。那么aotoreleasepool 什么时候drain呢?答案是在runloop一次迭代结束的时候。

runloop什么时候迭代结束呢?来现在跟我一起看图说话:

这里我们加入一个__weak的全局变量reference来指向我们的对象。因为__weak引用不持有我们的对象,不会影响所指向对象的生命周期,所以我们用它来输出以判断我们的对象什么时候释放。

我们能看到referenceviewDidLoadviewWillAppear的时候有输出,而在viewDidAppear的时候为null,说明被释放了。那我们来猜测一下runloop的迭代周期。

viewWillAppear很容易理解是即将进入页面嘛,那runloop肯定是还有事要做的嘛,当viewDidAppear的时候表示已经进入页面了。那就表示没事做了,进入睡眠,等待用户动作的时候再次唤醒。你可能会觉得我口说无凭不靠谱,好那我就拿出证据来,我们来看下面两张图。

断点1和断点2

断点3

我们能看到断点1和断点2 runloop还是在执行的,断点3表示runloop一个迭代已经结束了,即将进入睡眠。这里如果对runloop不了解的话可以看ibireme的这篇深入理解RunLoop。我截取里面的一小段,大家可以看一下。

  1. //触发 Source0 (非基于port的) 回调。
  2. __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
  3. //out_to_block表示从block跳出,block执行完毕,即将进入睡眠。
  4. __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

注意:

上面图中的例子,有一段被注释的代码:

  1. //返回一个自己Alloc申请内存的NSString实例
  2. NSString *string = [[NSString alloc] initWithFormat:@"齐滇大圣"];

在MRC中alloc创建的实例是需要手动release,而在ARC中alloc创建的实例也没有放入AutoreleasePool。应该是系统在你创建完并有指针指向对象的时候引用计数马上就-1了。所以当viewDidLoad结束的时候,string被回收,引用计数减为0,对象被释放。我们来看一下输出:

  1. 2016-04-22 09:29:10.780 test[2001:743463] string: 齐滇大圣
  2. 2016-04-22 09:29:11.727 test[2001:743463] string: (null)
  3. 2016-04-22 09:29:12.576 test[2001:743463] string: (null)

更新:

以上测试基于iOS8和iOS9,在iOS10上输出都是有值的,可能iOS10上runloop的迭代周期已经改变了。

AutoreleasePool的本质

AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。

所以如果说AutoreleasePool的本质的话其实就是AutoreleasePoolPage这个C++类。

  1. class AutoreleasePoolPage
  2. {
  3. #define POOL_SENTINEL 0
  4. static size_t const SIZE =
  5. #if PROTECT_AUTORELEASEPOOL
  6. 4096; // must be multiple of vm page size
  7. #else
  8. 4096; // size and alignment, power of 2
  9. #endif
  10. magic_t const magic; //用于数据校验
  11. id *next; //栈顶地址
  12. pthread_t const thread; //所在的线程
  13. AutoreleasePoolPage * const parent; //父对象
  14. AutoreleasePoolPage *child; //子对象
  15. uint32_t const depth; //page的序号
  16. uint32_t hiwat;
  17. ...
  18. }

若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:

屏幕快照 2020-03-25 下午11.13.14.png-429.8kB

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置。

objc_autoreleasePoolPush

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:

屏幕快照 2020-03-25 下午11.19.02.png-488.2kB

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参。

objc_autoreleasePoolPop

  1. objc_autoreleasePoolPop根据传入的哨兵对象地址找到哨兵对象所处的page
  2. 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置。
  3. 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page(在一个page中,是从高地址向低地址清理)

objc_autoreleasePoolPop执行后,最终变成了下面的样子:

屏幕快照 2020-03-25 下午11.22.41.png-368.2kB

@autoreleasepool{}

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:

  1. void *context = objc_autoreleasePoolPush();
  2. // {}中的代码
  3. objc_autoreleasePoolPop(context);

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

什么时候使用AutoreleasePool

1.如果你编写的程序不是基于 UI 框架的,比如说命令行工具;

2.如果你编写的循环中创建了大量的临时对象;

在普通的for或for in循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool。

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool,所以使用block版本枚举器更加方便:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    //这里被一个局部@autoreleasepool包围着
}];

@AutoreleasePool可以大幅度降低程序的内存占用。
比如500000次循环,每次循环创建一个NSNumber实例和两个NSString实例。

没有用@autoreleasepool时的内存占用会一直增加,创建的临时对象只在当前runloop迭代结束时释放。

用了@autoreleasepool的话,每次循环结束时都把那些临时对象释放了,所以内存没有一直增加。

最后结束时内存占用还是一样的

这有个例子做了测试AutoReleasePoolTestExample

@autoreleasepool-内存的分配与释放这篇文章有介绍循环里使用@autoreleasepool的一些机制。

3.如果你创建了一个辅助线程。

4.长时间在后台运行的任务。

比如AFNetworking:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
     @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}

概念

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop。

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。

从上面的代码中我们可以看到,AFNetworking在等待请求时其实只有个一个Thread, 然后在这个Thread上启动一个runLoop监听 NSURLConnection 的 NSMachPort 类型源。

解释

NSThread currentThread和NSRunLoop currentRunLoop返回的实例都是autorelease的,所以为了尽快释放减少占用就用autoreleasepool。这种工厂、单例、静态方法返回的基本都是autorelease的。

但是如果是只创建一次的话,应该也没什么必要手动加@autoreleasepool 。因为他很快也会被系统的释放的。我猜想是不是因为这些实例是加在当前runloop上的,这个runloop是一直监听 NSURLConnection 的 NSMachPort 类型源。所以这个runloop没结束那些对象都没办法释放。

参考

AutoreleasePoolPage结构
Autorelease Pool
黑幕背后的Autorelease

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注