iOS類結(jié)構(gòu):cache_t分析

一砰奕、cache_t 內(nèi)部結(jié)構(gòu)分析

1.1iOS類的結(jié)構(gòu)分析中康震,我們已經(jīng)分析過類(Class)的本質(zhì)是一個結(jié)構(gòu)體 燎含,結(jié)構(gòu)體內(nèi)部結(jié)構(gòu)如下 :

typedef struct objc_class *Class;
typedef struct objc_object *id;

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
    class_rw_t *data() const {
        return bits.data();
    }
    ...
}
  • Class ISA :指向關(guān)聯(lián)類 , 繼承自 objc_object 。 參考 isa底層結(jié)構(gòu)分析
  • Class superclass:父類指針 , 同樣參考上述文章中有詳細指向探索腿短。
  • cache_t cache , 方法緩存存儲數(shù)據(jù)結(jié)構(gòu)屏箍。
  • class_data_bits_t bits , bit 中存儲了屬性,方法等類的源數(shù)據(jù)答姥。

1.2iOS類的結(jié)構(gòu)分析中铣除,我們已經(jīng)分析過 cache_t 結(jié)構(gòu)體,分為以下四個部分:

struct cache_t {
    struct bucket_t * _buckets; // 緩存數(shù)組鹦付,即哈希桶
    mask_t _mask; // 緩存數(shù)組的容量臨界值,實際上是為了 capacity 服務(wù)
    uint16_t _flags; // 位置標記
    uint16_t _occupied; // 緩存數(shù)組中已緩存方法數(shù)量
    ...省略
}
  • _buckets:是 bucket_t 結(jié)構(gòu)體的數(shù)組,bucket_t 是用來存放方法編號 SEL 和函數(shù)指針 IMP 的择卦。
struct bucket_t {
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
}
  • _mask: mask_t m = capacity - 1; (capacity = MAX_CACHE_SIZE;)敲长,用作掩碼。因為這里緩存 Cache 的容量 Size 一直是2倍擴容的秉继,所以 MAX_CACHE_SIZE 是2的整數(shù)次冪祈噪,所以 mask 的二進制位 000011, 000111, 001111 )剛好可以用作 Hash取余數(shù)的掩碼。剛好保證相與后不超過緩存大小尚辑。
capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴容至兩倍
  • _flags: 位置標記
  • _occupied是當前已緩存的方法數(shù)量辑鲤。即數(shù)組中已使用了多少位置。

二杠茬、方法緩存原理探索

源碼如下:

@interface LGPerson : NSObject

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

#import <Foundation/Foundation.h>
#import "LGPerson.h"
#import <objc/runtime.h>


// cache_t
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        LGPerson *p  = [LGPerson alloc];
        Class pClass = [LGPerson class];

        [p sayHello];
        [p sayCode];
        [p sayMaster];
        [p sayNB];

        NSLog(@"%@",pClass);
    }
    return 0;
}

2.1 我們再sayHello方法前設(shè)置斷點月褥,LLDB調(diào)試 其中的 cache_t 的數(shù)據(jù)

因為在類結(jié)構(gòu)體中 cache_t 前面有 Class ISA指針Class superclass 父類指針 ,所以要偏移16位瓢喉。

(lldb) p/x pClass
(Class) $0 = 0x00000001000022a0 LGPerson
(lldb) p (cache_t *)0x00000001000022b0
(cache_t *) $1 = 0x00000001000022b0
(lldb) p *$1
(cache_t) $2 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x000000010032e420 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 0
  }
  _flags = 32804
  _occupied = 0
}

2.2 然后執(zhí)行一步 sayHello 方法宁赤,再次進行 LLDB調(diào)試 ,查看 cache_t 的數(shù)據(jù)

2020-09-17 22:37:33.187060+0800 KCObjc[34953:549295] LGPerson say : -[LGPerson sayHello]
(lldb) p *$1
(cache_t) $3 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x00000001006ad5f0 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 11936
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 1
}

2.3 走到這里栓票,大家應(yīng)該發(fā)現(xiàn) _buckets 决左、_mask_occupied 的變化了。其中_occupied 從0變?yōu)?佛猛,也證明了執(zhí)行完 sayHello 方法 之后惑芭,緩存方法數(shù)量 + 1 。接下來我們查看一下哈希桶 _buckets 的變化继找,哈希桶數(shù)據(jù)類型 struct bucket_t 我們點進去查看如下:

struct bucket_t {
public:
    inline SEL sel() const { return _sel.load(memory_order::memory_order_relaxed); }

