iOS OC類原理二

前言:

上一篇探索了屬性 成員變量 方法中是如何存儲的,即存儲在class_ro_t *ro中,上一篇中提到爲什麼在rw中也能打印相應的屬性 方法呢?

因爲rw中的屬性 方法在編譯期是沒有的,是在運行時從rocopy賦值到rw中。

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

    class_rw_t *data() { 
        return bits.data();
    }
    ···
}

的源碼中不難看出前兩個成員分別是isasuperclass,上一篇我們探索了屬性 成員變量 方法bits中的存儲,那麼cache_t cache中存儲的是什麼呢 ?

cache_t cache源碼:

struct cache_t {
    struct bucket_t *_buckets; // 8
    mask_t _mask;  // 4
    mask_t _occupied; // 4

public:
    struct bucket_t *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__
    MethodCacheIMP _imp;
    cache_key_t _key;
#else
    cache_key_t _key;
    MethodCacheIMP _imp;
#endif

public:
    inline cache_key_t key() const { return _key; }
    inline IMP imp() const { return (IMP)_imp; }
    inline void setKey(cache_key_t newKey) { _key = newKey; }
    inline void setImp(IMP newImp) { _imp = newImp; }

    void set(cache_key_t newKey, IMP newImp);
};

猜測:cache_t cache 中存儲的是方法的緩存。

1. cache_t cache LLDB 簡單分析

首先創建一個,代碼如下:

@interface LGPerson : NSObject{
    NSString *hobby;
}

@property (nonatomic, copy) NSString *name;

- (void)sayHello;

- (void)sayCode;

- (void)sayMaster;

- (void)sayNB;


+ (void)sayHappy;

@end

#import "LGPerson.h"

@implementation LGPerson

- (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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        Class pClass = [LGPerson class];
        // cache_t 爲什麼沒有 - 第一次
        [person sayHello];
        [person sayCode];
        [person sayNB]; 
    }
}

通過打印cache_t cache,發現方法緩存確實存在cache_t cache中(有時候系統可能會出現問題,打印出cache_t cache中的_key_imp爲空,多運行打印幾次就ok)。

2.cache_t cache 流程源碼分析

通過上面的打印查看了cache_t cache的緩存內容,接下來查看源碼分析一下cache_t cache的具體流程:

首先查看cache_t中容量mask_t capacity()的實現:

mask_t capacity()的實現:

mask_t cache_t::capacity() 
{
    return mask() ? mask()+1 : 0; 
}

mask_t cache_t::mask() 
{
    return _mask; 
}

在實現中對cache_t_mask進行+1,那麼什麼是調用對這個capacity()的呢?

通過搜索源碼查看在expand()方法中調用:

void cache_t::expand()  // 擴容
{
    cacheUpdateLock.assertLocked();
    
    uint32_t oldCapacity = capacity();
    uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;

    if ((uint32_t)(mask_t)newCapacity != newCapacity) {
        // mask overflow - can't grow further
        // fixme this wastes one bit of mask
        newCapacity = oldCapacity;
    }

    reallocate(oldCapacity, newCapacity);
}

擴容 expand()方法是在cache_fill_nolock方法中調用

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();

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

    // Make sure the entry wasn't added to the cache by some other thread 
    // before we grabbed the cacheUpdateLock.
    if (cache_getImp(cls, sel)) return;

    cache_t *cache = getCache(cls);
    cache_key_t key = getKey(sel);

    // Use the cache as-is if it is less than 3/4 full
    mask_t newOccupied = cache->occupied() + 1;
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // Cache is read-only. Replace it.
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        // Cache is too full. Expand it.
        cache->expand();
    }

    // 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.
    bucket_t *bucket = cache->find(key, receiver);
    if (bucket->key() == 0) cache->incrementOccupied();
    bucket->set(key, imp);
}

由此我們找到了這個方法調用的入口,接下來我們斷點調試分析一下這個詳細的流程:

cache_fill_nolock詳細流程:

cache_fill_nolock方法中:

1.先從 cache 中獲取 imp ,獲取到直接 return 
    if (cache_getImp(cls, sel)) return;
2.獲取 cache 和 key,第一次調用 sayHello 方法,cache 中_mask 和 _occupied 爲0

3. 對 cache 的 occupied + 1, 並獲取容量 capacity (此時爲0),
4. 判斷是否是 isConstantEmptyCache ,如果是 EmptyCache,
    調用cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE) 
    跳轉到第 7 步; 
    INIT_CACHE_SIZE 爲:1 << 2 = 4
    enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
    }

5. 判斷桶子 Buckets 的佔用量是否是 小於等於 容量的3/4
   即:newOccupied <= capacity / 4 * 3,小於3/4 直接跳轉到  第8步;

6. 當桶子 Buckets 的佔用量達到臨界點時,執行擴容 cache->expand();
   expand() 方法中:
   6.1 獲取 oldCapacity = capacity()  // 4;
       newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;  // 8
   6.2 reallocate(oldCapacity, newCapacity)
      即:reallocate(4, 8),跳轉到 第7步;

7. reallocate(mask_t oldCapacity, mask_t newCapacity) 傳入一個oldCapacity = 0,
   newCapacity = 1 << 2 = 4
   
   7.1 先獲取 freeOld 標識,是否釋放舊緩存,bool freeOld = canBeFreed()
       即:bool cache_t::canBeFreed()
          {
               return !isConstantEmptyCache();
          }
       此時,沒有緩存 cache, 所以 freeOld 爲false
       
   7.2 創建一個新的 bucket_t *newBuckets,開闢4個位置
   
   7.3 用新容量 newCapacity - 1,對創建的 newBuckets 進行設置
       setBucketsAndMask(newBuckets, newCapacity - 1);
       
   7.4 判斷 釋放標識 freeOld,
       true: 釋放舊的 oldBuckets 和 oldCapacity
             即:cache_collect_free(oldBuckets, oldCapacity);
       false:不執行
       此時 freeOld 爲 false
       
8. 從 cache 中根據 key 查找 合適 Buckets

   8.1 獲取 buckets,獲取 mask = 3 (sort) 
       bucket_t *b = buckets();
       mask_t m = mask();
   8.2 通過對 k (對 sel 哈希的到的 cache_key_t key = getKey(sel)) 和 m 進行哈希得到下標
       mask_t begin = cache_hash(k, m);
       static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
       {
            return (mask_t)(key & mask);
       }
       通過 sel 和 mask 的位運算,計算出一個合理的 begin,就是哈希的下標
       
   8.3 通過 begin, do...while 循環,查找 bucket_t。
       查找到就返回,查找不到返回 bad_cache
       
9. 桶子 bucket 查找到以後,可以佔用,對 _occupied++。

10. 把 key 和 imp 保存在桶子裏  bucket->set(key, imp)。

到此,cache_t cache中的緩存存儲流程分析完成。

簡單的總結,

1. 先查找緩存,緩存命中直接返回,
2. 當沒有緩存時,開闢新的緩存並初始化,然後查找桶子 _buckets,然後 _occupied ++,然後存儲 set(key, imp);

3. 當有緩存,並小於容量的3/4時,直接查找桶子 _buckets,然後 _occupied ++,然後存儲 set(key, imp);

4. 當有緩存,大於容量的3/4時,擴容到二倍,查找桶子 _buckets,然後 _occupied ++,然後存儲 set(key, imp);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章