objc_class 中 cache 原理分析

  • 准备工做

    1. objc4-781可编译源码
    2. 建立一个类随意建立几个方法以下
    #import <Foundation/Foundation.h>
    
     NS_ASSUME_NONNULL_BEGIN
    
     @interface TDPerson : NSObject
     @property (nonatomic, copy) NSString *lgName;
     @property (nonatomic, strong) NSString *nickName;
     -(void)sayHello;
     - (void)sayCode;
     - (void)sayMaster;
     - (void)sayNB;
     + (void)sayHappy;
     @end
     NS_ASSUME_NONNULL_END
     
     #import "TDPerson.h"
    
     @implementation TDPerson
     - (void)sayHello{
         NSLog(@"LGPerson say : %s",__func__);
     }
    
     - (void)sayCode{
         NSLog(@"LGPerson say : %s",__func__);
     }
    
     - (void)sayMaster{
         NSLog(@"LGPerson say : %s",__func__);
     }
    
     - (void)sayNB{
         NSLog(@"LGPerson say : %s",__func__);
     }
    
     + (void)sayHappy{
         NSLog(@"LGPerson say : %s",__func__);
     }
     @end
    复制代码
  • 经过源码探索cache所包含的内容

    struct objc_class : objc_object {
     // Class ISA;
     Class superclass;
     cache_t cache;             // formerly cache pointer and vtable
     class_data_bits_t bits;
    }
    
    #define CACHE_MASK_STORAGE_OUTLINED 1
    #define CACHE_MASK_STORAGE_HIGH_16 2
    #define CACHE_MASK_STORAGE_LOW_4 3
    
    #if defined(__arm64__) && __LP64__  //真机64位
    #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
    #elif defined(__arm64__) && !__LP64__ //真32位
    #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
    #else   //模拟器/mac
    #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
    #endif
    
    struct cache_t {
    #if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_OUTLINED
       //模拟器/真机中的cache _buckets和_mask是分开的
       //explicit_atomic 原子性,保证增删改查时候的线程安全
       //_buckets  查看源码可知里面存储的是sel和imp
       explicit_atomic<struct bucket_t *> _buckets;
       explicit_atomic<mask_t> _mask;
    
       ...  //其余都是写掩码 省略
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
       //真机将_mask和_buckets写在一块儿为了节省空间
       explicit_atomic<uintptr_t> _maskAndBuckets;
       mask_t _mask_unused;
    
       ...  //其余都是写掩码 省略
    #elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
       // _maskAndBuckets stores the mask shift in the low 4 bits, and
       // the buckets pointer in the remainder of the value. The mask
       // shift is the value where (0xffff >> shift) produces the correct
       // mask. This is equal to 16 - log2(cache_size).
       explicit_atomic<uintptr_t> _maskAndBuckets;
       mask_t _mask_unused;
    
       ...  //其余都是写掩码 省略
    
    }
    
    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
    }
    复制代码
    1. sel:方法编号(其实就是方法名)
    2. imp:函数指针地址
  • 查找cache中的sel和imp

    1. 经过源码查找
    应为buckets内bucket是连续存储的因此能够经过指针加一的方式找到下一个bucket以下 2. 脱离源码查找
    对象在底层其实就是一个结构体,oc代码的结果 仍是要翻译成c/c++/汇编语言,因此干脆能够将底层结构体搬出来运行以下:
    #import <Foundation/Foundation.h>
     #import "TDPerson.h"
     #import <objc/runtime.h>
    
     typedef uint32_t mask_t;  // x86_64 & arm64 asm are less efficient with 16-bits
    
     struct td_bucket_t {
         SEL _sel;
         IMP _imp;
     };
    
     struct td_cache_t {
         struct td_bucket_t * _buckets;
         mask_t _mask;
         uint16_t _flags;
         uint16_t _occupied;
     };
    
     struct td_class_data_bits_t {
         uintptr_t bits;
     };
    
     struct td_objc_class {
         Class ISA;
         Class superclass;
         struct td_cache_t cache;             // formerly cache pointer and vtable
         struct td_class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
     };
    
     int main(int argc, const char * argv[]) {
         @autoreleasepool {
             TDPerson *p  = [TDPerson alloc];
             Class pClass = [TDPerson class];  // objc_clas
     //        [p say1];
     //        [p say2];
     //        [p say3];
             [p say4];
    
             struct td_objc_class *td_pClass = (__bridge struct td_objc_class *)(pClass);
             NSLog(@"%hu - %u",td_pClass->cache._occupied,td_pClass->cache._mask);
             for (mask_t i = 0; i<td_pClass->cache._mask; i++) {
                 // 打印获取的 bucket
                 struct td_bucket_t bucket = td_pClass->cache._buckets[i];
                 NSLog(@"%@ - %p",NSStringFromSelector(bucket._sel),bucket._imp);
             }
    
             NSLog(@"Hello, World!");
         }
         return 0;
     }
    复制代码
    看运行结果打印_occupied 和 _mask,_sel,_imp: 发现低啊用两个方法的时候打印cache里面的缓存没问题是两个方法,可是调用三个方法的时候就会出现问题,缓冲中只有第三个方法,还有一个点事_occupied和_mask分别是什么
  • 源码分析_occupied

    1. 首先从cache_t结构体里面看发现有以下方法
    2. 在进到incrementOccupied方法内查看 发现是一个occupied自增加函数 3. 全局搜索该函数找到以下位置(这里也能够冲方法调用开始一步一步往下跟同样能找到)发现是在cache_t insert方法内找到该方法调用,而后又是在插入bucket的时候调用一次,因此很明确occupied是bucket的数量
  • _mask是什么?

