iOS Runloop 精品博客分享总结

100 阅读5分钟

一段常见的问题

看这段代码

for (int i = 0; i < largeNumber; i++) { 
    NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
    str = [str stringByAppendingString:@" - world"]; 
}
复制代码
  • [NSString stringWithFormat:@"hello -%04d", i]方法创建的对象会加入到自动释放池里,对象的释放权交给了RunLoop 的释放池
  • RunLoop 的释放池会等待Runloop即将进入睡眠或者即将退出的时候释放一次
  • for循环中线程一直在做事情,Runloop不会进入睡眠
  • 上边的代码for循环生成的NSString对象会无法及时释放,造成瞬时内存占用过大

解决办法,每次循环时都手动创建一个局部释放池,及时创建,及时释放,这样NSString对象就会及时得到释放

for (int i = 0; i < largeNumber; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"hello -%04d", i];
        str = [str stringByAppendingString:@" - world"];
    }
}
复制代码

在for循环大量使用imageNamed:之类的方法生成UIImage对象可能是个更要命的事情,内存随时可能因为占用过多被系统杀掉。这种情况下利用Autoreleasepool可以大幅度降低程序的内存占用。

Autorelease对象什么时候释放

当创建了局部释放池时,会在@autoreleasepool{}的右大括号结束时释放,及时释放对象大幅度降低程序的内存占用。在没有手动加@autoreleasepool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的(每个线程对应一个runloop),而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池。

面试题

1. 临时变量什么时候释放?

如果在正常情况下,一般是超出其作用域就会立即释放 如果将临时变量加入了自动释放池,会延迟释放,即在runloop休眠或者autoreleasepool作用域之后释放

2. AutoreleasePool原理

自动释放池的本质是一个AutoreleasePoolPage结构体对象,是一个栈结构存储的页,每一个AutoreleasePoolPage都是以双向链表的形式连接 自动释放池的压栈和出栈主要是通过结构体的构造函数和析构函数调用底层的objc_autoreleasePoolPush和objc_autoreleasePoolPop,实际上是调用AutoreleasePoolPage的push和pop两个方法 每次调用push操作其实就是创建一个新的AutoreleasePoolPage,而AutoreleasePoolPage的具体操作就是插入一个POOL_BOUNDARY,并返回插入POOL_BOUNDARY的内存地址。而push内部调用autoreleaseFast方法处理,主要有以下三种情况

  • 当page存在,且不满时,调用add方法将对象添加至page的next指针处,并next递增
  • 当page存在,且已满时,调用autoreleaseFullPage初始化一个新的page,然后调用add方法将对象添加至page栈中
  • 当page不存在时,调用autoreleaseNoPage创建一个hotPage,然后调用add方法将对象添加至page栈中
  • 当执行pop操作时,会传入一个值,这个值就是push操作的返回值,即POOL_BOUNDARY的内存地址token。

所以pop内部的实现就是根据token找到哨兵对象所处的page中,然后使用 objc_release 释放 token之前的对象,并把next 指针到正确位置

3. AutoreleasePool能否嵌套使用?

可以嵌套使用,其目的是可以控制应用程序的内存峰值,使其不要太高 可以嵌套的原因是因为自动释放池是以栈为节点,通过双向链表的形式连接的,且是和线程一一对应的 自动释放池的多层嵌套其实就是不停的pushs哨兵对象,在pop时,会先释放里面的,在释放外面的

4. 哪些对象可以加入AutoreleasePool?alloc创建可以吗?

使用new、alloc、copy关键字生成的对象和retain了的对象需要手动释放,不会被添加到自动释放池中 设置为autorelease的对象不需要手动释放,会直接进入自动释放池 所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中 面试题5:AutoreleasePool的释放时机是什么时候? App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。 第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创 建释放池发生在其他所有回调之前。 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即 将退出 Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

6. thread 和 AutoreleasePool的关系

每个线程,包括主线程在内都维护了自己的自动释放池堆栈结构 新的自动释放池在被创建时,会被添加到栈顶;当自动释放池销毁时,会从栈中移除对于当前线程来说,会将自动释放的对象放入自动释放池的栈顶;在线程停止时,会自动释放掉与该线程关联的所有自动释放池 总结:每个线程都有与之关联的自动释放池堆栈结构,新的pool在创建时会被压栈到栈顶,pool销毁时,会被出栈,对于当前线程来说,释放对象会被压栈到栈顶,线程停止时,会自动释放与之关联的自动释放池 面试题7:RunLoop 和 AutoreleasePool的关系 主程序的RunLoop在每次事件循环之前之前,会自动创建一个 autoreleasePool 并且会在事件循环结束时,执行drain操作,释放其中的对象

优秀的博客

文章:自动释放池 AutoreleasePool
作者:毛发浓密的女猴子
链接:juejin.cn/post/698026…
来源:稀土掘金

非常细致的AutoReleasepool详解: github.com/draveness/a…