AutoreleasePool面试题总结

2,357 阅读4分钟

第一题

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年,输出结果已经发生了改变。

  1. 在viewDidAppear或者是以后对reference的打印中,都会打印出sunnyxx,原因可能是因为tagger pointer做了优化。
  2. 如果这里不用NSString,而是用一个创建的NSObject,在viewWillAppear中已经为nil了,因为viewDidLoad与viewWillAppear已经不在同一个runloop中。

最后

这里的很多总结几乎只有答案没有原理与过程,希望大家看完之后另找资料深入理解,如果错误也请指出。