一、 寻找线索
为了探究 autoreleasepool 的原理,我们需要定位到源码。我们通过汇编断点试试
我们在 main 函数中添加断点:


这里我们发现 @autoreleasepool 关键字实际调用的是: objc_autoreleasePoolPush。同时还发现一个与之匹配的 objc_autoreleasePoolPop 。
void *objc_autoreleasePoolPush(void){ return AutoreleasePoolPage::push();}在源码中我们可以发现最终调用的是 AutoreleasePoolPage 的方法
二、 AutoreleasePoolPage
/*********************************************************************** Autorelease pool implementation
A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary. A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released. The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. Thread-local storage points to the hot page, where newly autoreleased objects are stored.**********************************************************************/一个线程的自动释放池是一个栈结构,存放的指针。每个指针要么是一个待释放的对象,要么是一个自动释放边界 POOL_BOUNDARY 。
一个 pool token 是一个指向该自动释放池的 POOL_BOUNDARY 的指针。
栈被绑定到一个双向链表。Pages 会按需进行增删。
POOL_BOUNDARY边界的作用是防止访问到不该访问的内存区域- 只在第一页中存在哨兵对象
- 这里使用双向链表的作用是,
Page能够互相找到- 每页的内存地址并不是连续的
分页结构图

每页的容量,源码中有
1 << 12
Push 流程

Pop 流程
- 先入后出
- 从栈顶开始通过内存平移进行处理
- 一个
Page空了就将这个Page释放- 并将上一个
Page, 即父节点 设为Hot
- 并将上一个
三、 一个注意点
通过 alloc new copy mutablecopy other 前缀的方法创建的对象不会加入自动释放池。
四、 Runloop

4.1 什么是RunLoop
- 使程序一直运行并接受用户输入
- 决定程序在何时应该处理那些
事件 - 消息队列
- 节省
CPU时间
4.2 RunLoop 与 线程
线程和RunLoop之间是一一对应的(可嵌套),其关系是保存在一个全局的Dictionary里- 线程结束时销毁, 或手动退出
4.3 结构
struct __CFRunLoop { CFMutableSetRef _commonModes; // Set CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer> CFRunLoopModeRef _currentMode; // Current Runloop Mode CFMutableSetRef _modes; // Set ...};
Source
RunLoop 的数据源抽象类
RunLoop运行必须要在加入NSTimer或Source0、Sourc1、Observer输入后运行否则会直接退出
Source0: 处理App内部事件, App自行管理
- 包含了一个
函数指针,它并不能主动触发事件 - 调用
CFRunLoopSourceSignal(source),将这个 Source 标记为待处理 - 调用
CFRunLoopWakeUp(runloop)来唤醒 RunLoop,让其处理这个事件
Source1: 由RunLoop内核管理, MachPort驱动(进程间通讯)
- 包含了一个
mach_port和一个函数指针 - 被用于通过内核和其他线程相互发送消息
- 如解锁/摇晃等
Timer
- 包含了时长 和
函数指针
Observer
向外部报告 RunLoop 状态的变化
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop};Observer 与 AutoreleasePool(UIKit)
Observer 在RunLoop两次Sleep间 Pop旧的并释放对象 Push新的
Mode
struct __CFRunLoopMode { CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode" CFMutableSetRef _sources0; // Set CFMutableSetRef _sources1; // Set CFMutableArrayRef _observers; // Array CFMutableArrayRef _timers; // Array ...};- 在同一时间必须且只能在一个
Mode下运行 - 切换
Mode必须退出RunLoop,重启 - 保证页面滑动流畅的关键
NSDefaultRunLoopMode // 默认状态 空闲状态UITrackingRunLoopMode // 滑动//私有Mode当创建一个
Timer并加到DefaultMode时,Timer会得到重复回调,但此时滑动一个TableView时,RunLoop会将mode切换为TrackingRunLoopMode,这时Timer就不会被回调,并且也不会影响到滑动操作。这两个Mode都已经被标记为Common
特殊Mode:
NSRunLoopCommonModes有时你需要一个
Timer,在两个Mode中都能得到回调,一种办法就是将这个Timer分别加入这两个Mode。还有一种方式,就是将Timer加入到顶层的RunLoop的commonModeItems中。commonModeItems被RunLoop自动更新到所有具有Common的Mode里去。
4.4 Runloop 原理

五、 AutoreleasePool 什么时候创建?什么时候销毁?
App启动后,系统在主线程RunLoop 里注册两个Observser,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
- 第一个 Observer 监视的事件 是 Entry(即将进入Loop)
- 其回调内会调用 _objc_autoreleasePoolPush()
创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。
- 其回调内会调用 _objc_autoreleasePoolPush()
- 第二个 Observer 监视了两个事件
_BeforeWaiting(准备进入休眠)时 _ 调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()释放旧的池并创建新池;_Exit(即将退出Loop)时 _ 调用_objc_autoreleasePoolPop()来释放自动释放池。- 这个 Observer 优先级最低,保证其
释放池子发生在其他所有回调之后。
现在我们知道了AutoreleasePool是在RunLoop即将进入RunLoop和准备进入休眠这两种状态的时候被创建和销毁的。
所以AutoreleasePool的释放有如下两种情况。
- 一是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
- 二是手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool