iOS 底层原理之 cache结构分析(下)

上篇中我们着重分析了类中cache的方法缓存的插入方式,讲到了buckets(桶),那么本篇就整体对cache的整个流程做一下总结

一.cache_t的成员变量

首先看下源码

    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
    };

通过上篇我们脱离源码分析得出:

1._occupied 是记录当前插入存储的新方法数量,核心代码如下

if (fastpath(b[i].sel() == 0)) {
   incrementOccupied();//_occupied++;  说白了每次插入一个新的bucket_t  _occupied++
   b[i].set<Atomic, Encoded>(b, sel, imp, cls());
   return;
}

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

每次在给新的buckets进行set时,就会+1

2._maybeMask 当前可存放的最大数量,核心代码如下

void cache_t::insert(SEL sel, IMP imp, id receiver){
。。。省略
	reallocate(oldCapacity, capacity, true);
。。。省略
}

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);
    //设置BucketsAndMask  插入空的容器(桶子)
    setBucketsAndMask(newBuckets, newCapacity - 1);
    //是否清空原来内存
    if (freeOld) {
        collect_free(oldBuckets, oldCapacity);
    }
}

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask){
....省略
	_maybeMask.store(newMask, memory_order_release);
....省略
}

可以看到_maybeMask就是扩容后-1

3._bucketsAndMaybeMask 是buckets的首地址,后续获取其他buckets,需要内存平移就是从_bucketsAndMaybeMask开始

示意图:

在这里插入图片描述

核心代码如下:

//注释也有说明:bucketsAndMaybeMask是一个低48位的buckets指针
// _bucketsAndMaybeMask is a buckets_t pointer in the low 48 bits

struct bucket_t *buckets() const;

struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);(这里是做数据强转,强转成指针)
}

我们也可以自己测试,代码如下

LGPerson *p  = [LGPerson alloc];
Class pClass = [LGPerson class];
[p say1];

开始lldb调试

2021-06-28 12:26:46.716115+0800 KCObjcBuild[9000:147161] LGPerson say : -[LGPerson say1]
(lldb) p/x pClass
(Class) $0 = 0x00000001000045e0 LGPerson
(lldb) p (cache_t *)0x00000001000045f0
(cache_t *) $1 = 0x00000001000045f0
(lldb) p *$1
(cache_t) $2 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic<unsigned long> = {
      Value = 4302932400//_bucketsAndMaybeMask的value值
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic<unsigned int> = {
          Value = 3
        }
      }
      _flags = 32808
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic<preopt_cache_t *> = {
        Value = 0x0001802800000003
      }
    }
  }
}
(lldb) p/x 4302932400  //强转16进制结果和bucket_t * 一样
(long) $3 = 0x00000001007989b0
(lldb) p $2.buckets()
(bucket_t *) $4 = 0x00000001007989b0

通过上面的调试也说明了一点,为什么_maybeMask为最大容量-1,因为需要留一块内存存储(bucket_t *)

二.内存平移获取buckets

还是延续上面,继续LLLD尝试下内存平移

(lldb) p $2.buckets()[1]
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p $2.buckets()+1
(bucket_t *) $6 = 0x00000001007989c0
(lldb) p $2.buckets()+2
(bucket_t *) $7 = 0x00000001007989d0
(lldb) p $2.buckets()+3
(bucket_t *) $8 = 0x00000001007989e0
(lldb) p *$6  //和p $2.buckets()[1] 是一样的
(bucket_t) $10 = {
  _sel = {
    std::__1::atomic<objc_selector *> = (null) {
      Value = (null)
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 0
    }
  }
}
(lldb) p *$7
(bucket_t) $11 = {
  _sel = {
    std::__1::atomic<objc_selector *> = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic<unsigned long> = {
      Value = 32640
    }
  }
}

相信通过上面也完全可以证实,_bucketsAndMaybeMaskbuckets的首地址,通过它进行内存平移的说法了,然后再看下sel,imp

struct bucket_t {
private:
。。。省略
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
。。。省略
public:
。。。省略
    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

    }
    //设置bucket_t 
    void set(bucket_t *base, SEL newSel, IMP newImp, Class cls);
};

从上面源码也可以也可以看出怎么获取sel,imp,继续打印

(lldb) p $11.sel()
(SEL) $12 = "say1"  //say1出来了
(lldb) p $11.imp(nil, pClass) //第一个参数直接穿nil就行,从上面源码可以看出,返回imp时,有三种情况判断
(IMP) $15 = 0x0000000100003a60 (KCObjcBuild`-[LGPerson say1])
(lldb) 

三.问题的延伸

到目前为止,其实cache的内部结构已然很清晰了,包括插入的流程,可能会有人疑问:

  • 为什么插入的bucket_t到容量的3/4就开始扩容,这方面其实涉及到数学一些运算,简单来说,0.75是负载因子,这个时候的空间利用率最高,在搭配上哈希函数,可以最大程度的利用好内存,也保证了插入和读取效率
  • 什么时候开始插入bucket_t,在此之前又做了什么呢?那么接下来我继续断点调试

首先在cache_t::insert函数调用时断点

在这里插入图片描述
可以看到从源码分析的话,从IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
走到log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
然后就是cache_t::insert(SEL sel, IMP imp, id receiver),当然源码中也有更具体的描述

  • 在objc_cache.mm文件头部给出的Cache闭环流程
 * Cache readers (PC-checked by collecting_in_critical())
 * objc_msgSend*
 * cache_getImp
 *
 * Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
 * cache_t::copyCacheNolock    (caller must hold the lock)
 * cache_t::eraseNolock        (caller must hold the lock)
 * cache_t::collectNolock      (caller must hold the lock)
 * cache_t::insert             (acquires lock)
 * cache_t::destroy            (acquires lock)

从上面论述也可以看出,如果想要继续探索就得开始探索_objc_msgSend,没关系下期,新的篇章就要开启,冲冲冲!!!

PS:下一篇Runtime学习之objc_msgSend分析

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值