iOS底層原理09:類結(jié)構(gòu)分析——cache屬性

在前面的文章中,我們探索了isasuperclass、bits屬性
iOS底層原理07:類 & 類結(jié)構(gòu)分析
iOS底層原理08:類結(jié)構(gòu)分析——bits屬性
本文主要探索cache的結(jié)構(gòu)和底層原理

1、探索cache的數(shù)據(jù)結(jié)構(gòu)

cache的類型是cache_t結(jié)構(gòu)體

1.1、cache_t結(jié)構(gòu)體

??來(lái)看看objc4-818源碼中cache_t結(jié)構(gòu)體

typedef unsigned long           uintptr_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

struct cache_t {
private:
    // explicit_atomic 顯示原子性,目的是為了能夠 保證 增刪改查時(shí) 線程的安全性
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //8字節(jié)
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask; //4字節(jié)
#if __LP64__
            uint16_t                   _flags; //2字節(jié)
#endif
            uint16_t                   _occupied;//2字節(jié)
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache; // 8字節(jié)
    };
    
    //下面是一些static屬性和方法,并不影響結(jié)構(gòu)體的內(nèi)存大小,主要是因?yàn)閟tatic類型的屬性 不存在結(jié)構(gòu)體的內(nèi)存中
    /*
     #if defined(__arm64__) && __LP64__
     #if TARGET_OS_OSX || TARGET_OS_SIMULATOR
     // macOS 或 __arm64__的模擬器
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
     #else
     //__arm64__的真機(jī)
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16
     #endif
     #elif defined(__arm64__) && !__LP64__
     //32位 真機(jī)
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
     #else
     //macOS 模擬器
     #define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
     #endif
     ******  中間是不同的架構(gòu)之間的判斷 主要是用來(lái)不同類型 mask 和 buckets 的掩碼
    */
        
    // ...省略代碼
    // 下面是幾個(gè)比較重要的方法
    void incrementOccupied();
    void setBucketsAndMask(struct bucket_t *newBuckets, mask_t newMask);

    void reallocate(mask_t oldCapacity, mask_t newCapacity, bool freeOld);
    void collect_free(bucket_t *oldBuckets, mask_t oldCapacity);
    
    unsigned capacity() const;
    struct bucket_t *buckets() const;
    Class cls() const;
    
    void insert(SEL sel, IMP imp, id receiver);
    /// 快速計(jì)算對(duì)象內(nèi)存大小,16字節(jié)對(duì)齊,在對(duì)象的alloc中我們已經(jīng)分析過(guò)了
    size_t fastInstanceSize(size_t extra) const {...}
    
    // ...省略代碼
}

cache_t是結(jié)構(gòu)體類型,有兩個(gè)成員變量:_bucketsAndMaybeMask和一個(gè)聯(lián)合體

  • _bucketsAndMaybeMaskuintptr_t類型,占8字節(jié)
  • 聯(lián)合體里面有兩個(gè)成員變量:結(jié)構(gòu)體_originalPreoptCache,聯(lián)合體由最大的成員變量的大小決定
    • _originalPreoptCachepreopt_cache_t *結(jié)構(gòu)體指針,占8字節(jié)
    • 結(jié)構(gòu)體中有_maybeMask、_flags、_occupied三個(gè)成員變量。
      • _maybeMask的大小取決于mask_tuint32_t,占4字節(jié)
      • _flagsuint16_t類型,占2字節(jié)
      • _occupieduint16_t類型,占2字節(jié)

所以cache_t的大小等于 8+8或者8+4+2+2,即16字節(jié)

  • cache_t結(jié)構(gòu)體提供了buckets()方法,返回類型是bucket_t *結(jié)構(gòu)體指針
struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}
  • cache_t結(jié)構(gòu)體還提供了insert方法,插入selimp,即對(duì)方法的緩存
void cache_t::insert(SEL sel, IMP imp, id receiver) {
   //對(duì)各種不符合條件的判斷報(bào)出錯(cuò)誤碼
      
   //省略代碼。。。。
       
    //通過(guò)buckets數(shù)組來(lái)判斷需要插入的內(nèi)容情況
    bucket_t *b = buckets();  
    mask_t m = capacity - 1;
    mask_t begin = cache_hash(sel, m);
    mask_t i = begin;

   //省略代碼。。。。
}

1.2、bucket_t結(jié)構(gòu)體

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__  // arm64架構(gòu)
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else  // 其他架構(gòu)
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif

    // ...省略代碼
}
  • bucket_t的成員順序與架構(gòu)有關(guān)
  • bucket_t有兩個(gè)成員變量_sel_imp,存儲(chǔ)方法的信息

1.3、lldb調(diào)試驗(yàn)證

  • 創(chuàng)建HTPerson
image
  • main.m中代碼如下,在[p sayHello];設(shè)置斷點(diǎn),運(yùn)行代碼
