本文首发于个人博客
前言
文章开始之前,先想想下面三种场景,分别输出什么呢?
注意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_BOUNDARY
id *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_BOUNDARY
id *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()