第一题
for i in 0....10000 {
let view = UIView()
}
问:请问这段代码有什么问题,应该如何解决?
在大量的循环中,每次生成的对象没有被及时释放,导致内存暴增。
解决办法: 添加autoreleasepool
for i in 0....10000 {
autoreleasepool {
let view = UIView()
}
}
在autoreleasepool的作用域结束时,对象就会及时被释放。
第二题
Autorelease对象什么时候释放?
很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。 在没有手动加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop
可以在主线程中添加observer去监控runloop的状态:
let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, { (observer, activity) in
switch activity {
case .entry:
print("进入RunLoop");
case .beforeTimers:
print("即将处理Timer事件");
case .beforeSources:
print("即将处理Source事件");
case .beforeWaiting:
print("即将休眠");
case .afterWaiting:
print("被唤醒");
case .exit:
print("退出RunLoop");
default:
break;
}
})
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, .commonModes)
当重复打印button的时候可以看到日志,只有当即将休眠时,才会清理内存,这是因为苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()
创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()
和objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
第三题
一个线程可以多少个Autorelease Pool?
可能很多同学会回答:无数个。
因为在我们写代码时候可能会有这么一个情况:
autoreleasepool {
//dosomething
autoreleasepool {
//dosomething
}
}
会使用到嵌套的autoreleasepool
,所以看上去数量是不确定的。
问题就在于Autorelease Pool的是什么?
其实是AutoreleasePoolPage的双向链表的数据结构。
AutoreleasePoolPage:
class AutoreleasePoolPage
{
magic_t const magic; // 当前类完整性的校验
id *next;
pthread_t const thread; // 当前类所处的线程
// 双向链表 父子指针
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
static size_t const SIZE = PAGE_MAX_SIZE; // 4096
}
每创建一个自动释放池,就会在当前线程的 poolPage 的栈中先添加一个边界对象,然后把池中的对象添加进去,直至栈满,创建子 page,继续添加。
所以其实线程与自动释放池是一一对应的。
第四题
main函数的@autoreleasepool
做了什么?
通过第三题我们可以知道线程与自动释放池是一一对应的,那么为什么在OC的main函数中要写这样的代码呢?
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
而且有不少朋友也做过实验,将这里的autoreleasepool代码注释,发现程序运行还是没差别。
这个答案我再最新的xcode中找到了,在xcode11中,创建的main函数发生了变化。
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
这里其实只是为了可能创建的autorelease对象写的。
第五题
已知在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool,那么在子线程中创建的对象,如果没有启动runloop,也没有声明autoreleasepool,怎么管理呢?
答:创建第一个 AutoreleasePoolPage 然后加入当前线程局部存储。
第六题
在第五题的前提下,已知已经有了autoreleasepool,但是没有runloop,子线程中的autoreleasepool什么时候会清空?
答: 线程在销毁时会清空autoreleasepool。
其他
在sunnyxx的介绍黑幕背后的Autorelease中,有这么一段代码:
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:@“sunnyxx”];
// str是一个autorelease对象,设置一个weak的引用来观察它
reference = str;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@“%@“, reference); // Console: sunnyxx
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@“%@“, reference); // Console: (null)
}
本意是想证明autorelease的释放时机与runloop有关,但现在已经过了5年,输出结果已经发生了改变。
- 在viewDidAppear或者是以后对reference的打印中,都会打印出sunnyxx,原因可能是因为tagger pointer做了优化。
- 如果这里不用NSString,而是用一个创建的NSObject,在viewWillAppear中已经为nil了,因为viewDidLoad与viewWillAppear已经不在同一个runloop中。
最后
这里的很多总结几乎只有答案没有原理与过程,希望大家看完之后另找资料深入理解,如果错误也请指出。