AutoreleasePool 的实现机制 (二)

本文章基于 objc4-725 进行测试.
objc4 的代码可以在 https://opensource.apple.com/tarballs/objc4/ 中得到.
本篇文章主要分析 AutoreleasePoolPage 类的成员变量、常量、静态成员等.

AutoreleasePoolPage 类的成员变量

自动释放池实际上是一个由 AutoreleasePoolPage 组成的双向链表结构, 一个自动释放池中有一个或多个 AutoreleasePoolPage 节点, 每个节点除了有本类的成员之外, 还有系统分配的一部分用来存储 Autorelease 变量地址的内存. 每当有对象被标记为 Autorelease 对象时, 该对象的地址就会存入对应的 AutoreleasePoolPage 中预留好的地址里, 如果一个 AutoreleasePoolPage 节点预留的地址空间已满, 就会重新生成一个新的节点放入链表中, 然后在新的节点中加入 Autorelease 对象地址.

#   define EMPTY_POOL_PLACEHOLDER ((id*)1) //pool 中没有对象存入
#   define POOL_BOUNDARY nil //哨兵对象, 用来标识释放边界
    static pthread_key_t const key = AUTORELEASE_POOL_KEY; //键值对中的key, 值为 AutoreleasePoolPage * 类型指针
    static uint8_t const SCRIBBLE = 0xA3; //释放后的位置会置为 0xA3A3A3A3
    static size_t const SIZE = PAGE_MAX_SIZE; //每个 page 的大小, 4096 个字节(Page 对象本身占 56 个字节)
    static size_t const COUNT = SIZE / sizeof(id); //page 中能存放的对象个数

    magic_t const magic; //用于校验 AutoreleasePoolPage 的结构
    id *next; //下一个对象要插入的位置
    pthread_t const thread; //所属线程句柄
    AutoreleasePoolPage * const parent; //双向链表的上一个节点
    AutoreleasePoolPage *child; //双向链表的下一个节点
    uint32_t const depth; //链表深度
    uint32_t hiwat; //AutoreleasePool 中存储对象个数的最大值

宏定义和静态常量

  • EMPTY_POOL_PLACEHOLDER

根据注释以及代码分析, 可以大致得出这个宏定义用作 pool 中没有 add 入对象时的标记. 当一个自动释放池被创建但是没有加入任何 Autorelease 对象时, 会让这个自动释放池的句柄等于 EMPTY_POOL_PLACEHOLDER, 并不为其分配内存. 注释说在顶层(即 libdispatch)进行 pool 的 push 和 pop 但并没有使用这个池子的时候, 能节省空间, OC类中有许多自带 block 的方法, 比如 NSArray 以及 NSDictionary 的 enumerat 系列方法中, 自带的 block 会内嵌 AutoreleasePool:

    NSArray * array = @[@1];
    [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        //这里的代码已经处于内嵌的 @autoreleasepool {} 中了
    }];
    NSLog(@"%@", array);

验证方式:

    __block __weak NSArray * weakArray;
    NSArray * bc = @[@"1"];
    [bc enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSArray * array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%@", @(argc)], nil];
        weakArray = array;
    }];
    NSLog(@"%@", weakArray);

以上代码输出为 null, 证明 block 中内嵌了 @autoreleasepool.
如果内嵌了 @autoreleasepool 的代码块中没有使用到 Autorelease 对象, 却为池子分配了节点, 无疑是对内存的浪费, EMPTY_POOL_PLACEHOLDER 即是为了解决这种情况下发生的内存浪费而存在的.

  • POOL_BOUNDARY

POOL_BOUNDARY 会在建立新的自动释放池时作为第一个对象加入到池中, 被称为哨兵对象, 哨兵对象是自动释放池中非常巧妙而且重要的一环. 我们已经知道 @autoreleasepool {} 是在作用域的开始使用 push() 方法来创建自动释放池, 在作用域结束时, 使用 pop() 方法来销毁自动释放池. 在嵌套结构中 push() 方法不一定会创建新的 page 节点, 如果当前节点未满则会直接插入一个哨兵对象, 如果当前节点已满则创建一个新的 page 节点并且插入一个哨兵对象, push() 函数的返回值就是这个哨兵对象的地址(哨兵对象的值是 nil, 但哨兵对象的地址不为 nil), 然后在 pop() 方法调用时, 传入这个哨兵对象的地址, 对这个地址之后的 Autorelease 对象发送 release 方法.


自动释放池的嵌套

第一个 pop() 方法, 会对 Page3 中的哨兵对象之后的对象发送 release 消息, 第二个 pop() 方法则会对 Page1 中的哨兵对象之后的对象发送 release 消息, 完美的解决了自动释放池的嵌套问题.

  • key

key 是一个静态常量, 值为 AUTORELEASE_POOL_KEY, 查看宏定义得知 AUTORELEASE_POOL_KEY 最终值为 43, 是一个确定的数字, 通过 tls_get_direct() 以 key 作为参数, 可以取出当前线程对应的自动释放池的 hotPage(即当前正在使用的 page 节点). tls_get_direct() 函数内部结合线程ID和 AUTORELEASE_POOL_KEY(43) 两个信息最终找出该线程对应的 hotPage.

  • SCRIBBLE

SCRIBBLE 的值为 0xA3. 在 pop() 方法被执行时, 会给对应的哨兵对象之后的所有对象都发送 release 消息, 同时从 page 节点中移除这些对象, 移除的方法就是使用 memset() 函数给原来存储对象地址的地方, 存入SCRIBBLE(memset() 会为 8 个字节的地址都存入 0xA3, 即为 0xA3A3A3A3A3A3A3A3).

  • SIZE

SIZE = PAGE_MAX_SIZE, 宏定义 PAGE_MAX_SIZE 的最终值为 4096(单位是字节), 这个静态常量表示一个 AutoreleasePoolPage 中用于存储对象指针的内存大小.

  • COUNT

COUNT 即每个 page 节点能存储的 Autorelease 对象个数, 在 64 位操作系统下, 4096 个字节可以存储 8 字节的指针共 512 个, 即一个 page 节点能存储 512 个对象. page 对象本身占 56 个字节, 所以实际能存储 Autorelease 对象个数为 505 个.
COUNT 在代码中并没有实际用途, 只是配合 depth 和 hiwat 在调试输出时使用.

成员变量

  • magic

magic 是一个 magic_t 类型的结构体, 该结构体的成员变量只有一个 uint32_t m[4], 这个数组中存的是固定的值, 比较重要的成员函数是 check(), check() 方法中通过对数组 m 和原有固定值做对比来判断 AutoreleasePoolPage 的完整性. 在几乎所有调用 check() 方法的地方, 如果检查不通过, 即认定 AutoreleasePoolPage 结构被破坏, 然后会抛出异常并打印异常信息.
成员变量 magic 在该类的成员变量列表的第一个, 数组 m 也处于 magic_t 结构体的成员变量列表的第一个, 也就是说一旦 AutoreleasePoolPage 结构被破坏, 最先破坏的就是对象的起始位置. 目前还不清楚何种情况下 AutoreleasePoolPage 的结构会被破坏, 但日常开发中几乎没有遇到过 AutoreleasePoolPage 破坏的异常信息.

  • next

指向 page 中下一个将要存放 Autorelease 对象的地址, 通常通过 *next++ = obj 来实现对象的存入和 next 指针的累加, 用 *--next 来取出要 release 的对象和实现 next 的递减.

  • thread

当前 page 节点所属的线程句柄.

  • parent 和 child

parent 是双向链表结构中指向上一个节点的指针, child 是双向链表中指向下一个节点的指针.

  • depth 和 hiwat

depth 是链表的深度(即总共的节点数), hiwat 是对应的自动释放池中存储对象个数的最大值(比如曾经存储过100个, 现在只剩50个, hiwat 就是 100), 在源码里, 只找到了 depth 为 hiwat 服务的代码, hiwat 只找到了在输出 AutoreleasePool 信息时使用的代码, 除此之外还不了解 depth 和 hiwat 两个成员变量的使用位置, 可能只是为了调试时输出 AutoreleasePool 信息.

下一篇文章会深入 objc4 代码, 解析 AutoreleasePoolPage 类的成员函数(已更新 Autoreleasepool 的实现机制 (三) https://www.jianshu.com/p/2cda49470fa4).

你可能感兴趣的:(AutoreleasePool 的实现机制 (二))