本文首发于个人博客
前言
文章开始之前,先想想下面三种场景,分别输出什么呢?
注意str的长度不能太短
注意str的长度不能太短
注意str的长度不能太短
1 | @interface ViewController () |
这个问题,暂时先放下,继续往下看。
autoreleasepool生成c++文件
有如下代码
1 |
|
执行命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp生成c++文件,其对应的代码如下所示。
1 | { __AtAutoreleasePool __autoreleasepool; |
简化一下也就是
1 | { |
其中__AtAutoreleasePool是什么呢?这是一个结构体,其内容如下,包含一个构造函数,在创建结构体的时候调用。一个析构函数,在结构体销毁的时候调用。
1 | struct __AtAutoreleasePool { |
所以,放在一起就是在开始的时候调用 objc_autoreleasePoolPush()结束时候调用objc_autoreleasePoolPop(atautoreleasepoolobj)
1 | struct __AtAutoreleasePool { |
源码分析
AutoreleasePoolPage
具体源码可以再Runtime源码中查看,从源码可以看到objc_autoreleasePoolPush()和objc_autoreleasePoolPop
1 | void * |
也就是说,这两个函数都是操作AutoreleasePoolPage来实现的。
类AutoreleasePoolPage中代码较多,筛选出主要代码如下
1 | class AutoreleasePoolPage |
可以看出
AutoreleasePoolPage对象通过双向链表的形式连接在一起
其中
- magic 用来校验 AutoreleasePoolPage 的结构是否完整;
- next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
- thread 指向当前线程;说明了,AutoreleasePoolPage和线程一一对应的。
- parent 指向父结点
- child 指向子结点
- depth 代表深度,从 0 开始,往后递增 1;
- hiwat 代表 high water mark 。
每个AutoreleasePoolPage对象占用4096字节
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
1 |
|
从上面的源码中可以看出来
- 每个
AutoreleasePoolPage有是4096字节, - 以及
begin指向的是开始存放autorelease对象的地方, end指向结尾的位置
AutoreleasePoolPage存不下了怎么办?
如果一个AutoreleasePoolPage存不下了,就会再创建一个AutoreleasePoolPage对象,第一个AutoreleasePoolPage对象的child指向第二个AutoreleasePoolPage对象,第二个AutoreleasePoolPage对象的parent指向第一个AutoreleasePoolPage对象。图形表示就是如下

push、pop、autorelease
AutoreleasePoolPage里面有push和pop函数
调用
push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址调用
pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARYid *next指向了下一个能存放autorelease对象地址的区域
push
1 | static inline void *push() |
1 | static inline id *autoreleaseFast(id obj) |
pop
1 | static inline void pop(void *token) |
autorelease
1 | inline id |
rootAutorelease调用rootAutorelease2
1 | inline id |
rootAutorelease2调用autorelease
1 | __attribute__((noinline,used)) |
autorelease
1 | static inline id autorelease(id obj) |
POOL_BOUNDARY
上面的源码可以发现POOL_BOUNDARY是个很重要的角色,相当于一个哨兵,
- 每当进行一次
objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象(POOL_BOUNDARY),值为0(也就是个nil) objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是- 根据传入的哨兵对象地址找到哨兵对象所处的page
- 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次
- release消息,并向回移动next指针到正确位置 - 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page
@autoreleasepool的嵌套
如果多个@autoreleasepool嵌套会怎么样呢?
打印
源码中有如下代码
1 | void |
也就是说_objc_autoreleasePoolPrint函数可以用来打印一些日志
一层@autoreleasepool
1 | extern void _objc_autoreleasePoolPrint(void); |
输出如下,只有一个哨兵对象(POOL)
1 | objc[32644]: ############## |
三层@autoreleasepool
如果有三个@autoreleasepool呢?
1 | extern void _objc_autoreleasePoolPrint(void); |
输出如下,有三个POOL,说明有三个哨兵。
1 | objc[32735]: ############## |
销毁一个@autoreleasepool
如果上面代码中最里面的@autoreleasepool退出之后再打印呢?
1 | extern void _objc_autoreleasePoolPrint(void); |
输出为如下,只有两个哨兵(POOL)
1 | objc[32812]: ############## |
进一步说明了,
调用push方法会将一个
POOL_BOUNDARY入栈,并且返回其存放的内存地址调用pop方法时传入一个
POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
开头的问题
现在我们回过头看文章开头的问题,应该很好回答了。
1 | @interface ViewController () |
输出结果如下:
1 | // 场景一 |
场景一
当使用 [NSString stringWithFormat:@"https://ityongzhen.github.io/"] 创建一个对象时,这个对象的引用计数为 1 ,并且这个对象被系统自动添加到了当前的 autoreleasepool 中。当使用局部变量 str 指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。因为在 ARC 下 NSString *str本质上就是 __strong NSString *str 。所以在 viewDidLoad 方法返回前,这个对象是一直存在的,且引用计数为 2 。而当viewDidLoad 方法返回时,局部变量 str 被回收,指向了 nil 。因此,其所指向对象的引用计数 -1 ,变成了 1 。
而在 viewWillAppear 方法中,我们仍然可以打印出这个对象的值,在viewDidAppear方法中,这个值为空,这个就要牵扯到RunLoop的知识了。详解RunLoop之源码分析一文讲述了RunLoop的底层,这里说一下,我们的iOS处理事件是以RunLoop一直循环执行的。viewDidLoad和viewWillAppear在同一个RunLoop循环中,所以在 viewWillAppear 方法中,我们仍然可以打印出这个对象的值,但是viewDidLoad的时候,那个RunLoop循环已经执行完了,这个对象才被彻底的释放。
场景二
当通过 [NSString stringWithFormat:@"https://ityongzhen.github.io/"] 创建一个对象时,这个对象的引用计数为 1 。而当使用局部变量 str 指向这个对象时,这个对象的引用计数 +1 ,变成了 2 。而出了当前作用域时,局部变量 str 变成了 nil ,所以其所指向对象的引用计数变成 1 。另外,我们知道当出了 @autoreleasepool {}的作用域时,当前 autoreleasepool 被 drain ,其中的 autoreleased 对象被 release 。所以这个对象的引用计数变成了 0 ,对象最终被释放
场景三
当出了 @autoreleasepool {} 的作用域时,其中的 autoreleased 对象被 release ,对象的引用计数变成 1 。当出了局部变量 str 的作用域,即 viewDidLoad 方法返回时,str 指向了 nil ,其所指向对象的引用计数变成 0 ,对象最终被释放
注意点
前面说了注意str的长度不能太短是为什么呢?
是因为如果str过短。例如
1 | @interface ViewController () |
结果如下:
1 | // 场景一 |
这是因为,字符串的abc采用的是Tagged Pointer技术,不是一个标准的OC对象。不存在说再堆上开辟空间存储对象什么的。关于Tagged Pointer可以参考这篇文章iOS中的引用计数,这里不做赘述。
总结
自动释放池的主要底层数据结构是:
__AtAutoreleasePool、AutoreleasePoolPage调用了
autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的每个
AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址所有的
AutoreleasePoolPage对象通过双向链表的形式连接在一起调用
push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址调用
pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARYid *next指向了下一个能存放autorelease对象地址的区域iOS在主线程的
Runloop中注册了2个Observer- 第1个
Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush() - 第2个
Observer- 监听了
kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() - 监听了
kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()
- 监听了
- 第1个
在当次RunLoop将要结束的时候,调用
objc_autoreleasePoolPop()