iOS 底层学习6

iOS 底层第6天的学习。今天又学了一个新的技能,原来还能把底层源码拉出来在局部进行分析,真是受益匪浅啊

cache 探索

  • 我们已经知道在类的底层结构中有 isa,superclass,bit,cache
name desc
isa isa指针,objc isa-> Class,Class isa -> metal Class
superclass 父类
bit bit 里有 ro,rw,里有属性,方法,协议 等等
cache 缓存

而这里 cache 里到底存了有哪些东西呢?

  • 我们先 lldb 获取到 cache 的内存地址,经过一番操作如下所示
(lldb) p/x pClass
(Class) $0 = 0x00000001000085d0 Person
(lldb) p/x 0x00000001000085d0 + 0x10 // 平移16字节
(long) $1 = 0x00000001000085e0
(lldb) p (cache_t *)0x00000001000085e0 
(cache_t *) $2 = 0x00000001000085e0
(lldb) p *$2
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4298515360
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 0
        }
      }
      _flags = 32800
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0000802000000000
      }
    }
  }
}
  • 接下里就是要去分析 cache_t 源码,目的是为了找到相应的方法。
struct cache_t {

  ..... 省略部分代码
public:
    // The following four fields are public for objcdt's use only.
    // objcdt reaches into fields while the process is suspended
    // hence doesn't care for locks and pesky little details like this
    // and can safely use these.
    unsigned capacity() const;

    struct bucket_t *buckets() const;

    Class cls() const;

#if CONFIG_USE_PREOPT_CACHES
    const preopt_cache_t *preopt_cache() const;
#endif

    mask_t occupied() const;
    void initializeToEmpty();

#if CONFIG_USE_PREOPT_CACHES
    bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = (uintptr_t)&_objc_empty_cache) const;
    bool shouldFlush(SEL sel, IMP imp) const;
    bool isConstantOptimizedCacheWithInlinedSels() const;
    Class preoptFallbackClass() const;
    void maybeConvertToPreoptimized();
    void initializeToEmptyOrPreoptimizedInDisguise();
#else
    inline bool isConstantOptimizedCache(bool strict = false, uintptr_t empty_addr = 0) const { return false; }
    inline bool shouldFlush(SEL sel, IMP imp) const {
        return cache_getImp(cls(), sel) == imp;
    }
    inline bool isConstantOptimizedCacheWithInlinedSels() const { return false; }
    inline void initializeToEmptyOrPreoptimizedInDisguise() { initializeToEmpty(); }
#endif

    void insert(SEL sel, IMP imp, id receiver);

}
  • 我们看到 有 一个 struct bucket_t *buckets() const; buckets的结构体,
    点击进入 bucket_t 看一下。
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
}

发现了 _imp_sel ,由此我们可以知道 buckets 里面存储了 _imp 好 `_sel,

  • 接下来我们回到 lldb 去验证一下 buckets
(lldb) p  $3.buckets()
(bucket_t *) $5 = 0x00000001003623a0
(lldb) p *$5
(bucket_t) $6 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
  • 为什么这里的 Valuenil 呢? 原来是buckets 是一个哈希结构.
(lldb) p $3.buckets()[1]
(bucket_t) $6 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 48880
    }
  }
}
  • 查找bucket_t源码 ,发现2个方法
  inline SEL sel() const { return _sel.load(memory_order_relaxed); }
  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
    }

继续 lldb 输出 sel & imp

(lldb) p $6.sel()
(SEL) $8 = "doSomething"
(lldb) p $6.imp(nil,pClass)
(IMP) $12 = 0x0000000100003b00 (KCObjcBuild`-[Person doSomething])
  • 验证成功。但是每次都要去 lldb 打印 感觉真的好麻烦,有没有更方便的方法呢?接下来我们用底层局部代码块的形式继续分析,用 NSLog 日志方式进行打印。

局部代码分析 cache

  • 把获取的源码更改成自己需要的 struct
struct x_bucket_t {
    SEL _sel;
    IMP _imp;
};
 struct x_cache_t {
    uintptr_t _bucketsAndMaybeMask;
    mask_t    _maybeMask;
    uint16_t  _flags;
    uint16_t  _occupied;
};
struct x_class_data_bits_t {
    uintptr_t bits;
};
struct x_objc_class {
    // Class ISA;
    Class isa;
    Class superclass;
    struct x_cache_t cache;
    struct x_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person alloc];
        Class p_class = p.class;
        // 进行 x_objc_class 的强转
        struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
        NSLog(@"x_class is %@",x_class);      
}
  • 打印结果
x_class is Person
  • 说明这样是可以底层源码更进行 NSLog 输出的,我们继续往下走。
  • 我们的目的是要打印 x_cache_t 里的 buckets ,可现在x_cache_t 里没有,我想能不能在 x_cache_t 加一个x_bucket_t试试呢,我们把_bucketsAndMaybeMask 替换成 struct x_bucket_t *_buckets;
struct x_cache_t {
    struct x_bucket_t *_buckets;
//    uintptr_t _bucketsAndMaybeMask;
    mask_t    _maybeMask;
    uint16_t  _flags;
    uint16_t  _occupied;
};
  Class p_class = p.class;
  [p readBook1]; //
 // 进行 x_objc_class 的强转
 struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
 for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
      struct x_bucket_t bucket = x_class->cache._buckets[I];
      NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
 }
   NSLog(@"_occupied is %hu,_maybeMask is %u",x_class->cache._occupied,x_class->cache._maybeMask);

  • 打印输出结果,我们发现 _bucketsAndMaybeMask 替换成 struct x_bucket_t *_buckets 可以输出结果打印 selimp
  • 已经成功找到 selreadBook1
 sel is (null), imp is 0x0f
 sel is (null), imp is 0x0f
 sel is readBook1, imp is 0xbcb0f
