IOS 底层原理之cache分析

前言

IOS 底层原理之对象的本质&isa关联类IOS底层原理之类结构分析 分别分析了isabits里面的成员变量还有superclasscache,今天就来探究下cache的底层原理。说实话内心觉着cache没什么好探索的不就是个缓存嘛。我承认我飘了,我向cache道歉,个人觉着cache的底层探索是比较复杂的,里面有许多苹果底层代码的设计思路

准备工作

cache 结构分析

首先探究下cache的类型cache_t,源码中查看cache_t具体类型发现底层也是结构体

cache_t 结构分析

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
   
    /*
     #if defined(__arm64__) && __LP64__
     #if TARGET_OS_OSX || TARGET_OS_SIMULATOR
     // __arm64__的模拟器
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
     #else
     //__arm64__的真机
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
     #endif
     #elif defined(__arm64__) && !__LP64__
     //32位 真机
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
     #else
     //macOS 模拟器
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
     #endif
     ******  中间是不同的架构之间的判断 主要是用来不同类型 mask 和 buckets 的掩码
    */
    
    public:
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);
    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    void insert(SEL sel, IMP imp, id receiver);
    
    // 下面是基本上都是其他的方法的方法
 
};
复制代码
  • _bucketsAndMaybeMask变量uintptr_t8字节isa_t中的bits类似,也是一个指针类型里面存放地址
  • 联合体里有一个结构体和一个结构体指针_originalPreoptCache
  • 结构体中有三个成员变量 _maybeMask_flags_occupied__LP64__指的是UnixUnix类系统(LinxmacOS
  • _originalPreoptCache和结构体是互斥的,_originalPreoptCache初始时候的缓存,现在探究类中的缓存,这个变量基本不会用到
  • cache_t提供了公用的方法去获取值,以及根据不同的架构系统去获取maskbuckets的掩码

cache_t看到了buckets(),这个类似于class_data_bits_t里面的提供的methods(),都是通过方法获取值。查看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
  ....
  //下面是方法省略
};
复制代码
  • bucket_t区分真机其它,但是变量没变都是_sel_imp只不过顺序不一样
  • bucket_t里面存的是_sel_impcache里面缓存的应该是方法

cache_t 整体结构图

image.png

lldb调试验证

首先创建LWPerson类,自定义一些实例方法,在main函数中创建LWPerson的实例化对象,然后进行lldb调试

image.png

  • cache的变量的地址,需要首地址偏移16字节0x10cache的地址首地址+0x10
  • cache_t中的方法buckets()指向的是一块内存的首地址,也是第一个bucket的地址
  • p/x $3.buckets()[indx]的方式打印内存中其余的bucket发现_selimp
  • LWPerson对象没有调用对象方法,buckets中没有缓存方法的数据

lldb中调用对象方法,[p sayHello]继续lldb调试

image.png

  • 调用sayHello后,_mayMaskoccupied被赋值,这两个变量应该和缓存是有关系
  • bucket_t结构提供了sel()imp(nil,pClass)方法
  • sayhello方法的selimp,存在bucket中,存在cache

总结

通过lldb调试,结合源码。cache中存的是方法,方法的selimp存在bucketlldb调试是比较麻烦的比如调用方法后,需要重新获取bukets(),不舒适,不丝滑。有没有一种比较丝滑的方法呢,那是必须有的嘛

代码转换测试

通过lldb调试和源码,基本弄清楚cache_t的结构。我们可以按照cache_t的代码结构模仿写一套,这样就不需要在源码环境下的通过lldb。如果需要调用方法,直接添加代码,重新运行就好,这是我们最熟悉的方式了。

typedef uint32_t mask_t;
struct lw_bucket_t {
    SEL _sel;
    IMP _imp;
};
struct lw_cache_t{
      struct lw_bucket_t * _buckets;
      mask_t      _maybeMask;
      uint16_t    _flags;
      uint16_t    _occupied;
};
 

