AutoreleasePool 的实现机制 (三)

本文章基于 objc4-725 进行测试.
objc4 的代码可以在 https://opensource.apple.com/tarballs/objc4/ 中得到.
本篇文章主要分析 AutoreleasePool 的创建和加入对象的操作.

AutoreleasePoolPage 类的成员函数

AutoreleasePoolPage 类的静态函数和成员函数众多, 有些函数没有贴出源码, 只写了内部逻辑, 所以需要结合源码来看.

  • push

函数内部通过对 bool 值 DebugPoolAllocation 的判断, 来决定调用什么函数. 根据注释以及变量名来看, DebugPoolAllocation 是用在调试模式下的, 调试模式下会直接生成新的 page 节点, 所以最终 push() 函数可以简化为:

static inline void *push()
{
    return autoreleaseFast(POOL_BOUNDARY);
}
  • autoreleaseFast
static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage(); //取出当前在用的 AutoreleasePoolPage 节点
    if (page && !page->full()) { //如果存在正在使用的节点并且节点未存满
        return page->add(obj); //将 obj 加入到该节点中
    } else if (page) { //如果有正在使用的节点
        return autoreleaseFullPage(obj, page); //该节点已满
    } else {
        return autoreleaseNoPage(obj); //没有正在使用的节点
    }
}
[obj autorelease], 开发中常用的 autorelease 方法, 其实内部就是直接调用了 autoreleaseFast.
  • hotPage
static inline AutoreleasePoolPage *hotPage()
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *)
    tls_get_direct(key); //使用tls_get_direct方法根据 key(前面提到 key 为固定值 43) 获取当前在用的节点
    if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil; //如果节点值为 EMPTY_POOL_PLACEHOLDER 则返回 nil
    if (result) result->fastcheck(); //如果节点存在, 调用 fastcheck()
    return result; //返回该节点
}

EMPTY_POOL_PLACEHOLDER 是空释放池的标记, 详情见前文(Autoreleasepool 的实现机制 (二) https://www.jianshu.com/p/b0b739e064ad)

  • tls_get_direct

函数内部, 排除对 _pthread_has_direct_tsd() 以及 TARGET_IPHONE_SIMULATOR 的判断(两个都是简单的判断是真机还是模拟器), 只考虑真机运行的情况, 最终调用情况是

__attribute__((always_inline)) static __inline__ void* _os_tsd_get_direct(unsigned long slot)
{
    return _os_tsd_get_base()[slot]; //slot即为传进来的key
}

//_os_tsd_get_base 函数的实现
__attribute__((always_inline, pure)) static __inline__ void** _os_tsd_get_base(void)
{
#if ......
  //一些非 arm64 环境下的操作
#elif defined(__arm64__) // arm64 版本对应的命令
    uint64_t tsd;
    __asm__("mrs %0, TPIDRRO_EL0" : "=r" (tsd)); //将线程指针寄存器(TPIDRRO_EL0)里的值赋值给变量 tsd
    tsd &= ~0x7ull; //tsd 和 0x7ull 取非后的数字进行按位与操作
#endif
    return (void**)(uintptr_t)tsd;
}

可见 tls_get_direct 函数内部最终是通过对线程指针寄存器中的数据经过运算后得到一个元素为 void * 型指针的数组, 该数组的第 43 号元素即为 hotPage(当前使用的 page 节点).

  • fastcheck 和 check

fastcheck() 中通过判断 CHECK_AUTORELEASEPOOL 来决定调用哪种 check 方式, 无论调用了哪种 check 方式, 最终都会涉及到 magic_t 结构体中的 check 方式, 最终可以简化为:

if (CHECK_AUTORELEASEPOOL) {
    return (m[0] == M0 && 0 == strncmp((char *)&m[1], M1, M1_len));
} else {
    return (m[0] == M0);
}

即是否判断数组 m 的后 3 个元素的问题. 最终 check 不通过的话, 就会触发 crash.

  • begin, end, empty, full
id * begin() {
    return (id *) ((uint8_t *)this+sizeof(*this)); //本对象的地址加上本对象的 size(56)
}

id * end() {
    return (id *) ((uint8_t *)this+SIZE); //本对象的地址加上 4096
}

bool empty() {
    return next == begin(); //next 指针指向 begin() 则代表 page 为空
}

bool full() { 
    return next == end(); //next 指针指向 end() 则代表 page 已满
}

Page 对象本身占 56 个字节, 所以 begin() 需要排除这 56 个字节, 真正用于存储 autorelease 对象地址的内存量为 end() - begin(), 共有 4040 个字节, 可存储 505 个 autorelease 变量.

  • add
id *add(id obj)
{
    assert(!full());
    unprotect();  //通常情况下不做任何操作, 解除自动释放池的保护
    id *ret = next;  //需要返回保存 autorelease 对象的地址, 注释说比先移位再返回 *(next - 1) 快
    *next++ = obj; //将 obj 存入 next 指向的空间中, 并将 next 指针向后移一位
    protect(); //通常情况下不做任何操作, 设置自动释放池保护
    return ret;
}

add() 函数是实现自动释放池的关键一环, 所有要加入到自动释放池中的操作最终都由 add() 函数来完成. 每次创建自动释放池时调用的 push() 的返回值就是 add() 函数的返回值, 所以当 push() 加入哨兵对象时, 得到的就是该哨兵对象存入的地址.

  • protect 和 unprotect
inline void protect() {
#if PROTECT_AUTORELEASEPOOL //宏定义的值为固定值 0
     mprotect(this, SIZE, PROT_READ); //改写 this 指向内存的属性为只读
     check();
#endif
}

inline void unprotect() {
#if PROTECT_AUTORELEASEPOOL //同上
     check();
     mprotect(this, SIZE, PROT_READ | PROT_WRITE); //改写 this 指向内存的属性为可读可写
#endif
}

PROTECT_AUTORELEASEPOOL 的宏定义的值在源码中写死为 0, 所以这两个函数通常情况下都没有任何作用, 设置这个值为 1 将对自动释放池进行保护, mprotect(start, size, pro) 是一个 Linux 的函数, 作用就是将从 start 开始, size 个字节的地址空间的读写权限设置为 pro. 即是说, 如果 PROTECT_AUTORELEASEPOOL 的宏定义值设置为 1, 则每次操作自动释放池时, 都需要先解除保护(设置为内存可读写), 之后再设置保护(设置为内存只读).

  • autoreleaseFullPage
static __attribute__((noinline)) id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    assert(page == hotPage());
    assert(page->full()  ||  DebugPoolAllocation);
    do {
         if (page->child) page = page->child; //如果 page 有 child, 则再向后找节点
         else page = new AutoreleasePoolPage(page); //如果 page 没有child, 则新建一个 page 节点, 此时会调用构造函数
    } while (page->full()); //如果 page 节点已满则继续循环
    setHotPage(page); //新节点设置为 hotPage
    return page->add(obj); //将 obj 添加到新节点里
}

