OC底层原理之《Cache 分析01》

已知类是objc_class结构体类型,之前的文章中已经分析了bits,本篇文章将介绍一下cache

一、源码环境下分析

struct objc_class : objc_object {
  objc_class(const objc_class&) = delete;
  objc_class(objc_class&&) = delete;
  void operator=(const objc_class&) = delete;
  void operator=(objc_class&&) = delete;
    // Class ISA;  isa指针来源于objc_object  占8个字节
    Class superclass;   // Class 为结构体指针,占8个字节
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

如图可知objc_class偏移16个字节则为cache的地址空间。首先在源码环境下去调试验证

已知有个名为JPerson的类,接下来去lldb控制台来打印JPerson类的数据。

(lldb) p/x pClass
(Class) $0 = 0x00000001000044c0 JPerson
(lldb) p/x 0x00000001000044c0 + 0x10 
(long) $1 = 0x00000001000044d0
(lldb) p (cache_t *)0x00000001000044d0
(cache_t *) $2 = 0x00000001000044d0
(lldb) p *$2
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298433376
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32808
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802800000000
      }
    }
  }
}
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8
    };

  inline SEL sel() const { return _sel.load(memory_order_relaxed); }

  #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
  #define MAYBE_UNUSED_ISA
  #else
  #define MAYBE_UNUSED_ISA __attribute__((unused))
  #endif
      inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
          uintptr_t imp = _imp.load(memory_order_relaxed);
          if (!imp) return nil;
  #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
  #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
          imp ^= (uintptr_t)cls;
  #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
  #else
  #error Unknown method cache IMP encoding.
  #endif
         return (IMP)imp;
      }

     inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)   const {
           uintptr_t imp = _imp.load(memory_order_relaxed);
          if (!imp) return nil;
  #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
          SEL sel = _sel.load(memory_order_relaxed);
          return (IMP)
               ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, sel, cls),
                                    ptrauth_key_function_pointer, 0);
  #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
          return (IMP)(imp ^ (uintptr_t)cls);
  #elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
          return (IMP)imp;
  #else
  #error Unknown method cache IMP encoding.
  #endif
    }

如代码所示,对$2取值可以得到一个结构体类型。通过查看objc源码可知,结构体为cache_t结构体。

(lldb) p $3._bucketsAndMaybeMask
(explicit_atomic<unsigned long>) $4 = {
  std::__1::atomic<unsigned long> = {
    Value = 4298433376
  }
}
(lldb) p $4.Value
error: <user expression 5>:1:4: no member named 'Value' in 'explicit_atomic<unsigned long>'
$4.Value
~~ ^
(lldb) p $3._originalPreoptCache
(explicit_atomic<preopt_cache_t *>) $5 = {
  std::__1::atomic<preopt_cache_t *> = {
    Value = 0x0000802800000000
  }
}
(lldb) p $5.Value
error: <user expression 7>:1:4: no member named 'Value' in 'explicit_atomic<preopt_cache_t *>'
$5.Value
~~ ^

通过打印cache_t结构中的 _bucketsAndMaybeMask 和 _originalPreoptCache 发现Value都是不存在的,那么cache_t中的数据存在了哪里呢?这里通过阅读源码的cache_t 可知在结构体中的buckets的类型为bucket_t类型的结构体指针。而结构体bucket_t中存储的是sel和imp。

struct bucket_t *buckets() const;

struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

那么取buckets来看一下内容

(lldb) p $3.buckets()
(bucket_t *) $7 = 0x000000010034e360
(lldb) p *$7
(bucket_t) $8 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}

取出bucket_t之后发现sel 和 imp都是空的。这里是因为初始化JPerson之后,并没有执行方法,所以并没有缓存进来,执行

(lldb) p [p saySomething]
2021-06-30 18:39:08.561194+0800 KCObjcBuild[15414:540423] -[JPerson saySomething]

重新走一遍上述流程

(lldb) p [p saySomething]
2021-06-30 23:20:16.366619+0800 KCObjcBuild[16917:745477] -[JPerson saySomething]
(lldb) p/x pClass
(Class) $0 = 0x00000001000044c0 JPerson
(lldb) p (cache_t *)0x00000001000044d0
(cache_t *) $1 = 0x00000001000044d0
(lldb) p *$1 
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4302648560
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 7
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000007
      }
    }
  }
}
(lldb) p $2.buckets()  
(bucket_t *) $3 = 0x00000001007534f0
(lldb) p *$3
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 30816
    }
  }
}
(lldb) p $4.sel()
(SEL) $5 = "saySomething"

如代码所示,get $4的sel之后可以得到saySomething。已知是对象p调用了一个saySomething方法,如果我们再调用一个方法呢

(lldb) p [p doSomething]
2021-06-30 23:28:08.085634+0800 KCObjcBuild[16996:751214] -[JPerson doSomething]

那么doSomething 应该如何取出呢,重复上述流程,

(lldb) p $2.buckets()
(bucket_t *) $3 = 0x000000010121aa60
(lldb) p *$3
(bucket_t) $4 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 3376648
    }
  }
}
(lldb) p $4.sel()
(SEL) $5 = "respondsToSelector:"

如代码所示,$4的sel突然变成了respondsToSelector:方法,doSomething 和saySomething都不见了。

(lldb) p $2.buckets()[1]
(bucket_t) $6 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 30856
    }
  }
}
(lldb) p $6.sel()
(SEL) $7 = "saySomething"