    inline IMP imp(Class cls) const {
        uintptr_t imp = _imp.load(memory_order::memory_order_relaxed);
        if (!imp) return nil;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        SEL sel = _sel.load(memory_order::memory_order_relaxed);
        return (IMP)
            ptrauth_auth_and_resign((const void *)imp,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(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
    }
}

我們就可以查看 _bucketsSELIMP 信息遂跟。

(lldb) p $3.buckets()
(bucket_t *) $4 = 0x00000001006ad5f0
(lldb) p *$4
(bucket_t) $5 = {
  _sel = {
    std::__1::atomic<objc_selector *> = ""
  }
  _imp = {
    std::__1::atomic<unsigned long> = 11936
  }
}
(lldb) p $5.sel()
(SEL) $6 = "sayHello"
(lldb) p $5.imp(pClass)
(IMP) $7 = 0x0000000100000c00 (KCObjc`-[LGPerson sayHello])

然后我們也可以打開 MachOView 查看一下 sayHello 方法的 IMP 指針

MachOView

與我們 LLDB調(diào)試 結(jié)果不謀而合,完美~

2.4 接下來我們繼續(xù)執(zhí)行 sayMaster 方法sayNB 方法 码荔,進行 LLDB調(diào)試 漩勤,查看 cache_t 的數(shù)據(jù)

2020-09-17 23:12:37.095330+0800 KCObjc[34953:549295] LGPerson say : -[LGPerson sayCode]
(lldb) p *$1
(cache_t) $8 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x00000001006ad5f0 {
      _sel = {
        std::__1::atomic<objc_selector *> = ""
      }
      _imp = {
        std::__1::atomic<unsigned long> = 11936
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 3
  }
  _flags = 32804
  _occupied = 2
}
2020-09-17 23:12:59.163825+0800 KCObjc[34953:549295] LGPerson say : -[LGPerson sayMaster]
(lldb) p *$1
(cache_t) $9 = {
  _buckets = {
    std::__1::atomic<bucket_t *> = 0x0000000103b4c7d0 {
      _sel = {
        std::__1::atomic<objc_selector *> = (null)
      }
      _imp = {
        std::__1::atomic<unsigned long> = 0
      }
    }
  }
  _mask = {
    std::__1::atomic<unsigned int> = 7
  }
  _flags = 32804
  _occupied = 1
}

走到這里,我們發(fā)現(xiàn):
問題①. _occupied 由 2 變?yōu)榱?1 缩搅,緩存方法數(shù)量 _occupied 為什么會減少呢越败?
問題②. _mask 由 3 變?yōu)榱?7 ,至于 _mask 的變化硼瓣,大家可以能想到究飞,前面我們講過, _mask 是受緩存容量 CACHE SIZE 2 倍擴容的影響堂鲤。緩存容量 CACHE SIZE 由 4 變?yōu)榱?8 亿傅。
問題③. _buckets 里面的 SELIMP 消失了。

2.5 接下來瘟栖,我們來一探究竟葵擎。在 void incrementOccupied(); 方法中我們看到了 _occupied++;

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

2.6 然后我們在源碼中找一下半哟,什么地方執(zhí)行了 incrementOccupied(); 這個方法酬滤。驚喜來了,cache_t::insert() 方法中執(zhí)行了 incrementOccupied(); 這個方法寓涨。從名稱我們就可以發(fā)現(xiàn)盯串,這是向緩存插入的方法。

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
    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 <= capacity / 4 * 3)) { // 4  3 + 1 bucket cache_t
        // Cache is less than 3/4 full. Use it as-is.
    }
    else {
        capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴容兩倍 4
        if (capacity > MAX_CACHE_SIZE) {
            capacity = MAX_CACHE_SIZE;
        }
        reallocate(oldCapacity, capacity, true);  // 內(nèi)存 庫容完畢
    }

    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 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) {
            // 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);
}

2.6.1 接下來我們分析一下這個小概率事件 -> 初始化方法:
如果緩存為空戒良,則開辟緩存 INIT_CACHE_SIZE :4体捏。然后利用 reallocate() 方法 開辟空間。

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2),
    MAX_CACHE_SIZE_LOG2  = 16,
    MAX_CACHE_SIZE       = (1 << MAX_CACHE_SIZE_LOG2),
};
if (slowpath(isConstantEmptyCache())) { // 小概率事件 -> 初始化方法
    // Cache is read-only. Replace it.
    if (!capacity) capacity = INIT_CACHE_SIZE; // 4 (枚舉定義:1 左移 2 位)
    reallocate(oldCapacity, capacity, /* freeOld */false);
}

reallocate() 方法

  1. 申請 newCapacity 大小的地址
  2. 調(diào)用 setBucketsAndMask() 方法 初始化 bucket
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) {
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

setBucketsAndMask() 方法

  1. 舊bucket 存入 新bucket
  2. _occupied = 0糯崎,這里我們留意到了 reallocate() 方法 會將 _occupied = 0几缭。
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();

    _buckets.store(newBuckets, memory_order::memory_order_relaxed);
    
    // ensure other threads see new buckets before new mask
    mega_barrier();
    
    _mask.store(newMask, memory_order::memory_order_relaxed);
    _occupied = 0;
#elif __x86_64__ || i386
    // ensure other threads see buckets contents before buckets pointer
    _buckets.store(newBuckets, memory_order::memory_order_release);
    
    // ensure other threads see new buckets before new mask
    _mask.store(newMask, memory_order::memory_order_release);
    _occupied = 0;
#else
#error Don't know how to do setBucketsAndMask on this architecture.
#endif
}

2.6.2 接下來就是大概率事件方法

如果緩存 newOccupied + CACHE_END_MARKER(1) < capacity / 4 * 3,則什么都不需要做拇颅。

#define CACHE_END_MARKER 1
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.
}

2.6.3 接下來就是擴容方法

  • 如果大于總?cè)萘康?3 / 4 的時候奏司,就需要擴容了(擴容至2倍)。
  • 擴容之后仍然需要利用 reallocate() 方法 開辟空間樟插,在 2.6.1
    setBucketsAndMask() 方法 中我們講過韵洋, reallocate() 方法 會將 _occupied = 0竿刁。到這,我們終于理解了2.4 當中的 問題② 搪缨,為什么 _occupied 會減少食拜,因為擴容之后 _occupied 會初始化至 0,重新計算副编。
else {
    capacity = capacity ? capacity * 2 : INIT_CACHE_SIZE;  // 擴容至兩倍 4
    if (capacity > MAX_CACHE_SIZE) {
        capacity = MAX_CACHE_SIZE;
    }
    reallocate(oldCapacity, capacity, true);  // 內(nèi)存 擴容完畢
}

2.6.4 reallocate() 方法

  • 調(diào)用 setBucketsAndMask() 方法 初始化 bucket 负甸,因為 bucket 受擴容影響重新初始化了,所以2.4 當中的 問題③ 的原因就在這里痹届。
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) {
        cache_collect_free(oldBuckets, oldCapacity);
    }
}

2.6.5 接下來就是 _mask 變化的方法呻待,在2.6.3 中我們知道容量擴容到 2 倍,那么 mask 的值就是 2 的 n次冪 - 1 , 所以 2.4 當中的 問題① 便迎刃而解了队腐。

mask_t m = capacity - 1;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蚕捉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子柴淘,更是在濱河造成了極大的恐慌迫淹,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件为严,死亡現(xiàn)場離奇詭異敛熬,居然都是意外死亡,警方通過查閱死者的電腦和手機第股,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門应民,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夕吻,你說我怎么就攤上這事瑞妇。” “怎么了梭冠?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長改备。 經(jīng)常有香客問我控漠,道長,這世上最難降的妖魔是什么悬钳? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任盐捷,我火速辦了婚禮,結(jié)果婚禮上默勾,老公的妹妹穿的比我還像新娘碉渡。我一直安慰自己,他們只是感情好母剥,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布滞诺。 她就那樣靜靜地躺著形导,像睡著了一般。 火紅的嫁衣襯著肌膚如雪习霹。 梳的紋絲不亂的頭發(fā)上朵耕,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機與錄音淋叶,去河邊找鬼阎曹。 笑死,一個胖子當著我的面吹牛煞檩,可吹牛的內(nèi)容都是我干的处嫌。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼斟湃,長吁一口氣:“原來是場噩夢啊……” “哼熏迹!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起桐早,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤癣缅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后哄酝,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體友存,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年陶衅,在試婚紗的時候發(fā)現(xiàn)自己被綠了屡立。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡搀军,死狀恐怖膨俐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情罩句,我是刑警寧澤焚刺,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站门烂,受9級特大地震影響乳愉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屯远,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一蔓姚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧慨丐,春花似錦坡脐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晌端。三九已至,卻和暖如春浅役,著一層夾襖步出監(jiān)牢的瞬間斩松,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工觉既, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留惧盹,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓瞪讼,卻偏偏與公主長得像钧椰,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子符欠,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345