# 05.自动释放池与Runloop

### 一、 寻找线索

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

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

![1](/files/KxLuaHq63pTrxzl5SJm6)

![2](/files/1BqrTN1FTCPt3FlPVLQx)

这里我们发现 `@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` 能够互相找到
  * 每页的内存地址并不是连续的

#### 分页结构图

![自动释放池-双向链表分页](/files/1DB5otf3jDt4MLCE8pj4)

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

#### Push 流程

![自动释放池-入栈逻辑](/files/Dbgd4tIba6jyJjqXjCuS)

#### Pop 流程

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

### 三、 一个注意点

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

### 四、 Runloop

![3](/files/0PRffIGaHh6wst6CKb7d)

#### 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-结构](/files/ZkErUE6WcBTNNQ8FpELK)

**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原理](/files/FbXXNYsDOJFFHqjPOISU)

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://ryukiedev.gitbook.io/wiki/ios/nei-cun-guan-li/05.-zi-dong-shi-fang-chi-yu-runloop.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