这里说明一下buckets 其实是一个哈希链表结构,通过取buckets 的第一个元素,再打印$6的sel得到了saySomething方法

(lldb) p $2.buckets()[5]
(bucket_t) $11 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 30808
    }
  }
}
(lldb) p $11.sel()
(SEL) $12 = "doSomething"

通过此段代码可知,读取imp的方式
 inline IMP imp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, Class cls)   const {
           uintptr_t imp = _imp.load(memory_order_relaxed);
          if (!imp) return nil;
  #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
          SEL sel = _sel.load(memory_order_relaxed);
          return (IMP)

(lldb) p $11.imp(nil,pClass)
(IMP) $13 = 0x0000000100003c80 (KCObjcBuild`-[JPerson doSomething])

上述的调试是在源码环境下使用了lldb的方式去调试的,如果实在非源码环境下如何去调试呢。

二、非源码环境下分析

typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits

struct sn_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct sn_cache_t {
    struct sn_bucket_t *_bukets; // 8
    mask_t    _maybeMask; // 4
    uint16_t  _flags;  // 2
    uint16_t  _occupied; // 2
};

struct sn_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct sn_objc_class {
    Class isa;
    Class superclass;
    struct sn_cache_t cache;             // formerly cache pointer and vtable
    struct sn_class_data_bits_t bits;
};

如代码所示,将objc_class结构采用伪代码的形式,自定义一份。

        SNPerson *p  = [SNPerson alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];

        struct sn_objc_class *sn_class = (__bridge struct sn_objc_class *)(pClass);
        NSLog(@"%hu - %u",sn_class->cache._occupied,sn_class->cache._maybeMask);
        
        for (mask_t i = 0; i<sn_class->cache._occupied; i++) {
            struct sn_bucket_t bucket = sn_class->cache._bukets[I];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
        }

   lldb输出的结果为
   2021-07-06 13:00:52.781654+0800 003-cache_t脱离源码环境分析 [67319:589187] 2 - 3
   2021-07-06 13:00:52.781733+0800 003-cache_t脱离源码环境分析[67319:589187] say1 - 0x5750f
   2021-07-06 13:00:52.781823+0800 003-cache_t脱离源码环境分析[67319:589187] class - 0x7ffe6e4c7145f

通过输出可知_occupied_maybeMask分别为23,以_occupied为循环体条件输出了两个方法,say1和class。那么_occupied_maybeMask分别是什么呢?这里我们将回归源码去查询。

    void insert(SEL sel, IMP imp, id receiver);
    void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
    void destroy();
    void eraseNolock(const char *func);

查看cache_t结构体,发现有个insert方法。查看insert方法

此时的occupied只是getter方法,默认为0。如果是首次进来那么将会进入isConstantEmptyCache的方法

enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
    // When we have a cache end marker it fills a bucket slot, so having a
    // initial cache size of 2 buckets would not be efficient when one of the
    // slots is always filled with the end marker. So start with a cache size
    // 4 buckets.
    INIT_CACHE_SIZE_LOG2 = 2,
#else
    // Allow an initial bucket size of 2 buckets, since a large number of
    // classes, especially metaclasses, have very few imps, and we support
    // the ability to fill 100% of the cache before resizing.
    INIT_CACHE_SIZE_LOG2 = 1,
#endif
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
    FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
    FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};

if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE;//4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }

// newCapacity = 4 
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    // Cache's old contents are not propagated. 
    // This is thought to save cache memory at the cost of extra cache fills.
    // fixme re-measure this

    ASSERT(newCapacity > 0);
    ASSERT((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);

    setBucketsAndMask(newBuckets, newCapacity - 1);
    
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
    // ensure other threads see buckets contents before buckets pointer
    mega_barrier();

    _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);

    // ensure other threads see new buckets before new mask
    mega_barrier();

    _maybeMask.store(newMask, memory_order_relaxed);
    _occupied = 0;
}

已知我们的真机环境是arm64,所以INIT_CACHE_SIZE_LOG2等于2。INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),那么INIT_CACHE_SIZE等于4。通过reallocate里面的setBucketsAndMask方法可知,开辟的newBuckets是存在了_bucketsAndMaybeMask中,而_maybeMask则是存储了开辟的桶子大小减一。所以这就是前面伪代码输出的为什么_maybeMask为3的原因。而此时_occupied依然为0。再分析插入的代码。

bucket_t *b = buckets();
    mask_t m = capacity - 1; // 4-1=3
    mask_t begin = cache_hash(sel, m); // 通过hash来得到当前sel的下标
    mask_t i = begin;

    // Scan for the first unused slot and insert there.
    // There is guaranteed to be an empty slot.
    do {
        if (fastpath(b[i].sel() == 0)) {
            incrementOccupied();
            b[i].set<Atomic, Encoded>(b, sel, imp, cls());
            return;
        }
        if (b[i].sel() == sel) {
            // The entry was added to the cache by some other thread
            // before we grabbed the cacheUpdateLock.
            return;
        }
    } while (fastpath((i = cache_next(i, m)) != begin));

    void cache_t::incrementOccupied() 
    {
        _occupied++;
    }

当首次调用方法时fastpath(b[i].sel() == 0) 是成立的。调用incrementOccupied()方法可以看到_occupied++。所以这就是前面伪代码输出的为什么_occupied为1的原因。

总结

当首次调用方法时,会先开辟一个大小为4的内存空间,_maybeMask的大小为桶子的大小减一。如果要插入的方法不存在时,则_occupied会加1。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容