_occupied is 1,_maybeMask is 3 
  • 我们继续测试多打印几个方法调用,看看 cache_buckets会有什么变化
  Class p_class = p.class;
  [p readBook1]; //
  [p readBook2]; //
  [p readBook3]; //
 // 进行 x_objc_class 的强转
 struct x_objc_class *x_class = (__bridge struct x_objc_class *)p_class;
 for (mask_t i = 0 ; i<x_class->cache._maybeMask; i++) {
      struct x_bucket_t bucket = x_class->cache._buckets[I];
      NSLog(@"sel is %@, imp is %pf",NSStringFromSelector(bucket._sel),bucket._imp);
 }
 >>> readBook1
 >>> readBook2
 >>> readBook3
 sel is (null), imp is 0x0f // 1
 sel is (null), imp is 0x0f // 2
 sel is (null), imp is 0x0f // 3
 sel is (null), imp is 0x0f // 4
 sel is (null), imp is 0x0f // 5
 sel is (null), imp is 0x0f // 6 
 sel is readBook3, imp is 0xbc50f // 7
_occupied is 1,_maybeMask is 7

疑问1:为什么我调用了 3 个 方法,buckets 里的 maybeMask 都变成7个了呢?
疑问2:readBook2,readBook3 为什么没有呢?

  • 我们只调用 readBook1readBook2 看看输出如何
sel is readBook2, imp is 0xbc78f
sel is (null), imp is 0x0f
sel is readBook1, imp is 0xbc08f
_occupied is 2,_maybeMask is 3
  • 我们可以看到如果是打印 2个方法 occupied2, 但 maybeMask3 没有增加, 打印 1个方法, occupied1, maybeMask 还是 3,也没有增加。
  • 我们带着上面的机个疑问,去分析下cache底层源码到底是如何实现的。
  • 分析前先要思考下我们在调用 方法的时候是在从 cache把,那到底是怎么进去的呢?
struct cache_t { 
      ...
      void insert(SEL sel, IMP imp, id receiver);
      ...
}
  • 我源码里找到了 insert 插入 = 把,我们进入 insert 方法里继续分析。
void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    // ... 省略部分代码
    // Use the cache as-is if until we exceed our expected fill ratio.
    mask_t newOccupied = occupied() + 1; // occupied 默认 0 + 1
    unsigned oldCapacity = capacity(), capacity = oldCapacity;
    // 先从这里开始分析
    if (slowpath(isConstantEmptyCache())) {
        // Cache is read-only. Replace it.
        if (!capacity) capacity = INIT_CACHE_SIZE; // 4
        reallocate(oldCapacity, capacity, /* freeOld */false);
    }
    else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) { // <=  capacity * 3/4
        // Cache is less than 3/4 or 7/8 full. Use it as-is.
    }
#if CACHE_ALLOW_FULL_UTILIZATION // 1
    else if (capacity <= FULL_UTILIZATION_CACHE_SIZE && newOccupied + CACHE_END_MARKER <= capacity) { 
        // Allow 100% cache utilization for small buckets. Use it as-is.
    }
#endif
    else { // newOccupied +1 >  capacity * 3/4 会进来   
       // 4*2 = 8
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);
    }

    bucket_t *b = buckets();
    mask_t m = capacity - 1; // 4-1=3
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

   
  //. 通过 do while 寻找合适的下标操作
    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));

}
  • isConstantEmptyCache 判断是否有缓存,没缓存直接进入 reallocate 方法开辟内存
ALWAYS_INLINE
/*
  oldCapacity = 0
  newCapacity = 4
  freeOld = false
**/
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
{
    bucket_t *oldBuckets = buckets();
    bucket_t *newBuckets = allocateBuckets(newCapacity);
    // ... 省略部分代码 
    // newCapacity = 4
    setBucketsAndMask(newBuckets, newCapacity - 1);
  
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity); // 回收清空
    }
}
  • (3 = newOccupied +1) = {4 > 3} = (4 = capacity * 3/4 ) 时会进入else 这个方法里。这里的3/4 只针对__arm__|| __x86_64__ || __i386__

  • capacity = capacity ? capacity * 2 -> capacity = 8 -> reallocate(oldCapacity, capacity, true) -> collect_free(oldBuckets, oldCapacity);

  • capacity = 8 -> mask_t m = capacity - 1 = 7 就能解释 疑问1:为什么我调用了 3 个 方法,buckets 里的 maybeMask 都变成7个了

  • 调用3 个 methods =>occupied = 2 -> else 代码 -> freeOld = true -> 重新梳理缓存 , 这样就能解释疑问2:readBook2,readBook3 没有的原因了。

  • cache 分析图如下


总结

  • cache 里的 occupied + 2> capacity * 3/4, 就会进行一次扩容,并把原有的旧方法都清空。而capacity 无值的时默认4, 扩容后的 capacity = 8,扩容的算法就是当前 capacity * 2。 而maybeMask = capacity - 1

知识点补充

  • 脱离源码进行小规模底层分析到底有什么好处呢?
    1:解决了有时源码无法调试的问题。
    2:解决了lldb 操作麻烦的问题。
    3:小规模取样 ,让你研究的东西 更加的简单和清晰。

ps: 真机架构 - arm64
模拟器 - i386
Mac - x86_64
_LP64 -> Unix 和 Unix类系统(Linux,Mac OS X)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容