iOS-底层原理 06 类的设计原理 & cache的insert过程

389

类设计原理

类的本质和基本结构

  • 在开发过程中,很多类都要去继承NSObject。而类的本质是一个含有isa成员的objc_object结构体。isa是一个指向objc_class的指针。所以,objc_class的设计就很重要。
struct objc_class : objc_object {

    //Class ISA;               //指向元类的指针
    Class superclass;          //超类
    cache_t cache;             //缓存使用过的方法等信息,在下次使用时可以快速查找到,节约查找成本
    class_data_bits_t bits;    //用来查找存储在内存中的方法、属性等  
          ...
    }

结构体中各成员的存在意义

  • isa:指向类或者元类的指针。一个类型的类或元类在内存中只存在一个。一个类对应多个实例对象,对象执行某个方法,需要根据该isa去类和元类中查询。
  • superclass:用来获取父类的信息。在需要的时候会去这里查询方法地址,属性地址等。
  • cache:存储一些使用过的方法等信息(比如父类的方法)。避免多次查询消耗资源。
  • bits:本类的方法,属性等存储的地址。

cache在类中扮演的角色

  • 对象方法的执行,都是以信号进行传递的(objc_msgSend),执行过程中会去内存中找该对象的方法实现内存地址。然后才可以执行。
  • cache的存在意义就是把使用过的方法地址缓存起来。减少查找内存的长度以及复杂度。

cache的源码探究

cache_t的结构

cache是一个cache_t类型的结构体,查看源码如下所示。

struct cache_t {
     
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 8个内存
    union {
        struct {
            //当前的缓存区count,第一次开辟是3
            explicit_atomic<mask_t>    _maybeMask; // 4  
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2 
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8 
    };
 ...
 struct bucket_t *buckets() const; //查
 void insert(SEL sel, IMP imp, id receiver); //插入缓存的方法
 ...

  • _bucketsAndMaybeMask:当前存放的是bucketsmaybeMask的信息。通过往高位平移buckets的内存地址并且把maybeMask放在了地位来实现存储。
  • _maybeMask:当前cache的可存储的buckets数量,默认是0
  • _originalPreoptCache:执行_occupied++,_occupied默认是0,每次有方法的插入都会被执行,本质上就是占位+1
  • buckets():可以获取到缓存中的方法列表
  • insert:插入新的方法到缓存中

2251862-01f4cba6213b80d7.png

cache_t 实现缓存的过程

在执行msg_msgSend后会先去查询是否有缓存,没有会执行insert方法

截屏2021-06-30 下午4.28.42.png

insert方法的实现

  • 首次走类的方法时没有旧的缓存。默认创建4个容量的存储空间。

截屏2021-06-27 上午2.46.51.png

  • 缓存时会判断缓存的容量是否达到了3/4。如果达到了会新创建一个扩容后的内存地址去存储新的方法。并且清除旧的缓存。(为什么要清空oldBuckets,而不是空间扩容,然后在后面附加新的缓存呢?因为已经创建的内存无法更改,即使把旧的缓存取出来,有涉及到了遍历等操作,不如直接新创建一个内存空间,删除旧的缓存。)

截屏2021-06-27 上午2.47.56.png

  • 如果bucket为空的,就把selimp写入到该bucket

截屏2021-06-27 上午2.48.54.png

LLDB输出验证

需要验证的代码

@interface HTPerson : NSObject

- (void)playGame_wangZhe;
- (void)playGame_chiJi;
- (void)playGame_CF;
- (void)playGame_CC;
- (void)playGame_Sleep;

@end

@implementation HTPerson

- (void)playGame_wangZhe{
    NSLog(@"%s",__func__);
}
- (void)playGame_chiJi{
    NSLog(@"%s",__func__);
}
- (void)playGame_CF{
    NSLog(@"%s",__func__);
}
- (void)playGame_CC{
    NSLog(@"%s",__func__);
}
- (void)playGame_Sleep{
    NSLog(@"%s",__func__);
}
@end

LLDB输出结果

  • 在方法执行前,缓存里没有内容。 18B20CFC-5111-4069-A638-626BD73BD6A1.png
  • 执行了2个方法后,缓存里生成了4个方法的缓存(2个系统方法) 67BC2E68-43EC-4342-9E8F-CBA272664F34.png
  • 执行 p $4.buckets()
  • 执行 p $5[1]。是取内存平移
  • 输出方法名称如下所示 () 截屏2021-06-27 下午4.46.22.png

防源代码查看方法缓存

模仿源代码,进行强制转换,来输出bucket。


typedef uint32_t mask_t;
//bucketsMask:掩码,用来通过_bucketsAndMaybeMask解析初buckets
static uintptr_t bucketsMask = ~0ul;

//bucket_t源码模仿
struct HTBucket_t {
    SEL _sel;
    IMP _imp;
};
struct HTCache_t {
    uintptr_t       _bucketsAndMaybeMask; // 8
    mask_t          _maybeMask; // 4
    uint16_t        _flags;  // 2
    uint16_t        _occupied; // 2
};
struct HTClass_data_bits_t {
    uintptr_t bits;
};
struct HTObjc_object {
    Class isa;
};
struct HTObjc_Class: HTObjc_object{
    Class superclass;
    HTCache_t cache;             // formerly cache pointer and vtable
    HTClass_data_bits_t bits;
};
int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson *p  = [HTPerson alloc];
        
        [p playGame_CC];
        
        [p playGame_Sleep];
//
//        [p playGame_wangZhe];
//
//        [p playGame_chiJi];
//
//        [p playGame_CF];
        
        struct HTObjc_Class *pClass = (HTObjc_Class *)HTPerson.class;
        
        //打印当前有多少个方法缓存与最大缓存数量
        NSLog(@"%u-%u",pClass->cache._occupied,pClass->cache._maybeMask);
        
        //通过_bucketsAndMaybeMask解析初buckets
        uintptr_t bucketAndM = pClass->cache._bucketsAndMaybeMask;
        HTBucket_t *bucket =  (HTBucket_t *)(bucketAndM & bucketsMask);
        
        //循环遍历打印缓存的sel与imp
        for (int i = 0; i < pClass->cache._maybeMask ; i++) {
            struct HTBucket_t b = *(bucket + i);
            NSLog(@"第%d位==%@-%p",i,NSStringFromSelector(b._sel),b._imp);
        }
        
    }
    return 0;
}

输出结果如下

_occupied==2个方法-最大缓存数_maybeMask==3
第0位==playGame_CC-0x7f38
第1位==playGame_Sleep-0x7ee8
第2位==(null)-0x0

执行3个方法的话,会删除旧的缓存然后创建一个可以存放7个方法的内存

_occupied==1个方法-最大缓存数_maybeMask==70位==(null)-0x01位==(null)-0x02位==(null)-0x03位==playGame_wangZhe-0x7fb04位==(null)-0x05位==(null)-0x06位==(null)-0x0