本函数是创建新节点并向新节点中添加 autorelease 对象的方法, 该方法会从 hotPage 节点开始向后查找, 直到找到一个未满节点, 将找到的节点设为 hotPage 并且将对象加入到其中. 如果找不到未满节点, 则新建一个节点来使用.

  • AutoreleasePoolPage
AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
    : magic(), next(begin()), thread(pthread_self()),
     parent(newParent), child(nil), 
     depth(parent ? 1+parent->depth : 0), 
     hiwat(parent ? parent->hiwat : 0)
{ 
     if (parent) { //如果存在 parent
          parent->check(); //检查 parent 是否完整
          assert(!parent->child); //parent 如果已存在 child, 则抛出异常
          parent->unprotect(); //解除保护
          parent->child = this; //将 parent 的 child 指针指向本对象
          parent->protect(); //设置保护
    }
    protect(); //设置保护
}

本函数为 AutoreleasePoolPage 的构造函数, 创建新的 page 对象时, 会自动调用本函数. 构造新的对象时, 创建并初始化了一个 magic 结构体, 将 next 赋值为 begin(), thread 赋值为 pthread_self(), 将 parent 赋值为接收到的参数 newParent, child 初始化为 nil, depth 为 parent 的 depth + 1, hiwat 设置为 parent 的 hiwat, 如果 newParent 为 nil 则 depth 和 hiwat 都设置为0.

  • new
static void * operator new(size_t size) {
    return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}

重载 new 函数, 为 AutoreleasePoolPage 开辟一块大小为 SIZE, 地址对齐方式为 SIZE 的地址空间.

  • setHotPage
static inline void setHotPage(AutoreleasePoolPage *page) 
    {
        if (page) page->fastcheck(); //根据情况对 page 进行 check 操作
        tls_set_direct(key, (void *)page); //和 tls_get_direct 对应
    }

tls_set_direct(key, page) 和 tls_get_direct(key) 的内部逻辑基本一致, 只是 tls_get_direct(key) 在最后一步返回了第 43 号元素, 而 tls_set_direct(key, page) 在最后一步给第 43 号元素设置为 page.

  • autoreleaseNoPage
static __attribute__((noinline)) id *autoreleaseNoPage(id obj)
{
    assert(!hotPage());
    bool pushExtraBoundary = false;
    if (haveEmptyPoolPlaceholder()) { //是否存在未使用的空池
        pushExtraBoundary = true;
    } else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
        //obj 不是哨兵对象时, 说明上一次的 push() 没有成功, 如果打开了丢失 pools 的调试
        _objc_inform(...); //输出一些调试信息
        objc_autoreleaseNoPool(obj); //这个函数没有找到源码
        return nil;
    } else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
        //如果 obj 是哨兵对象, 并且没有开启 pool 内存申请的调试
        return setEmptyPoolPlaceholder(); //将本自动释放池标记为未使用的空池
    }
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); //创建一个 page 节点, 这里会调用构造函数
    setHotPage(page); //将该节点设置为 hotPage
    if (pushExtraBoundary) { //如果本自动释放池是一个未使用的空池
        page->add(POOL_BOUNDARY); //加入哨兵对象
    }
    return page->add(obj); //将 obj 加入自动释放池
}
  • haveEmptyPoolPlaceholder
static inline bool haveEmptyPoolPlaceholder()
{
     id *tls = (id *)tls_get_direct(key); //取出本线程对应的 hotPage
     return (tls == EMPTY_POOL_PLACEHOLDER); //如果为空池标识符则返回 true
}

判断本线程对应的自动释放池, 是否为空池.

  • setEmptyPoolPlaceholder
static inline id* setEmptyPoolPlaceholder()
{
     assert(tls_get_direct(key) == nil); //如果本线程对应的池非空, 则抛出异常
     tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER); //设置本线程对应的池为空池
     return EMPTY_POOL_PLACEHOLDER; //返回空池标志
}

和 haveEmptyPoolPlaceholder() 对应, 本函数为设置自动释放池为空池.

至此 push 和 add 相关的主要操作函数都已经列举完成, 需要注意的是, 每个线程第一次创建自动释放池时, 都会被标记为空池 EMPTY_POOL_PLACEHOLDER, 只有第一次加入 autorelease 对象时, 才会为其分配节点.

下一篇文章会继续解析 AutoreleasePool 销毁相关操作的函数.

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