image
  • 通過(guò)p *$1查看 cache的值,此時(shí)_maybeMask.Value_occupied的值都為0
image
  • 執(zhí)行完[p sayHello];對(duì)象方法,繼續(xù)查看cache的值,此時(shí)_maybeMask.Value_occupied的值都發(fā)生了變化
image
  • 完整的lldb調(diào)試過(guò)程如下圖
image

通過(guò)源碼lldb調(diào)試,可以發(fā)現(xiàn) cache存儲(chǔ)的 方法緩存

  • 調(diào)用對(duì)象方法sayHello后,_maybeMask_occupied被賦值,這兩個(gè)變量應(yīng)該和緩存是有關(guān)系的,我們?cè)诤竺孢M(jìn)行深入分析。
  • bucket_t結(jié)構(gòu)體提供了sel()imp(nil, Class)方法

2、根據(jù)源碼,對(duì)類和cache進(jìn)行仿寫

為什么需要進(jìn)行代碼仿寫呢?

  • 當(dāng)源碼無(wú)法直接運(yùn)行調(diào)試時(shí),就需要進(jìn)行代碼仿寫
  • 使用lldb調(diào)試時(shí),增減一些屬性、方法,就需要再次執(zhí)行比較多的重復(fù)步驟,比較繁瑣;
  • 小規(guī)模取樣的方式,會(huì)讓你對(duì)底層更加清晰。

2.1、準(zhǔn)備工作

  • 新建一個(gè)macOS -> Command Line Tool工程,并創(chuàng)建HTPerson類,代碼如下:
/*** HTPerson.h ***/
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface HTPerson : NSObject

- (void)say1;
- (void)say2;
- (void)say3;
- (void)say4;
- (void)say5;
- (void)say6;
- (void)say7;
+ (void)sayHappy;

@end

NS_ASSUME_NONNULL_END

/*** HTPerson.m ***/
#import "HTPerson.h"

@implementation HTPerson

- (void)say1{
    NSLog(@"%s",__func__);
}
- (void)say2{
    NSLog(@"%s",__func__);
}
- (void)say3{
    NSLog(@"%s",__func__);
}
- (void)say4{
    NSLog(@"%s",__func__);
}
- (void)say5{
    NSLog(@"%s",__func__);
}
- (void)say6{
    NSLog(@"%s",__func__);
}
- (void)say7{
    NSLog(@"%s",__func__);
}

+ (void)sayHappy{
    NSLog(@"%s",__func__);
}

@end
  • main.m文件中代碼如下:
#import <Foundation/Foundation.h>
#import "HTPerson.h"

typedef uint32_t mask_t;
// bucket_t結(jié)構(gòu)體
struct ht_bucket_t {
    SEL _sel;
    IMP _imp;
};

// cache_t結(jié)構(gòu)體
struct ht_cache_t {
    struct ht_bucket_t * _buckets; //8字節(jié)
    mask_t _maybeMask; //4字節(jié)
    uint16_t _flags; //2字節(jié)
    uint16_t _occupied;//2字節(jié)
};

// 類結(jié)構(gòu)體
struct ht_objc_class {
    Class isa;  // 8字節(jié)
    Class superclass; //8字節(jié)
    struct ht_cache_t cache;    //16字節(jié)
    uintptr_t  bits;    // 8字節(jié)
};


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        HTPerson *p = [HTPerson alloc];
        Class pClass = p.class;
        
//        [p say1];
//        [p say2];
//        [p say3];
//        [p say4];
//        [p say5];
//        [p say6];
//        [p say1];
//        [p say2];
        
        struct ht_objc_class * ht_class = (__bridge struct ht_objc_class *)(pClass);
        NSLog(@"- %hu - %u", ht_class->cache._occupied, ht_class->cache._maybeMask);

        for (int i = 0; i < ht_class->cache._maybeMask; i++) {
            struct ht_bucket_t bucket = ht_class->cache._buckets[I];
            NSLog(@"%@ - %pf", NSStringFromSelector(bucket._sel), bucket._imp);
        }
    }
    return 0;
}

2.2、對(duì)象方法的調(diào)用 與 cache值的關(guān)系

  • 未調(diào)用對(duì)象方法

如果對(duì)象方法都沒(méi)有調(diào)用,則cache不會(huì)進(jìn)行方法緩存,此時(shí)_occupied_maybeMask的值都為0

image

  • 調(diào)用say1say2方法,查看打印結(jié)果
image
  • 如果繼續(xù)調(diào)用say3、say4say5方法呢
image

【問(wèn)題】 這里就產(chǎn)生了幾個(gè)疑問(wèn)?

  • _occupied_maybeMask是什么?他們的值是如何變化?
  • 調(diào)用say3、say4say5方法后,say1say2怎么消失了?
  • cache存儲(chǔ)的位置怎么是亂序的呢?

