05.自动释放池与Runloop
一、 寻找线索
为了探究 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()
创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件
_BeforeWaiting(准备进入休眠)时 _ 调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()释放旧的池并创建新池;_Exit(即将退出Loop)时 _ 调用_objc_autoreleasePoolPop()来释放自动释放池。这个 Observer 优先级最低,保证其
释放池子发生在其他所有回调之后。
现在我们知道了AutoreleasePool是在RunLoop即将进入RunLoop和准备进入休眠这两种状态的时候被创建和销毁的。
所以AutoreleasePool的释放有如下两种情况。
一是Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
二是手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool
参考
Last updated
Was this helpful?