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)

ObserverRunLoop两次SleepPop旧的并释放对象 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 加入到顶层的 RunLoopcommonModeItems 中。commonModeItemsRunLoop 自动更新到所有具有 CommonMode 里去。

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

参考

Threading Programming Guide - Runloop

Last updated