struct lw_class_data_bits_t{
     uintptr_t bits;
};

 
struct lw_objc_class  {
    Class ISA;
    Class superclass;
    struct lw_cache_t cache;
    struct lw_class_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
    @autoreleasepool {
    LWPerson * p =  [LWPerson alloc];
    [p sayHello1];
    [p sayHello2];
    //[p sayHello3];
    //[p sayHello4];
    //[p sayHello5];
    Class lwClass  = [LWPerson class];
    struct lw_objc_class * lw_class = (__bridge struct lw_objc_class *)(lwClass);
    NSLog(@" - %hu  - %u",lw_class->cache._occupied,lw_class->cache._maybeMask);
    
    for (int i = 0; i < lw_class->cache._maybeMask; i++) {
        struct lw_bucket_t bucket =lw_class->cache._buckets[i];
        NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
      }  
  }
    return 0;
}
复制代码
2021-06-23 14:51:20.003332+0800 testClass[7899:291790] ---[LWPerson sayHello1]---
2021-06-23 14:51:20.003432+0800 testClass[7899:291790] ---[LWPerson sayHello2]---
2021-06-23 14:51:20.003516+0800 testClass[7899:291790]  - 2  - 3
2021-06-23 14:51:20.003603+0800 testClass[7899:291790] sayHello2 - 0x80b0f
2021-06-23 14:51:20.003688+0800 testClass[7899:291790] sayHello1 - 0x8360f
2021-06-23 14:51:20.003778+0800 testClass[7899:291790] (null) - 0x0f
复制代码
  • objc_classClass ISA是被注释的,因为objc_class是继承objc_object,她可以继承objc_objectClass ISA,自定义的结构体lw_objc_class要手动添加Class ISA,不然代码转换会转换错误
  • 自己定义的结构体越简便越好,只要能显示出主要的信息就可以

在添加sayHello3sayHello4sayHello5方法,看下打印结果

2021-06-23 14:53:45.514704+0800 testClass[7944:294241] ---[LWPerson sayHello1]---
2021-06-23 14:53:45.514817+0800 testClass[7944:294241] ---[LWPerson sayHello2]---
2021-06-23 14:53:45.514899+0800 testClass[7944:294241] ---[LWPerson sayHello3]---
2021-06-23 14:53:45.514982+0800 testClass[7944:294241] ---[LWPerson sayHello4]---
2021-06-23 14:53:45.515069+0800 testClass[7944:294241] ---[LWPerson sayHello5]---
2021-06-23 14:53:45.515161+0800 testClass[7944:294241]  - 3  - 7
2021-06-23 14:53:45.515235+0800 testClass[7944:294241] (null) - 0x0f
2021-06-23 14:53:45.515316+0800 testClass[7944:294241] sayHello3 - 0x180b8f
2021-06-23 14:53:45.515411+0800 testClass[7944:294241] (null) - 0x0f
2021-06-23 14:53:45.515525+0800 testClass[7944:294241] sayHello4 - 0x180e8f
2021-06-23 14:53:45.515610+0800 testClass[7944:294241] (null) - 0x0f
2021-06-23 14:53:45.515743+0800 testClass[7944:294241] sayHello5 - 0x180d8f
2021-06-23 14:53:45.515827+0800 testClass[7944:294241] (null) - 0x0f
复制代码

我们会产生下面几个疑问

  • _occupied_maybeMask是什么?怎么还在变化呢?
  • sayHello1sayHello2方法怎么消失了?是谁施了魔法吗?
  • cache存储的位置怎么是乱序的呢?比如sayHello2sayHello1前面,sayHello3前面的位置是空的

带着这些疑问继续探讨cache_t,下一步怎么走呢?想要知道_occupied_maybeMask是什么?只有去看源码,看看在什么地方赋值的。我们要缓存方法,首先就要是怎么把方法插入到buket中的。带着这个思路让我们遨游
cache_t源码中

cache_t源码探究