_occupied_maybeMask是什么?在什么地方賦值,只能去objc源碼中找答案。我們要緩存方法,首先看怎么把方法插入到bukets中的。帶著這些疑問(wèn)繼續(xù)探討cache_t源碼

3、cache_t源碼探究

  • 首先找到方法緩存的入口
image

從這個(gè)插入的方法來(lái)看,插入的參數(shù)有selimp、還有receiver消息接收者。??下面是這個(gè)插入方法的代碼實(shí)現(xiàn):

void cache_t::insert(SEL sel, IMP imp, id receiver)
{
    // ...省略代碼 (錯(cuò)誤處理相關(guān)代碼)
    
    // 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<Atomic, Encoded>(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
}

計(jì)算當(dāng)前所占容量

image
  • occupied()獲取當(dāng)前所占的容量,其實(shí)就是告訴你緩存中已經(jīng)有幾個(gè)bucket
  • newOccupied = occupied() + 1,表示當(dāng)前方法第幾個(gè)進(jìn)來(lái)緩存的
  • oldCapacity 目的是為了重新擴(kuò)容的時(shí)候釋放舊的內(nèi)存

開(kāi)辟容量

image
  • 第一次緩存方法的時(shí),開(kāi)辟默認(rèn)容量是 capacity = INIT_CACHE_SIZEcapacity = 4 就是4個(gè)bucket的內(nèi)存大小
  • reallocate(oldCapacity, capacity, /* freeOld */false)開(kāi)辟內(nèi)存,freeOld變量控制是否釋放舊的內(nèi)存

reallocate方法探究

image

reallocate 方法主要做三件事

  • allocateBuckets開(kāi)辟內(nèi)存
  • setBucketsAndMask設(shè)置maskbuckets的值
  • collect_free是否釋放舊的內(nèi)存,由freeOld控制

allocateBuckets方法(開(kāi)辟內(nèi)存)探究

image

allocateBuckets 方法主要做兩件事

  • calloc(bytesForCapacity(newCapacity), 1)開(kāi)辟內(nèi)存
  • end->set將開(kāi)辟內(nèi)存的最后一個(gè)位置存入sel = 1,imp = 第一個(gè)buket位置的地址

setBucketsAndMask 方法探究

image

setBucketsAndMask方法主要用來(lái)賦值

  • 根據(jù)不同的架構(gòu)系統(tǒng)向_bucketsAndMaybeMask_maybeMask寫入數(shù)據(jù)
  • _occupied重置為 0

collect_free 方法探究

image
  • collect_free方法主要是清空數(shù)據(jù),回收內(nèi)存

二倍擴(kuò)容

image
  • 當(dāng)方法緩存到總?cè)萘康?code>3/4或者7/8時(shí),回進(jìn)行二倍擴(kuò)容
  • 二倍擴(kuò)容即開(kāi)辟2倍新內(nèi)存,釋放舊內(nèi)存

方法緩存

image
  • 首先拿到buckets(),即開(kāi)辟這塊內(nèi)存首地址,也就是第一個(gè)bucket的地址,buckets()既不是數(shù)組也不是鏈表,只是一塊連續(xù)的內(nèi)存
  • cache_hash方法計(jì)算hash下標(biāo), cache_next方法處理hash沖突
  • 如果當(dāng)前的位置沒(méi)有數(shù)據(jù),就緩存該方法;如果該位置有方法且和你的方法一樣的,說(shuō)明該方法緩存過(guò)了,直接return;如果存在hash沖突,下標(biāo)一樣,sel不一樣,此時(shí)會(huì)進(jìn)行再次hash,沖突解決繼續(xù)緩存

方法緩存寫入方法 set

image

set方法:將impsel寫入bucket

insert方法調(diào)用流程

前面探究了insert方法的源碼實(shí)現(xiàn),接下來(lái)我們探究insert方法調(diào)用流程,是如何從調(diào)用實(shí)例方法走到cache里面的insert方法的?

  • 首先在insert方法中打個(gè)斷點(diǎn),然后運(yùn)行源碼,查看函數(shù)調(diào)用棧
image

從堆棧信息可以看出insert的調(diào)用流程:_objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert

【問(wèn)題】 _objc_msgSend_uncached方法又是何時(shí)調(diào)用的呢?

  • objc4-818源碼中搜索_objc_msgSend_uncached如下圖
image

我們發(fā)現(xiàn):objc_msgSend方法會(huì)調(diào)用_objc_msgSend_uncached,至此整個(gè)流程就串聯(lián)起來(lái)了

  • 方法調(diào)用的本質(zhì)就是消息發(fā)送,即調(diào)用objc_msgSend
  • 方法緩存的調(diào)用流程:objc_msgSend --> _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> cache_t::insert
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容