ios 底层原理 05-- cache_t的流程图

761 阅读8分钟

回顾

之前的流程中ios 底层原理 03--isa 的走位图&类的结构探索 我们知道了类的基础结构是这样的 类由 isa superclass cache bits 组成 cache_t是结构体类型,有 2 个成员变量_bucketsAndMaybeMask和一个union联合体

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

LGPerson --> cache_t
 cache_t -->bucket_t
LGPerson :  isa
LGPerson :  superClass
LGPerson :  cache
LGPerson :  bits
class cache_t{
_buckets
_mask
_flags
_occupied
 
}
class bucket_t{
_sel
_imp
}
 

图片展示

cache_t的本质

在类的方法调用过程中,已知过程是通过SEL(方法编号)在内存中查找IMP(方法指针),为了使方法响应更加快速,效率更高,不需要每一次都去内存中把方法都遍历一遍,cache_t结构体出现了。cache_t将调用过的方法的SELIMP以及receiverbucket_t结构体方式存储在当前类结构中,以便后续方法的查找。(PS:sel跟 imp 的关系 SEL:方法编号 SEL 相当于书本的目录名称(第几页的大纲)IMP:函数指针地址 IMP 相当于书的页码(第几页地址))

查看 cache_t的源码

cache_t看到了buckets(),这个类似于class_data_bits_t里面的提供的methods(),都是通过方法获取值。

继续查看 buckets

源码如下

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_imp,cache里面缓存的应该是方法

cache_t的流程图

classDiagram

objc_class --> cache_t真机
objc_class --> cache_t其他
cache_t其他 -->bucket_非真机
cache_t真机 -->maskAndBuckets说明
cache_t真机 -->cache_t中的mask和buckets
cache_t真机 -->bucket_真机
objc_class :  Class isa
objc_class :  Class superClass
objc_class :  cache_t *cache
objc_class :  class_data_bits_t bits
class cache_t真机{
uintptr_t  _bucketsAndMaybeMask
mask_t    _maybeMask
uint16_t   _flags 
uint16_t  _occupied 
 capacity()
 bucket_t *buckets()
 mask_t occupied()
 void incrementOccupied()
  void setBucketsAndMask
   void reallocate
   void insert
}
class cache_t其他{
 struct bucket_t *buckets()
  mask_t mask
  unit16_t flags
  unit16_t occupied
 
}
class maskAndBuckets说明{
为了节省内存,读取方便mask和buckets存在一起
}
class bucket_非真机{
explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
}
class cache_t中的mask和buckets{
 maskShift = 48
 maskZeroBits = 4
 maxMask =((uintptr_t)(64 - maskShift)) - 1
 static constexpr
 (maskShift - maskZeroBits)) - 1
}
class bucket_真机{
explicit_atomic<SEL> _imp;
    explicit_atomic<uintptr_t> _sel;
}

通过代码验证

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

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, strong) NSString *hobby;

- (void)saySomething;

@end

NS_ASSUME_NONNULL_END

#import "LGPerson.h"

@implementation LGPerson

- (instancetype)init{
    if (self = [super init]) {
        self.name = @"Cooci";
    }
    return self;
}

- (void)saySomething{
    NSLog(@"%s",__func__);
}

@end
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        
        Class pClass = [LGPerson class];
        NSLog(@"%@",pClass);
    }
    return 0;
}

运行起来代码,并且通过 lldb 来进行调试

说明:LGPerson对象没有调用对象方法,buckets中没有缓存方法的数据

在lldb中调用对象方法 继续lldb调试

在 buckets()里面寻找加入的saySomething方法

image.png 总结:

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

脱离源码环境分析cache

在 上面我们分析过 cache 的源码,那么我们可以仿照着重新写他的源码 代码如下

#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>

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

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

struct kc_class_data_bits_t {
    uintptr_t bits;
};

// cache class
struct kc_objc_class {
    Class isa;
    Class superclass;
    struct kc_cache_t cache;             // formerly cache pointer and vtable
    struct kc_class_data_bits_t bits;
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *p  = [LGPerson alloc];
        Class pClass = p.class;  // objc_clas
        [p say1];
        [p say2];
        [p say3];
        [p say4];
        [p say1];
        [p say2];
//        [p say3];

        [pClass sayHappy];
        struct kc_objc_class *kc_class = (__bridge struct kc_objc_class *)(pClass);
        NSLog(@"%hu - %u",kc_class->cache._occupied,kc_class->cache._maybeMask);
        // 0 - 8136976 count
        // 1 - 3
        // 1: 源码无法调试
        // 2: LLDB
        // 3: 小规模取样
        
