# 05.自动释放池与Runloop

### 一、 寻找线索

为了探究 autoreleasepool 的原理，我们需要定位到源码。我们通过汇编断点试试

我们在 main 函数中添加断点：

![1](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MI8JgbGh3U6X_oedqkm%2Fuploads%2Fgit-blob-23526596829cebc442d896dfe8f9aab44b6297e3%2F05-01.png?alt=media)

![2](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MI8JgbGh3U6X_oedqkm%2Fuploads%2Fgit-blob-8d20a7ff4da8e6b8ac246b681ff31b7fee18123b%2F05-02.png?alt=media)

这里我们发现 `@autoreleasepool` 关键字实际调用的是： `objc_autoreleasePoolPush`。同时还发现一个与之匹配的 `objc_autoreleasePoolPop` 。

```C++
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` 能够互相找到
  * 每页的内存地址并不是连续的

#### 分页结构图

![自动释放池-双向链表分页](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MI8JgbGh3U6X_oedqkm%2Fuploads%2Fgit-blob-818e5024bcd3f1630053abfd3d5a7f30a6a9c631%2F%E8%87%AA%E5%8A%A8%E9%87%8A%E6%94%BE%E6%B1%A0%E4%B8%8ERunloop-%E8%87%AA%E5%8A%A8%E9%87%8A%E6%94%BE%E6%B1%A0-%E5%8F%8C%E5%90%91%E9%93%BE%E8%A1%A8%E5%88%86%E9%A1%B5.png?alt=media)

> 每页的容量，源码中有 `1 << 12`

#### Push 流程

![自动释放池-入栈逻辑](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MI8JgbGh3U6X_oedqkm%2Fuploads%2Fgit-blob-46064e440efe8f3062619de7059d7b3d19289a9c%2F%E8%87%AA%E5%8A%A8%E9%87%8A%E6%94%BE%E6%B1%A0%E4%B8%8ERunloop-%E8%87%AA%E5%8A%A8%E9%87%8A%E6%94%BE%E6%B1%A0-%E5%85%A5%E6%A0%88%E9%80%BB%E8%BE%91.png?alt=media)

#### Pop 流程

* 先入后出
  * 从栈顶开始通过内存平移进行处理
* 一个 `Page` 空了就将这个 `Page` 释放
  * 并将上一个 `Page` ， 即父节点 设为 `Hot`

### 三、 一个注意点

通过 `alloc` `new` `copy` `mutablecopy` `other` 前缀的方法创建的对象不会加入自动释放池。

### 四、 Runloop

![3](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MI8JgbGh3U6X_oedqkm%2Fuploads%2Fgit-blob-3d41f537a00f0eb733beb54e56e2c926470c25e8%2F05-03.jpg?alt=media)

#### 4.1 什么是RunLoop

* 使程序一直运行并接受用户输入
* 决定程序在何时应该处理那些`事件`
* 消息队列
* 节省`CPU`时间

#### 4.2 RunLoop 与 线程

* `线程` 和 `RunLoop` 之间是一一对应的(可嵌套)，其关系是保存在一个全局的 `Dictionary` 里
* 线程结束时销毁, 或手动退出

#### 4.3 结构

```swift
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};
```

![Runloop-结构](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MI8JgbGh3U6X_oedqkm%2Fuploads%2Fgit-blob-af87f26e9eb3b358662f2e9653945f84f5bd2dc3%2F%E8%87%AA%E5%8A%A8%E9%87%8A%E6%94%BE%E6%B1%A0%E4%B8%8ERunloop-Runloop-%E7%BB%93%E6%9E%84.png?alt=media)

**Source**

`RunLoop` 的数据源抽象类

> RunLoop运行必须要在加入NSTimer或Source0、Sourc1、Observer输入后运行否则会直接退出

**Source0: 处理App内部事件, App自行管理**

* 包含了一个`函数指针`，它并不能主动触发事件
* 调用 `CFRunLoopSourceSignal(source)`，将这个 Source 标记为待处理
* 调用 `CFRunLoopWakeUp(runloop)` 来唤醒 RunLoop，让其处理这个事件

**Source1: 由`RunLoop`内核管理, `MachPort`驱动(进程间通讯)**

* 包含了一个 `mach_port` 和一个 `函数指针`
* 被用于通过内核和其他线程相互发送消息
* 如解锁/摇晃等

**Timer**

* 包含了时长 和 `函数指针`

**Observer**

向外部报告 `RunLoop` 状态的变化

```Swift
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**

```swift
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
```

* 在同一时间必须且只能在一个`Mode`下运行
* 切换`Mode`必须退出`RunLoop`,重启
* 保证页面滑动流畅的关键

```swift
NSDefaultRunLoopMode // 默认状态 空闲状态
UITrackingRunLoopMode // 滑动
//私有Mode
```

> 当创建一个 `Timer` 并加到 `DefaultMode` 时，`Timer` 会得到重复回调，但此时滑动一个`TableView` 时，`RunLoop` 会将 `mode` 切换为 `TrackingRunLoopMode`，这时 `Timer` 就不会被回调，并且也不会影响到滑动操作。这两个 `Mode` 都已经被标记为`Common`

特殊`Mode`:

```swift
NSRunLoopCommonModes
```

> 有时你需要一个 `Timer`，在两个 `Mode` 中都能得到回调，一种办法就是将这个 `Timer` 分别加入这两个 `Mode`。还有一种方式，就是将 `Timer` 加入到顶层的 `RunLoop` 的 `commonModeItems` 中。`commonModeItems` 被 `RunLoop` 自动更新到所有具有 `Common` 的 `Mode` 里去。

#### 4.4 Runloop 原理

![Runloop原理](https://4193904735-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MI8JgbGh3U6X_oedqkm%2Fuploads%2Fgit-blob-373070d64df294836d8458a6f8e9ebaf1da28264%2F%E8%87%AA%E5%8A%A8%E9%87%8A%E6%94%BE%E6%B1%A0%E4%B8%8ERunloop-Runloop%E5%8E%9F%E7%90%86.png?alt=media)

### 五、 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

### 参考

[Threading Programming Guide - Runloop](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1)