首先找到方法缓存的入口
image.png
insert(SEL sel, IMP imp, id receiver)里面有参数selimp,这不就是我们熟悉的方法嘛。而且还有方法名insert,看看它的具体实现,由于insert内的代码过多我们分步骤说明

计算当前所占容量

image.png

  • occupied()获取当前所占的容量,其实就是告诉你缓存中有几个bucket
  • newOccupied = occupied() + 1,表示你是第几个进来缓存的
  • oldCapacity 目的是为了重新扩容的时候释放旧的内存

开辟容量

image.png

  • 只有第一次缓存方法的时,才会去开辟容量默认开辟容量是 capacity = INIT_CACHE_SIZEcapacity = 4 就是4bucket的内存大小
  • reallocate(oldCapacity, capacity, /* freeOld */false)开辟内存,freeOld变量控制是否释放旧的内存

reallocate方法探究

image.png
reallocate 方法主要做三件事

  • allocateBuckets开辟内存
  • setBucketsAndMask设置maskbuckets的值
  • collect_free是否释放旧的内存,由freeOld控制

allocateBuckets方法探究

image.png

allocateBuckets 方法主要做两件事

  • calloc(bytesForCapacity(newCapacity), 1)开辟newCapacity * bucket_t 大小的内存
  • end->set将开辟内存的最后一个位置存入sel = 1imp = 第一个buket位置的地址

setBucketsAndMask方法探究

image.png

setBucketsAndMask主要根据不同的架构系统向_bucketsAndMaybeMask_maybeMask写入数据

collect_free方法探究

image.png

collect_free主要是清空数据,回收内存

setBucketsAndMask方法探究

容量不超过3/4

image.png

  • 当需要缓存的方法所占的容量总容量3/4是就会直接走缓存流程
  • 苹果的设计思想,探究了很多底层就会发现,苹果做什么事情都会留有余地。一方面可能为了日后的优化或者扩展,另一方面可能是为了安全,内存对齐也是这样

容量存满

image.png

  • 苹果提供变量,很人性化,如果你需要把缓存的容量存满,默认是不存满的
  • 个人建议不要存满,就按照默认的来,如果存满有可能出现其它的问题,很难去排查

容量超过3/4

image.png

  • 容量超过3/4,系统此时会进行两倍扩容,扩容的最大容量不会超过mask的最大值2^15
  • 扩容的时候会进行一步重要的操作,开辟新的内存,释放回收旧的内存,此时的freeOld = true

缓存方法

image.png

  • 首先拿到bucket()指向开辟这块内存首地址,也就是第一个bucket的地址,bucket()既不是数组也不是链表,只是一块连续的内存
  • hash函数根据缓存selmask,计算出hash下标。为什么需要mask呢?mask的实际作用是告诉系统你只能存前capacity - 1中的位置,比如capacity = 4时,缓存的方法只能存前面3个空位
  • 开始缓存,当前的位置没有数据,就缓存该方法。如果该位置有方法且和你的方法一样的,说明该方法缓存过了,直接return,如果存在hash冲突,下标一样,sel不一样,此时会进行再次hash,冲突解决继续缓存

cache_hashcache_next

image.png

cache_hash主要是生成hash下标,cache_next主要是解决hash冲突

缓存写入方法set

image.png

setselimp写入bucket,开始缓存方法

incrementOccupied

image.png

_occupied自动加1_occupied表示内存中已经存储缓存方法的的个数

cache_t 流程图后面在画

总结

cache_t 中各个变量的含义

  • _bucketsAndMaybeMask存取了bucketsmsak(真机),macOS或者模拟器存取了buckets
  • _maybeMask是指掩码数据,用于在哈希算法或者哈希冲突算法中哈希下标 _maybeMask = capacity -1
  • _occupied会随着缓存的个数增加,扩容是_occupied = 0
  • 数据丢失是因为扩容的时候旧的内存回收了数据全部清除
  • cache存储bucket的位置乱序,因为位置是hash根据你的selmask生成所以不固定

总结

探索的过程真的是痛并快乐着

补充

cache_tinset方法详解

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享