        // 底层原理
        // a: 1-3 -> 1 - 7
        // b: (null) - 0x0 方法去哪???
        // c: 2 - 7 + say4 - 0xb850 + 没有类方法
        // d: NSObject 父类
        
        for (mask_t i = 0; i<kc_class->cache._maybeMask; i++) {
            struct kc_bucket_t bucket = kc_class->cache._bukets[i];
            NSLog(@"%@ - %pf",NSStringFromSelector(bucket._sel),bucket._imp);
        }
        NSLog(@"Hello, World!");
    }
    return 0;
}

先只调用[p say1][p say2]

2021-07-14 16:16:30.205454+0800 003-cache_t脱离源码环境分析[19959:7458549] LGPerson say : -[LGPerson say1]
2021-07-14 16:16:30.205773+0800 003-cache_t脱离源码环境分析[19959:7458549] LGPerson say : -[LGPerson say2]
2021-07-14 16:16:30.205810+0800 003-cache_t脱离源码环境分析[19959:7458549] LGPerson say : +[LGPerson sayHappy]
2021-07-14 16:16:30.205830+0800 003-cache_t脱离源码环境分析[19959:7458549] cache._occupied2 - cache._maybeMask3
2021-07-14 16:16:30.205903+0800 003-cache_t脱离源码环境分析[19959:7458549] _selsay1 - _imp0xb850f
2021-07-14 16:16:30.205937+0800 003-cache_t脱离源码环境分析[19959:7458549] _selsay2 - _imp0xb820f
2021-07-14 16:16:30.205965+0800 003-cache_t脱离源码环境分析[19959:7458549] _sel(null) - _imp0x0f
2021-07-14 16:16:30.205992+0800 003-cache_t脱离源码环境分析[19959:7458549] Hello, World!

在调用 say3 跟 say4

2021-07-14 16:18:31.427671+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say1]
2021-07-14 16:18:31.428242+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say2]
2021-07-14 16:18:31.428296+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say3]
2021-07-14 16:18:31.428329+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : -[LGPerson say4]
2021-07-14 16:18:31.428366+0800 003-cache_t脱离源码环境分析[19990:7460225] LGPerson say : +[LGPerson sayHappy]
2021-07-14 16:18:31.428393+0800 003-cache_t脱离源码环境分析[19990:7460225] cache._occupied2 - cache._maybeMask7
2021-07-14 16:18:31.428497+0800 003-cache_t脱离源码环境分析[19990:7460225] _selsay4 - _imp0xb830f
2021-07-14 16:18:31.428547+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.428600+0800 003-cache_t脱离源码环境分析[19990:7460225] _selsay3 - _imp0xb800f
2021-07-14 16:18:31.428636+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.428670+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.434070+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.434127+0800 003-cache_t脱离源码环境分析[19990:7460225] _sel(null) - _imp0x0f
2021-07-14 16:18:31.434160+0800 003-cache_t脱离源码环境分析[19990:7460225] Hello, World!

可以看到say1say2方法消失了
cache存储的位置是乱序的比如say4在最前面,第二与第四是空
_occupied_maybeMask是什么
缓存方法是怎么插入到bucket中的。

cache_t重要方法分析

cache_t的方法缓存的入口insert(SEL sel, IMP imp, id receiver),里面有参数selimp;而且还有方法名insert,看看它的具体实现

总体流程:

  • occupied()获取当前所占的容量,其实就是告诉你缓存中有几个bucket
  • newOccupied = occupied() + 1,表示你是第几个进来缓存的
  • oldCapacity 目的是为了重新扩容的时候释放旧的内存
  • 只有第一次缓存方法的时,才会去开辟容量默认开辟容量是 capacity = INIT_CACHE_SIZE 即capacity = 4 就是4个bucket的内存大小
  • reallocate(oldCapacity, capacity, /* freeOld */false)开辟内存,freeOld变量控制是否释放旧的内存

reallocate方法探究

reallocate方法干了什么事情

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

allocateBuckets开辟内存方法

allocateBuckets方法主要做两件事:

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

setBucketsAndMask方法

在源码里面有 3 个setBucketsAndMask 分别对应不同架构下的不同实现,但是功能都是向_bucketsAndMaybeMask_maybeMask写入数据

collect_free

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

总结:

先计算当前所占的容量大小,然后开辟容量,当需要缓存的方法所占的容量小于总容量3/4时就会直接走缓存流程容量超过3/4,系统此时会进行两倍扩容,扩容的最大容量不会超过mask的最大值2^15 扩容的时候会进行一步重要的操作,开辟新的内存,释放回收旧的内存,此时的freeOld = true然后开始缓存方法

缓存方法

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

insert 流程图