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流程
相關文章
相關標籤/搜索