Cache 分析

1.jpeg

文章开头我再次贴出objc 类的源码:

struct objc_class : objc_object {
  //省略开头代码

    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

   //下面省略了大量代码
}

我们前面学习的时候就发现了,我们的objc_class类里面主要是四个成员:ISAsuperclasscache_tclass_data_bits。我们也探索了其他三个,今天就来浅显地看看我们跳过的这个cache_t到底是个什么东西。

前面我们获取class_data_bits的时候是利用内存平移的方式得到的。那我们用同样的方法来获取cache_t,因为class_data_bits大小为16,则我们平移的时候对比之前的平移32字节只需要平移16字节了。

objc 源码LLDB打印调试

我们直接在objc 源码里创建一个ZYPerson类,来利用lldb打印(同之前的方法在NSLog后一行打一个断点)上代码:

#import 
#import "ZYPerson.h"
#import "ZYIoser.h"

//cache_t
// class_data_bits_t

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ZYPerson *p  = [ZYPerson alloc];
        Class pClass = [ZYPerson class];
        NSLog(@"%@",pClass);
    }
    return 0;
}
lldb) x/4gx pClass
0x1000086a0: 0x00000001000086c8 0x000000010036a140
0x1000086b0: 0x00000001003623a0 0x0000801800000000
(lldb) p/x 0x1000086a0+0x10
(long) $1 = 0x00000001000086b0
(lldb) p (cache_t *)0x00000001000086b0
(cache_t *) $2 = 0x00000001000086b0
(lldb) p *$2
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4298515360
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 0
        }
      }
      _flags = 32792
      _occupied = 0
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0000801800000000
      }
    }
  }
}

发现其中结构是一个_bucketsAndMaybeMask和一个联合体,联合体中有
_maybeMask_flags_occupied_originalPreoptCache。如下:

struct cache_t {
private:
    explicit_atomic _bucketsAndMaybeMask; // 8
    union {
        struct {
            explicit_atomic    _maybeMask; // 4
#if __LP64__
            uint16_t                   _flags;  // 2
#endif
            uint16_t                   _occupied; // 2
        };
        explicit_atomic _originalPreoptCache; // 8
    };
//省略下面大量代码

既然这个cache_t是缓存,那到底缓存的是什么呢?是属性呢?还是方法呢?简单分析如果是缓存属性那肯定有IMPSEL。下面我们就看看到底缓存了什么。那么我们到底该查看那个部分呢?是_bucketsAndMaybeMask还是_originalPreoptCache还是其他呢?我们回到源码去查看下它提供的功能方法,既然是缓存那肯定是针对某些东西有增删改查的操作。

我们跟踪cache_t方法源码往下看:

1.png
2.png

我们可以看到上图红框标出的地方都是在对bucket_tOccupiedmask进行一系列操作 并且在后面有一个insert方法。所以我们可以初步判断他的重心在bucket_t。我们跟踪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 _imp;
    explicit_atomic _sel;
#else
    explicit_atomic _sel;
    explicit_atomic _imp;
#endif
 //省略了下面大量代码。

我们可以理解为bucket就是一个桶子,里面装的主要是imp和sel.
那么我们理解的数据结构就是如下图:

3.png

接下来我们利用lldb来验证下 方法的存储是否是我们理解的这样。我们挨个把看到的取一遍如下:

(explicit_atomic) $4 = {
  std::__1::atomic = {
    Value = 4298515360
  }
}
(lldb) p $4.Value
error: :1:4: no member named 'Value' in 'explicit_atomic'
$4.Value
~~ ^
(lldb) p $3._maybeMask
(explicit_atomic) $5 = {
  std::__1::atomic = {
    Value = 0
  }
}
(lldb) p $5.Value
error: :1:4: no member named 'Value' in 'explicit_atomic'
$5.Value
~~ ^
(lldb) p $3._originalPreoptCache
(explicit_atomic) $6 = {
  std::__1::atomic = {
    Value = 0x0000801800000000
  }
}
(lldb) p $6.Value
error: :1:4: no member named 'Value' in 'explicit_atomic'
$6.Value
~~ ^
(lldb) 

我们发现里面的Value根本取不出来,这个时候我们只能返回源码去找找看是否有类似ro()data()之类的方法.

4.png

如上图我们在cache_t方法里找到一个buckets()的方法。我们来尝试一下。

(lldb)  p $3.buckets()
(bucket_t *) $7 = 0x00000001003623a0
(lldb) p *$7
(bucket_t) $8 = {
  _sel = {
    std::__1::atomic = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic = {
      Value = 0
    }
  }
}
(lldb) 

接续上面的lldb调试打印buckets发现什么都没有.但是我们可以知道这样取可以取。并且发现我们取到的_sel_imp的顺序和上面buckets结构体中else里的顺序是一样的。因为我们现在用的是Mac来调试的不是真机。但是我们打印出来的内容都是空的,这是因为我们创建代码的时候根本没有创建方法。cache_t我们已经知道是缓存方法的。所以我们加上方法调用。

上代码:
ZYPerson.h

#import 

NS_ASSUME_NONNULL_BEGIN

@interface ZYPerson : NSObject

@property (nonatomic, copy) NSString *hobby;

- (void)zyPersonSay1;
- (void)zyPersonSay2;
- (void)zyPersonSay3;
- (void)zyPersonSay4;
- (void)zyPersonSay5;
+ (void)zyPersonSayHappy;
@end

ZYPerson.m

#import "ZYPerson.h"

@implementation ZYPerson

- (instancetype)init{
    if (self == [super init]) {
        NSLog(@"ZYPerson 初始化: %@",self);
        return self;
    }
    return nil;
}

- (void)zyPersonSay1{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay2{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay3{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay4{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay5{
    NSLog(@"%s",__func__);
}
+ (void)zyPersonSayHappy
{
    NSLog(@"%s",__func__);
}
@end

main.m

#import 
#import "ZYPerson.h"
#import "ZYIoser.h"

//cache_t
// class_data_bits_t

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

        ZYPerson *p  = [ZYPerson alloc];
        Class pClass = [ZYPerson class];
        NSLog(@"%@",pClass);
    
        [p zyPersonSay1];  
    }
    return 0;
}

重新运行代码,在[p zyPersonSay1]; 后一行打上断点。

(lldb) x/4gx pClass
0x100008708: 0x0000000100008730 0x000000010036a140
0x100008718: 0x0000000101512a80 0x0001801800000003
(lldb) p/x 0x100008708+0x10
(long) $1 = 0x0000000100008718
(lldb) p (cache_t *)0x0000000100008718
(cache_t *) $2 = 0x0000000100008718
(lldb) p *$2
(cache_t) $3 = {
  _bucketsAndMaybeMask = {
    std::__1::atomic = {
      Value = 4317063808
    }
  }
   = {
     = {
      _maybeMask = {
        std::__1::atomic = {
          Value = 3
        }
      }
      _flags = 32792
      _occupied = 1
    }
    _originalPreoptCache = {
      std::__1::atomic = {
        Value = 0x0001801800000003
      }
    }
  }
}
(lldb) p $3.buckets()
(bucket_t *) $4 = 0x0000000101512a80
(lldb) p *$4
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic = "" {
      Value = ""
    }
  }
  _imp = {
    std::__1::atomic = {
      Value = 48136
    }
  }
}
(lldb) 

发现bucket_t_impValue有值了Value = 48136,而且我们看到_occupiedValue都有值了,之前是没有的。到这里我们思考一下,既然buckets是个复数的存在,那我们尝试取一下他的其他位置的值。利用内存平移的方式。如下:

(lldb) p $3.buckets()[1]
(bucket_t) $6 = {
  _sel = {
    std::__1::atomic = (null) {
      Value = nil
    }
  }
  _imp = {
    std::__1::atomic = {
      Value = 0
    }
  }
}

发现可以取,值不够在1号位上没有值。

到这里我们还是没有看到我们想要的的东西就是zyPersonSay1。那么我们回到bucket里去找找,看看有没有相应的方法根据这个获取到的value = 48136 来取到方法名称。

struct bucket_t {
//省略了前面的代码
public:
    static inline size_t offsetOfSel() { return offsetof(bucket_t, _sel); }
    inline SEL sel() const { return _sel.load(memory_order_relaxed); }

//省略了后面的代码
}

我们在中间部分找到了上面这两句代码,有一个SEL sel()。我们尝试用一下。

(lldb) p $5.sel()
(SEL) $10 = "zyPersonSay1"
(lldb) 

发现确实可以打印出来了 zyPerosnSay1方法。

那我到这里我们拿到了sel:$10 = "zyPersonSay1",还剩下一个imp。我们下面再次回到源码找方法。看能不能拿到imp

struct bucket_t {
//省略了前面的代码
    #if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
#define MAYBE_UNUSED_ISA
#else
#define MAYBE_UNUSED_ISA __attribute__((unused))
#endif
    inline IMP rawImp(MAYBE_UNUSED_ISA objc_class *cls) const {
        uintptr_t imp = _imp.load(memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        imp ^= (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
#else
#error Unknown method cache IMP encoding.
#endif
        return (IMP)imp;
    }

    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
    }
//省略了后面的代码
}

我们在找sel()方法的下方就看到了 上面这一段代码,但是我们发现这个方法需要传一个cls和另一个的值但是我们不知道第一个传什么我们就尝试传nil。那我们尝试传入我们的ZYPersonpClass

(lldb) p $4->imp(nil,pClass)
(IMP) $7 = 0x0000000100003b00 (KCObjcBuild`-[ZYPerson zyPersonSay1])
(lldb) 

这样我们就拿到了我们要的imp:$7 = 0x0000000100003b00

模拟底层源码结构利用代码直接NSLog打印获取

当我们只有一份不能编译objc源码的时候我们就没有办法去利用个第一种方法去利用lldb源码里打印调试。这时我们可以创建一个普通的工程。同样创建ZYPerson 类。设置属性方法等。然后我们对着这份不能编译的objc 源码来构造方法利用NSLog来打印。

ZYPerson.h

#import 

NS_ASSUME_NONNULL_BEGIN

@interface ZYPerson : NSObject
@property (nonatomic, copy) NSString *hobby;

- (void)zyPersonSay1;
- (void)zyPersonSay2;
- (void)zyPersonSay3;
- (void)zyPersonSay4;
- (void)zyPersonSay5;
@end
NS_ASSUME_NONNULL_END

ZYPerson.m

#import "ZYPerson.h"

@implementation ZYPerson
- (instancetype)init{
    if (self == [super init]) {
        NSLog(@"ZYPerson 初始化: %@",self);
        return self;
    }
    return nil;
}

- (void)zyPersonSay1{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay2{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay3{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay4{
    NSLog(@"%s",__func__);
}
- (void)zyPersonSay5{
    NSLog(@"%s",__func__);
}
@end

mian.m

#import 
#import "ZYPerson.h"

//cache_t
// class_data_bits_t

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

        ZYPerson *p  = [ZYPerson alloc];
        Class pClass = [ZYPerson class];
        NSLog(@"%@",pClass);
    
        [p zyPersonSay1];
        
    }
    return 0;
}

首先我们回到不能编译的objc源码,我们要的cache在类里即objc_class里那我们先模仿造一个objc_class的数据结构。

objc_class源码结构:

struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc 
};

把源码里的objc_class数据结构主体复制过来。(为了和系统的名字区分我们前缀用zy_)

改造后zy_objc_class结构:

struct zy_objc_class {
    Class isa;
    Class superclass;
    struct zy_cache_t cache;
    struct zy_class_data_bits_t bits;
};

这个时候cache_tclass_data_bits_t报错,我们就用上面同样的方法针对cache_t来造一个数据结构,因为explicit_atomic _bucketsAndMaybeMask找不到并且其实就是一个uintptr_t类型的泛型那就改造成uintptr_t _bucketsAndMaybeMask,因为我们的电脑必定是支持__LP64__
的所以我们可以把联合体只保留结构体部分(互斥,所以保留一个就好了)
cache_t 源码结构:

struct cache_t {
    explicit_atomic _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic _originalPreoptCache;
    };
};

第一步改造后的zy_cache_t

struct zy_cache_t {
    uintptr_t _bucketsAndMaybeMask;
    struct {
        mask_t                    _maybeMask;
#if __LP64__
        uint16_t                   _flags;
#endif
        uint16_t                   _occupied;
    };
};

这个时候我们可以发现里面和外面都是struct那我们干脆合并且我们发现mask_t找不到那我们看看mask_t是什么数据结构。

源码mask_t数据结构:

#if __LP64__
typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
#else
typedef uint16_t mask_t;
#endif

第二步改造后的zy_cache_t

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

struct zy_cache_t {
    uintptr_t                  _bucketsAndMaybeMask;
    mask_t                    _maybeMask;
    uint16_t                   _flags;
    uint16_t                   _occupied;
};

这个时候我们还剩下一个找不到的class_data_bits_t。我们继续造数据结构

源码class_data_bits_t

struct class_data_bits_t {
    uintptr_t bits;
};

改造后zy_class_data_bits_t

struct zy_class_data_bits_t {
    uintptr_t bits;
};

总体改造后的zy_objc_class

struct zy_objc_class {
    // Class ISA;
    Class superclass;
    struct zy_cache_t cache;
    struct zy_class_data_bits_t bits;
};

因为我们主要是获取zy_cache_t,我们也知道cache_t里我们主要是获取里面的buckets。所以我们还需要对zy_Cache_t进行进一步改造。

源码bucket_t

struct bucket_t {
    // 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 _imp;
    explicit_atomic _sel;
#else
    explicit_atomic _sel;
    explicit_atomic _imp;
#endif
}
 

改造后bucket_t

struct zy_bucket_t {
    // 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__
    IMP _imp;
    SEL _sel;
#else
    SEL _sel;
    IMP _imp;
#endif
};

至此,我们前期的准备工作已经完成。我们先来尝试一下,我们知道cache里有一个occupied还有一个maybeMask。我们先来尝试打印下这两个看代码:

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

        ZYPerson *p  = [ZYPerson alloc];
        Class pClass = [ZYPerson class];
        
        [p zyPersonSay1];
        
        struct zy_objc_class *zy_class = (__bridge struct zy_objc_class *)(pClass);

        NSLog(@"_occupied------%hu-----%u",zy_class->cache._occupied,zy_class->cache._maybeMask);
        
    }
    return 0;
}
2021-06-28 17:22:45.290892+0800 ZYProjectSix000[54264:4774709] -[ZYPerson zyPersonSay1]
2021-06-28 17:22:45.291334+0800 ZYProjectSix000[54264:4774709] _occupied------1-----3
Program ended with exit code: 0

发现是真的可以哦。那我们下面就尝试获取我们需要的东西了。
我们for循环来获取bucket。但是获取bucket我们不知道怎么获取,所以我们回到源码查看系统怎么获取的。

struct cache_t {
   //省略前面大量代码
   unsigned capacity() const;
   struct bucket_t *buckets() const;
   Class cls() const;
 //省略后面大量代码
}

跟踪进去:

struct bucket_t *cache_t::buckets() const
{
   uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
   return (bucket_t *)(addr & bucketsMask);
}

发现是从_bucketsAndMaybeMaskload获取的,这个我们还是没法处理。那我们就直接改变cache_t结构里面的_bucketsAndMaybeMask ,改造后:

struct zy_cache_t {
   struct zy_bucket_t *     _bukets;
   mask_t                   _maybeMask;
   uint16_t                   _flags;
   uint16_t                   _occupied;
};

接下来我们利用for循环来遍历我们的zy_cache_t来获取_buket

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

       ZYPerson *p  = [ZYPerson alloc];
       Class pClass = [ZYPerson class];
       
       [p zyPersonSay1];
       [p zyPersonSay2];
       [p zyPersonSay3];
       struct zy_objc_class *zy_class = (__bridge struct zy_objc_class *)(pClass);

       NSLog(@"_occupied------%hu-----%u",zy_class->cache._occupied,zy_class->cache._maybeMask);
       //zyPersonSay1------1-----3
       
       for (mask_t i = 0; i < zy_class->cache._maybeMask; i++) {
           
           struct zy_bucket_t bucket = zy_class->cache._bukets[I];
           NSLog(@"zy_bucket_t==== %@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
       }
       
   }
   return 0;
}
2021-06-28 18:18:58.181204+0800 ZYProjectSix000[55066:4808195] -[ZYPerson zyPersonSay1]
2021-06-28 18:18:58.181646+0800 ZYProjectSix000[55066:4808195] -[ZYPerson zyPersonSay2]
2021-06-28 18:18:58.181709+0800 ZYProjectSix000[55066:4808195] -[ZYPerson zyPersonSay3]
2021-06-28 18:18:58.181747+0800 ZYProjectSix000[55066:4808195] _occupied------1-----7
2021-06-28 18:18:58.181789+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.181871+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.181903+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.181926+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.182029+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== zyPersonSay3 - 0xbe70
2021-06-28 18:18:58.182091+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.182132+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0

可以看到确实可以打印出来我们调用的方法。这种方式的调试打印有以下几个优点:

1,源码无法调试,找到替方法。
2,解决了LLDB出现错误就需要重新写一遍lldb命令从头再来一边的苦恼。
3,这种还实现了小规模取样,从而帮助我们更清晰分析源码。

Cache_t原理探索

从上面的打印我们看出第一次尝试打印的时候打印出来的_occupied_maybeMask1-----3,而第二次加入了[p zyPersonSay2]; [p zyPersonSay3];之后就变成了1-----7。并且只打印了zyPersonSay3一个方法,那其他的方法哪里去了?到这里我们不妨尝试下 加入一个类方法:

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

       ZYPerson *p  = [ZYPerson alloc];
       Class pClass = [ZYPerson class];
       
       [p zyPersonSay1];
       [p zyPersonSay2];
       [p zyPersonSay3];
       [ZYPerson zyPersonSayHappy];
       
       struct zy_objc_class *zy_class = (__bridge struct zy_objc_class *)(pClass);

       NSLog(@"_occupied------%hu-----%u",zy_class->cache._occupied,zy_class->cache._maybeMask);
       //zyPersonSay1------1-----3
       
       for (mask_t i = 0; i < zy_class->cache._maybeMask; i++) {
           struct zy_bucket_t bucket = zy_class->cache._bukets[I];
           NSLog(@"zy_bucket_t==== %@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
       }

   }
   return 0;
}
2021-06-28 18:18:58.181204+0800 ZYProjectSix000[55066:4808195] -[ZYPerson zyPersonSay1]
2021-06-28 18:18:58.181646+0800 ZYProjectSix000[55066:4808195] -[ZYPerson zyPersonSay2]
2021-06-28 18:18:58.181709+0800 ZYProjectSix000[55066:4808195] -[ZYPerson zyPersonSay3]
2021-06-28 18:18:58.181747+0800 ZYProjectSix000[55066:4808195] zyPersonSay1------1-----7
2021-06-28 18:18:58.181789+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.181871+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.181903+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.181926+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.182029+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== zyPersonSay3 - 0xbe70
2021-06-28 18:18:58.182091+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0
2021-06-28 18:18:58.182132+0800 ZYProjectSix000[55066:4808195] zy_bucket_t==== (null) - 0x0

发现还是没有。我们再加一个zyPersonSay4,打印:

2021-06-28 18:41:29.779431+0800 ZYProjectSix000[55204:4824980] _occupied------2-----7
2021-06-28 18:41:29.779461+0800 ZYProjectSix000[55204:4824980] zy_bucket_t==== (null) - 0x0
2021-06-28 18:41:29.779535+0800 ZYProjectSix000[55204:4824980] zy_bucket_t==== zyPersonSay4 - 0xbe70
2021-06-28 18:41:29.779575+0800 ZYProjectSix000[55204:4824980] zy_bucket_t==== (null) - 0x0
2021-06-28 18:41:29.779613+0800 ZYProjectSix000[55204:4824980] zy_bucket_t==== (null) - 0x0
2021-06-28 18:41:29.779656+0800 ZYProjectSix000[55204:4824980] zy_bucket_t==== zyPersonSay3 - 0xb9a0
2021-06-28 18:41:29.779748+0800 ZYProjectSix000[55204:4824980] zy_bucket_t==== (null) - 0x0
2021-06-28 18:41:29.779810+0800 ZYProjectSix000[55204:4824980] zy_bucket_t==== (null) - 0x0

发现只有一个zyPersonSay3zyPersonSay4。但是_occupiedmaybeMask变成了2-7

现在我们把所有方法屏蔽,只调用一个init方法。

2021-06-28 18:50:38.244319+0800 ZYProjectSix000[55308:4832732] ZYPerson 初始化: 
2021-06-28 18:50:38.244788+0800 ZYProjectSix000[55308:4832732] +[ZYPerson zyPersonSayHappy]
2021-06-28 18:50:38.244820+0800 ZYProjectSix000[55308:4832732] _occupied------1-----3
2021-06-28 18:50:38.244875+0800 ZYProjectSix000[55308:4832732] zy_bucket_t==== init - 0x7ffe204a5d5df
2021-06-28 18:50:38.244901+0800 ZYProjectSix000[55308:4832732] zy_bucket_t==== (null) - 0x0
2021-06-28 18:50:38.244923+0800 ZYProjectSix000[55308:4832732] zy_bucket_t==== (null) - 0x0

我们发现这个init方法本来是父类NSObject的方法。但是这里也有打印。而且我们看到调用自己的方法的时候有缓存方法,但是并不是所有调用的方法。而且打印的_occupiedmask并不是我们想的那样直接递增的。这个规则是什么?接下来我们回到objc源码 探究下:

struct cache_t {
private:
   explicit_atomic _bucketsAndMaybeMask;
   union {
       struct {
           explicit_atomic    _maybeMask;
#if __LP64__
           uint16_t                   _flags;
#endif
           uint16_t                   _occupied;
       };
       explicit_atomic _originalPreoptCache;
   };
//省略部分代码
}

从上面的机构我们可以看到,_bucketsAndMaybeMask,其实存的就是bucketsmaybeMask两个数据。之前我们不论是lldb调试 打印还是 自己造数据结构打印都是一个从缓存cache_t取/读的过程,那要搞清楚它读出来的东西为什么是这样的,我们就必须要搞清楚它存/写的过程。

其实在上面的代码中我们见过一个insert方法。如下:

struct cache_t {
//省略前面部分代码
   void insert(SEL sel, IMP imp, id receiver);
   void copyCacheNolock(objc_imp_cache_entry *buffer, int len);
   void destroy();
   void eraseNolock(const char *func);

   static void init();
   static void collectNolock(bool collectALot);
   static size_t bytesForCapacity(uint32_t cap);
//省略后面部分代码
}

从这段代码我们看到它调用insert 方法 传入的参数是 selimpreceiver。我们跟踪进去看看:

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
   runtimeLock.assertLocked();

   // Never cache before +initialize is done
   if (slowpath(!cls()->isInitialized())) {
       return;
   }

   if (isConstantOptimizedCache()) {
       _objc_fatal("cache_t::insert() called with a preoptimized cache for %s",
                   cls()->nameForLogging());
   }

#if DEBUG_TASK_THREADS
   return _collecting_in_critical();
#else
#if CONFIG_USE_CACHE_LOCK
   mutex_locker_t lock(cacheUpdateLock);
#endif

   ASSERT(sel != 0 && cls()->isInitialized());

   // Use the cache as-is if until we exceed our expected fill ratio.
   mask_t newOccupied = occupied() + 1;
   unsigned oldCapacity = capacity(), capacity = oldCapacity;
   if (slowpath(isConstantEmptyCache())) {
       // Cache is read-only. Replace it.
       if (!capacity) capacity = INIT_CACHE_SIZE;
       reallocate(oldCapacity, capacity, /* freeOld */false);
   }
   else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
       // Cache is less than 3/4 or 7/8 full. Use it as-is.
   }
#if CACHE_ALLOW_FULL_UTILIZATION
   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 {
       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;
   mask_t begin = cache_hash(sel, m);
   mask_t i = begin;

   // Scan for the first unused slot and insert there.
   // There is guaranteed to be an empty slot.
   do {
       if (fastpath(b[i].sel() == 0)) {
           incrementOccupied();
           b[i].set(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));

   bad_cache(receiver, (SEL)sel);
#endif // !DEBUG_TASK_THREADS
}

我们看到有一个mask_t newOccupied = occupied() + 1,跟踪进去发现就是一个赋值返回。这里可以看到newOccupied第一次来其实就是个初始化过程结果为1。下面我们看到一个判断isConstantEmptyCache.是否是空的缓存,如果是则进入if。很明显第一次来会走这里。而if里的代码其实就是一个赋值capacity的过程。而枚举的INIT_CACHE_SIZE

/* Initial cache bucket count. INIT_CACHE_SIZE must be a power of two. */
enum {
#if CACHE_END_MARKER || (__arm64__ && !__LP64__)
   // When we have a cache end marker it fills a bucket slot, so having a
   // initial cache size of 2 buckets would not be efficient when one of the
   // slots is always filled with the end marker. So start with a cache size
   // 4 buckets.
   INIT_CACHE_SIZE_LOG2 = 2,
#else
   // Allow an initial bucket size of 2 buckets, since a large number of
   // classes, especially metaclasses, have very few imps, and we support
   // the ability to fill 100% of the cache before resizing.
   INIT_CACHE_SIZE_LOG2 = 1,
#endif
   INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
   MAX_CACHE_SIZE_LOG2  = 16,
   MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
   FULL_UTILIZATION_CACHE_SIZE_LOG2 = 3,
   FULL_UTILIZATION_CACHE_SIZE = (1 << FULL_UTILIZATION_CACHE_SIZE_LOG2),
};

INIT_CACHE_SIZEarm64架构下 等于1,并且左移两位就等于4。然后就走reallocate方法开辟空间。跟踪进去:

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

   setBucketsAndMask(newBuckets, newCapacity - 1);
   
   if (freeOld) {
       collect_free(oldBuckets, oldCapacity);
   }
}

发现里面就是开辟了两个桶子oldBucketsnewBuckets。然后就是setBucketsAndMask(newBuckets, newCapacity - 1)。这时候把新创建的newBuckets存进cache_t第一位成员的_bucketsAndMaybeMask 里面。

void cache_t::setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask)
{
   // objc_msgSend uses mask and buckets with no locks.
   // It is safe for objc_msgSend to see new buckets but old mask.
   // (It will get a cache miss but not overrun the buckets' bounds).
   // It is unsafe for objc_msgSend to see old buckets and new mask.
   // Therefore we write new buckets, wait a lot, then write new mask.
   // objc_msgSend reads mask first, then buckets.

#ifdef __arm__
   // ensure other threads see buckets contents before buckets pointer
   mega_barrier();

   _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_relaxed);

   // ensure other threads see new buckets before new mask
   mega_barrier();

   _maybeMask.store(newMask, memory_order_relaxed);
   _occupied = 0;
#elif __x86_64__ || i386
   // ensure other threads see buckets contents before buckets pointer
   _bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);

   // ensure other threads see new buckets before new mask
   _maybeMask.store(newMask, memory_order_release);
   _occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

就看到_bucketsAndMaybeMask.store((uintptr_t)newBuckets, memory_order_release);这句存储代码把newBuckets的地址指针强转uintptr_t后存在了_bucketsAndMaybeMask里。并且把_occupied = 0;因为现在还没存东西。所以回到insert方法里。接着往下走。

接下来我们看到创建了一个bucket

bucket_t *b = buckets();

跟踪进去发现是从刚才存buckets_bucketsAndMaybeMask里取出来。

struct bucket_t *cache_t::buckets() const
{
   uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
   return (bucket_t *)(addr & bucketsMask);
}

接着 将capacity-1,这个时候的capacity还是刚开始我们得到的4,减去了1之后等于3,赋值给了m.其实这里的m就是后期我们打印的mask
然后就是进行了一下cache_hash得到一个地址。

mask_t begin = cache_hash(sel, m);
mask_t i = begin;

跟踪下cache_hash:

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
   uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
   value ^= value >> 7;
#endif
   return (mask_t)(value & mask);
}

然后就开始循环遍历了,做do while处理。

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
//省略前面代码

// Scan for the first unused slot and insert there.
   // There is guaranteed to be an empty slot.
   do {
       if (fastpath(b[i].sel() == 0)) {
           incrementOccupied();
           b[i].set(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));

   bad_cache(receiver, (SEL)sel);
}

看下while条件cache_next

#if CACHE_END_MARKER
static inline mask_t cache_next(mask_t i, mask_t mask) {
   return (i+1) & mask;
}
#elif __arm64__
static inline mask_t cache_next(mask_t i, mask_t mask) {
   return i ? i-1 : mask;
}
#else
#error unexpected configuration
#endif

这里就是为了防止在插入的过程中查找的位置已经存在值了出现hash冲突所以进行了一次再hash。直到它不等于我们的开始位置。然后进行插入数据。

if (fastpath(b[i].sel() == 0)) {
           incrementOccupied();
           b[i].set(b, sel, imp, cls());
           return;
}

当第ibucketsel 不存在(即从来没有进来缓存过值)就进入if
然后就增加occupied

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

这个时候occupied = 1了。这就是为什么我们调用一次zyPersonSay1方法 时候 occupied == 1,_maybeMask == 3
然后就利用bucketset方法 插入bucketselimpcls()

如果不是第一次进来 就判断sel是否存在,存在就直接返回,不再存储。这就是一次完整的存储过程(主要讲第一次)。

下面我们来看看第二次进来的时候(occupied == 2,_maybeMask == 7):

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;
   unsigned oldCapacity = capacity(), capacity = oldCapacity;
   if (slowpath(isConstantEmptyCache())) {
       // Cache is read-only. Replace it.
       if (!capacity) capacity = INIT_CACHE_SIZE;
       reallocate(oldCapacity, capacity, /* freeOld */false);
   }
   else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {
       // Cache is less than 3/4 or 7/8 full. Use it as-is.
   }
#if CACHE_ALLOW_FULL_UTILIZATION
   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 {
       capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
       if (capacity > MAX_CACHE_SIZE) {
           capacity = MAX_CACHE_SIZE;
       }
       reallocate(oldCapacity, capacity, true);
   }
//省略后面代码
}

第一次缓存后occupied == 1,_maybeMask == 3。 这个时候

mask_t newOccupied = occupied() + 1;

newOccupied = 2,并且走else if判断。

else if (fastpath(newOccupied + CACHE_END_MARKER <= cache_fill_ratio(capacity))) {

查看判断条件中cache_fill_ratio:

static inline mask_t cache_fill_ratio(mask_t capacity) {
   return capacity * 3 / 4;
}

cache_fill_ratio就是3/4容积,这个时候在判断里我们得出的刚好是2+ 1 <= 4*3/4成立。

ps:这里用到了3/4容积扩容算法。很多数组和空间扩容方法里都用到这种扩容算法。

最后走到else里。

else {
       capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;
       if (capacity > MAX_CACHE_SIZE) {
           capacity = MAX_CACHE_SIZE;
       }
       reallocate(oldCapacity, capacity, true);
   }

这里直接把capacity *2 扩大了两倍。则这个时候capacity = 8
然后继续往下走去扩容了

reallocate(oldCapacity, capacity, true);

跟踪进去

ALWAYS_INLINE
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);

   setBucketsAndMask(newBuckets, newCapacity - 1);
   
   if (freeOld) {
       collect_free(oldBuckets, oldCapacity);
   }
}

发现这里的扩容并不是我们想的一样直接把第一创建的空间扩大2倍。而是根据新的newCapacity = 8创建一个新的newBuckets。并且获取之前的buckets : oldBuckets然后:

setBucketsAndMask(newBuckets, newCapacity - 1);

if (freeOld) {
       collect_free(oldBuckets, oldCapacity);
   }

保存新的newBuckets 并且把上次创建的buckets清空回收了,所以新的bucket里不存在第一次存的方法了。

然后回到insert方法接着往下走:

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
//省略前面代码
   bucket_t *b = buckets();
   mask_t m = capacity - 1;
   mask_t begin = cache_hash(sel, m);
   mask_t i = begin;

   // Scan for the first unused slot and insert there.
   // There is guaranteed to be an empty slot.
   do {
       if (fastpath(b[i].sel() == 0)) {
           incrementOccupied();
           b[i].set(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));

   bad_cache(receiver, (SEL)sel);
}

获取到刚创建的8个空间的新的newBuckets

bucket_t *b = buckets(); 

capacity-1得到新的mask = 7

mask_t m = capacity - 1;

然后重复第一次的hash算法重复判断是否存在sel 然后增加 occupied并且保存bucketbuckets

这也是为什么当我们调用方法到zyPersonSay4的时候 occupied == 2,_maybeMask == 7,因为扩容和算法导致了变化。

ps:疑问:为什么不把第一次存的方法复制到新扩容创建的内存中呢?原因是,这种类似数组平移的方法很消耗内存。也很浪费时间。并且这个方法的数量不定,当数量大了后效率会非常低。所以苹果会采用这种保存新的方法舍弃旧方法的方式来保证运行的速度和流畅。

总结: 首次buckets 只有4个内存,当我们调用方法到第三个方法即zyPersonSay3时候已经满足了3/4 buckets了,当第四个方法zyPersonSay4进来的时候先去判断了是否符合扩容条件,这个时候刚好符合,就先去创建了一个有8个内存的新的newbuckets,并且把之前存了三个方法的oldBuckets清空回收了。然后把第四个方法作为newbuckets的第一个方法插入保存。这时候我们读取 得到的就只有一个方法。并且得到的occupied == 2._maybeMask == 7。

补充:

苹果架构:
arm64 :真机
i386:模拟器
x86_64:Mac

所以我们有这样一张表格:

1.png

我们对照这张表格就知道我们在看源码的时候如何去根据那些架构的宏定义来选择代码进行分析。

如下表示支持Unix和Unix类的系统(LInux、Mac OS X)

#if __LP64__

如下:

#define CACHE_MASK_STORAGE_OUTLINED 1
#define CACHE_MASK_STORAGE_HIGH_16 2
#define CACHE_MASK_STORAGE_LOW_4 3
#define CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS 4

#if defined(__arm64__) && __LP64__
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif

至此,文章也告一段落了。但是这里的东西还只是个开始,后面还有很多东西需要我们去更细致的分析。

遇事不决,可问春风。站在巨人的肩膀上学习,如有疏忽或者错误的地方还请多多指教。谢谢!

你可能感兴趣的:(Cache 分析)