@qidiandasheng
2020-07-22T13:39:51.000000Z
字数 5589
阅读 1934
iOS理论
MRC中,调用[obj autorelease]
来延迟内存的释放。
ARC下就是系统自动给你加了autorelease
和自动释放池,来延迟内存的释放。
为什么要延迟内存的释放呢?我觉得是有弱引用的问题,弱引用不持有对象。如果对象一没强引用持有马上释放了,那弱引用再去调用对象的时候就会出问题。
主线程:在没有手加Autorelease Pool
的情况下,Autorelease
对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
子线程:子线程在使用autorelease
对象时,如果没有autoreleasepool
会在autoreleaseNoPage
中懒加载一个出来。在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc
,在这里面会清空autoreleasepool
。
子线程加入runloop:在runloop的run:beforeDate
,以及一些source的callback中,有autoreleasepool
的push和pop操作,总结就是系统在很多地方都差不多有autorelease
的管理操作。
手动加Autorelease Pool
的话,Autorelease Pool
里的临时对象出了Autorelease Pool
的大括号时就释放了。
子线程AutoRelease对象何时释放这篇文章根据源码介绍了子线程中AutoRelease对象的释放
我们来看下面一段代码:
- (void)viewDidLoad{
[super viewDidLoad];
NSString *string = [NSString stringWithFormat:@"齐滇大圣"];
}
这里[NSString stringWithFormat:@"齐滇大圣"];
创建对象时这个对象的引用计数为 1 。当使用局部变量 string
指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。而当 viewDidLoad
方法返回时,局部变量 string
被回收,指向了 nil 。因此,其所指向对象的引用计数 -1 ,变成了 1 。
那么问题来了还有一个引用计数怎么办呢?其实我们的这个对象被系统自动添加到了当前的 autoreleasepool
中,当autoreleasepool drain
的时候引用计数就会-1,对象就被释放了。那么aotoreleasepool
什么时候drain
呢?答案是在runloop
一次迭代结束的时候。
那runloop
什么时候迭代结束呢?来现在跟我一起看图说话:
这里我们加入一个__weak
的全局变量reference
来指向我们的对象。因为__weak
引用不持有我们的对象,不会影响所指向对象的生命周期,所以我们用它来输出以判断我们的对象什么时候释放。
我们能看到reference
在viewDidLoad
和viewWillAppear
的时候有输出,而在viewDidAppear
的时候为null,说明被释放了。那我们来猜测一下runloop的迭代周期。
viewWillAppear
很容易理解是即将进入页面嘛,那runloop肯定是还有事要做的嘛,当viewDidAppear
的时候表示已经进入页面了。那就表示没事做了,进入睡眠,等待用户动作的时候再次唤醒。你可能会觉得我口说无凭不靠谱,好那我就拿出证据来,我们来看下面两张图。
断点1和断点2
断点3
我们能看到断点1和断点2 runloop还是在执行的,断点3表示runloop一个迭代已经结束了,即将进入睡眠。这里如果对runloop不了解的话可以看ibireme的这篇深入理解RunLoop。我截取里面的一小段,大家可以看一下。
//触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
//out_to_block表示从block跳出,block执行完毕,即将进入睡眠。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
注意:
上面图中的例子,有一段被注释的代码:
//返回一个自己Alloc申请内存的NSString实例
NSString *string = [[NSString alloc] initWithFormat:@"齐滇大圣"];
在MRC中alloc创建的实例是需要手动release,而在ARC中alloc创建的实例也没有放入AutoreleasePool
。应该是系统在你创建完并有指针指向对象的时候引用计数马上就-1了。所以当viewDidLoad
结束的时候,string
被回收,引用计数减为0,对象被释放。我们来看一下输出:
2016-04-22 09:29:10.780 test[2001:743463] string: 齐滇大圣
2016-04-22 09:29:11.727 test[2001:743463] string: (null)
2016-04-22 09:29:12.576 test[2001:743463] string: (null)
更新:
以上测试基于iOS8和iOS9,在iOS10上输出都是有值的,可能iOS10上runloop的迭代周期已经改变了。
AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)。
所以如果说AutoreleasePool的本质的话其实就是AutoreleasePoolPage这个C++类。
class AutoreleasePoolPage
{
#define POOL_SENTINEL 0
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
4096; // must be multiple of vm page size
#else
4096; // size and alignment, power of 2
#endif
magic_t const magic; //用于数据校验
id *next; //栈顶地址
pthread_t const thread; //所在的线程
AutoreleasePoolPage * const parent; //父对象
AutoreleasePoolPage *child; //子对象
uint32_t const depth; //page的序号
uint32_t hiwat;
...
}
若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:
图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。
所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置。
每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:
objc_autoreleasePoolPush
的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参。
objc_autoreleasePoolPop
根据传入的哨兵对象地址找到哨兵对象所处的pageobjc_autoreleasePoolPop执行后,最终变成了下面的样子:
ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:
void *context = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(context);
而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。
在普通的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的一些机制。
比如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没结束那些对象都没办法释放。