_mask是指掩码数据,用于在哈希算法或者哈希冲突算法中计算哈希下标,其中mask 等于capacity - 1c++

  • 上文中打印occupied数值问题

    上文的案例能够看到调用两个方法的时候打印occupied是2,没问题应为调用了两个方法,可是当调用第三个方法的时候就发现问题了,发现occupied打印的是1,按照上文所说应该是3啊,接下来分析一下插入缓存的核心流程insert原理
    1. insert源码分析
    ALWAYS_INLINE
    void cache_t::insert(Class cls, SEL sel, IMP imp, id receiver)
    {
    #if CONFIG_USE_CACHE_LOCK
       cacheUpdateLock.assertLocked();
    #else
       runtimeLock.assertLocked();
    #endif
    
       ASSERT(sel != 0 && cls->isInitialized());
    
       // Use the cache as-is if it is less than 3/4 full
       //第一步计算newOccupied  这里若果没有init和属性赋值(会有一个set方法)的状况下而且是第一次调用方法则occupied() = 0 newOccupied = 1
       mask_t newOccupied = occupied() + 1;
       //第二步获得当前容量
       unsigned oldCapacity = capacity(), capacity = oldCapacity;
       //slowpath小几率事件isConstantEmptyCache  buckets是空的状况下 建立缓存
       if (slowpath(isConstantEmptyCache())) {
           // Cache is read-only. Replace it.
           //INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2),  INIT_CACHE_SIZE_LOG2 =2
           //其实就是初始化容量是4
           if (!capacity) capacity = INIT_CACHE_SIZE;
           //开辟空间
           reallocate(oldCapacity, capacity, /* freeOld */false);
       }
       else if (fastpath(newOccupied + CACHE_END_MARKER <= capacity / 4 * 3)) { // 4  3 + 1 bucket cache_t
           // Cache is less than 3/4 full. Use it as-is.
           //若是newOccupied + CACHE_END_MARKER = newOccupied + 1的数量小于总容量的3/4的话不须要作处理
           //直接走下面的插入流程
       }
       else {
           //大于总容量的3/4则进行扩容(原来容量的两倍)
           capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 扩容两倍 4
           if (capacity > MAX_CACHE_SIZE) {
               capacity = MAX_CACHE_SIZE;
           }
           reallocate(oldCapacity, capacity, true);  // 内存 库容完毕
       }
    
       bucket_t *b = buckets();
       mask_t m = capacity - 1;
       //经过哈希计算bucket_t下标
       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 because the
       // minimum size is 4 and we resized at 3/4 full.
       do {
           if (fastpath(b[i].sel() == 0)) {
               //若是当前下面下面没有存储方法则插入到该位置而后返回
               incrementOccupied();
               b[i].set<Atomic, Encoded>(sel, imp, cls);
               return;
           }
           if (b[i].sel() == sel) {
               //若是当前位置下存储的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));
    
       cache_t::bad_cache(receiver, (SEL)sel, cls);
    }
    
    复制代码
    从这段代码能够知道方法插入计算下标的时候可能会出现哈希冲突,若是出现哈希冲突就须要从新计算方法的下标,因此在遍历buckets的时候有时候方法打印的顺序不必定就和方法调用的顺序一致注意:init和属性赋值也会插入cache,init自己就是一个方法,属性赋值会触发set方法因此也会存到cache 2. reallocate源码分析
    ALWAYS_INLINE
    void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld)
    {
       bucket_t *oldBuckets = buckets();  //获得旧的buckets
       bucket_t *newBuckets = allocateBuckets(newCapacity);  //建立一个新的buckets,此时是个临时的buckets
    
       // 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);
    
       //将临时的buckets存到缓存中
       setBucketsAndMask(newBuckets, newCapacity - 1);
    
       if (freeOld) {
           //释放老的bucket
           /**
            static void cache_collect_free(bucket_t *data, mask_t capacity)
            {
            #if CONFIG_USE_CACHE_LOCK
                cacheUpdateLock.assertLocked();
            #else
                runtimeLock.assertLocked();
            #endif
    
                if (PrintCaches) recordDeadCache(capacity);
    
                _garbage_make_room ();  // 建立垃圾回收空间
                garbage_byte_size += cache_t::bytesForCapacity(capacity);
                garbage_refs[garbage_count++] = data; //将传入的buckets向后添加
                cache_collect(false);  //开始释放
            }
            */
           cache_collect_free(oldBuckets, oldCapacity);
       }
    }
    复制代码
    从这里能够发现当须要扩容的时候会将之前存储的方法清理掉,因此上述调用到say3的时候你会发现say1和say2不见了 3. insert流程
相关文章
相关标签/搜索