iOS底層原理總結 - 探尋Runtime本質(二)

Class的結構

經過上一章中對isa本質結構有了新的認識,今天來回顧Class的結構,從新認識Class內部結構。數組

首先來看一下Class的內部結構代碼,對探尋Class的本質作簡單回顧。緩存

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() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}
複製代碼
class_rw_t* data() {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
}
複製代碼

class_rw_t

上述源碼中咱們知道bits & FAST_DATA_MASK位運算以後,能夠獲得class_rw_t,而class_rw_t中存儲着方法列表、屬性列表以及協議列表,來看一下class_rw_t部分代碼安全

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods; // 方法列表
    property_array_t properties; // 屬性列表
    protocol_array_t protocols; // 協議列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
};
複製代碼

上述源碼中,method_array_t、property_array_t、protocol_array_t其實都是二維數組,來到method_array_t、property_array_t、protocol_array_t內部看一下。這裏以method_array_t爲例,method_array_t自己就是一個數組,數組裏面存放的是數組method_list_tmethod_list_t裏面最終存放的是method_tbash

class method_array_t : 
    public list_array_tt<method_t, method_list_t> 
{
    typedef list_array_tt<method_t, method_list_t> Super;

 public:
    method_list_t **beginCategoryMethodLists() {
        return beginLists();
    }
    
    method_list_t **endCategoryMethodLists(Class cls);

    method_array_t duplicate() {
        return Super::duplicate<method_array_t>();
    }
};


class property_array_t : 
    public list_array_tt<property_t, property_list_t> 
{
    typedef list_array_tt<property_t, property_list_t> Super;

 public:
    property_array_t duplicate() {
        return Super::duplicate<property_array_t>();
    }
};


class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t> 
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t> Super;

 public:
    protocol_array_t duplicate() {
        return Super::duplicate<protocol_array_t>();
    }
};
複製代碼

class_rw_t裏面的methods、properties、protocols是二維數組,是可讀可寫的,其中包含了類的初始內容以及分類的內容。數據結構

這裏以method_array_t爲例,圖示其中的結構。app

methods、properties、protocols內結構

class_ro_t

咱們以前提到過class_ro_t中也有存儲方法、屬性、協議列表,另外還有成員變量列表。函數

接着來看一下class_ro_t部分代碼post

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};
複製代碼

上述源碼中能夠看到class_ro_t *ro是隻讀的,內部直接存儲的直接就是method_list_t、protocol_list_t 、property_list_t類型的一維數組,數組裏面分別存放的是類的初始信息,以method_list_t爲例,method_list_t中直接存放的就是method_t,可是是隻讀的,不容許增長刪除修改。性能

總結

以方法列表爲例,class_rw_t中的methods是二維數組的結構,而且可讀可寫,所以能夠動態的添加方法,而且更加便於分類方法的添加。由於咱們在Category的本質裏面提到過,attachList函數內經過memmove 和 memcpy兩個操做將分類的方法列表合併在本類的方法列表中。那麼此時就將分類的方法和本類的方法統一整合到一塊兒了。學習

其實一開始類的方法,屬性,成員變量屬性協議等等都是存放在class_ro_t中的,當程序運行的時候,須要將分類中的列表跟類初始的列表合併在一塊兒的時,就會將class_ro_t中的列表和分類中的列表合併起來存放在class_rw_t中,也就是說class_rw_t中有部分列表是從class_ro_t裏面拿出來的。而且最終和分類的方法合併。能夠經過源碼提現這裏一點。

realizeClass部分源碼

static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();

    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // 最開始cls->data是指向ro的
    ro = (const class_ro_t *)cls->data();

    if (ro->flags & RO_FUTURE) { 
        // rw已經初始化而且分配內存空間
        rw = cls->data();  // cls->data指向rw
        ro = cls->data()->ro;  // cls->data()->ro指向ro
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else { 
        // 若是rw並不存在,則爲rw分配空間
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1); // 分配空間
        rw->ro = ro;  // rw->ro從新指向ro
        rw->flags = RW_REALIZED|RW_REALIZING;
        // 將rw傳入setData函數,等於cls->data()從新指向rw
        cls->setData(rw); 
    }
}
複製代碼

那麼從上述源碼中就能夠發現,類的初始信息原本實際上是存儲在class_ro_t中的,而且ro原本是指向cls->data()的,也就是說bits.data()獲得的是ro,可是在運行過程當中建立了class_rw_t,並將cls->data指向rw,同時將初始信息ro賦值給rw中的ro。最後在經過setData(rw)設置data。那麼此時bits.data()獲得的就是rw,以後再去檢查是否有分類,同時將分類的方法,屬性,協議列表整合存儲在class_rw_t的方法,屬性及協議列表中。

經過上述對源碼的分析,咱們對class_rw_t內存儲方法、屬性、協議列表的過程有了更清晰的認識,那麼接下來探尋class_rw_t中是如何存儲方法的。

class_rw_t中是如何存儲方法的

method_t

咱們知道method_array_t、property_array_t、protocol_array_t中以method_array_t爲例,method_array_t中最終存儲的是method_tmethod_t是對方法、函數的封裝,每個方法對象就是一個method_t。經過源碼看一下method_t的結構體

struct method_t {
    SEL name;  // 函數名
    const char *types;  // 編碼(返回值類型,參數類型)
    IMP imp; // 指向函數的指針(函數地址)
};
複製代碼

method_t結構體中能夠看到三個成員變量,咱們依次來看三個成員變量分別表明什麼。

SEL

SEL表明方法\函數名,通常叫作選擇器,底層結構跟char *相似 typedef struct objc_selector *SEL;,能夠把SEL看作是方法名字符串。

SEL能夠經過@selector()sel_registerName()得到

SEL sel1 = @selector(test);
SEL sel2 = sel_registerName("test");
複製代碼

也能夠經過sel_getName()NSStringFromSelector()將SEL轉成字符串

char *string = sel_getName(sel1);
NSString *string2 = NSStringFromSelector(sel2);
複製代碼

不一樣類中相同名字的方法,所對應的方法選擇器是相同的。

NSLog(@"%p,%p", sel1,sel2);
Runtime-test[23738:8888825] 0x1017718a3,0x1017718a3
複製代碼

SEL僅僅表明方法的名字,而且不一樣類中相同的方法名的SEL是全局惟一的。

types

types包含了函數返回值,參數編碼的字符串。經過字符串拼接的方式將返回值和參數拼接成一個字符串,來表明函數返回值及參數。

咱們經過代碼查看一下types是如何表明函數返回值及參數的,首先經過本身模擬Class的內部實現,經過強制轉化來探尋內部數據,相關代碼在探尋Class的本質中提到過,這裏不在贅述。

Person *person = [[Person alloc] init];
xx_objc_class *cls = (__bridge xx_objc_class *)[Person class];
class_rw_t *data = cls->data();
複製代碼

經過斷點能夠在data中找到types的值

data中types的值

上圖中能夠看出types的值爲v16@0:8,那麼這個值表明什麼呢?apple爲了可以清晰的使用字符串表示方法及其返回值,制定了一系列對應規則,經過下表能夠看到一一對應關係

Objective-C type encodings

將types的值同表中的一一對照查看types的值v16@0:8 表明什麼

- (void) test;

 v    16      @     0     :     8
void         id          SEL
// 16表示參數的佔用空間大小,id後面跟的0表示從0位開始存儲,id佔8位空間。
// SEL後面的8表示從第8位開始存儲,SEL一樣佔8位空間
複製代碼

咱們知道任何方法都默認有兩個參數的,id類型的self,和SEL類型的_cmd,而上述經過對types的分析同時也驗證了這個說法。

爲了可以看的更加清晰,咱們爲test添加返回值及參數以後從新查看types的值。

types的值

一樣經過上表找出一一對應的值,查看types的值表明的方法

- (int)testWithAge:(int)age Height:(float)height
{
    return 0;
}
  i    24    @    0    :    8    i    16    f    20
int         id        SEL       int        float
// 參數的總佔用空間爲 8 + 8 + 4 + 4 = 24
// id 從第0位開始佔據8位空間
// SEL 從第8位開始佔據8位空間
// int 從第16位開始佔據4位空間
// float 從第20位開始佔據4位空間
複製代碼

iOS提供了@encode的指令,能夠將具體的類型轉化成字符串編碼。

NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));

// 打印內容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :
複製代碼

上述代碼中能夠看到,對應關係確實如上表所示。

IMP

IMP表明函數的具體實現,存儲的內容是函數地址。也就是說當找到imp的時候就能夠找到函數實現,進而對函數進行調用。

在上述代碼中打印IMP的值

Printing description of data->methods->first.imp:
(IMP) imp = 0x000000010c66a4a0 (Runtime-test`-[Person testWithAge:Height:] at Person.m:13)
複製代碼

以後在test方法內部打印斷點,並來到其方法內部能夠看出imp中的存儲的地址也就是方法實現的地址。

test方法內部

經過上面的學習咱們知道了方法列表是如何存儲在Class類對象中的,可是當屢次繼承的子類想要調用基類方法時,就須要經過superclass指針一層一層找到基類,在從基類方法列表中找到對應的方法進行調用。若是屢次調用基類方法,那麼就須要屢次遍歷每一層父類的方法列表,這對性能來講無疑是傷害巨大的。

apple經過方法緩存的形式解決了這一問題,接下來咱們來探尋Class類對象是如何進行方法緩存的

方法緩存 cache_t

回到類對象結構體,成員變量cache就是用來對方法進行緩存的。

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() { 
        return bits.data();
    }
    void setData(class_rw_t *newData) {
        bits.setData(newData);
    }
}
複製代碼

cache_t cache;用來緩存曾經調用過的方法,能夠提升方法的查找速度。

回顧方法調用過程:調用方法的時候,須要去方法列表裏面進行遍歷查找。若是方法不在列表裏面,就會經過superclass找到父類的類對象,在去父類類對象方法列表裏面遍歷查找。

若是方法須要調用不少次的話,那就至關於每次調用都須要去遍歷屢次方法列表,爲了可以快速查找方法,apple設計了cache_t來進行方法緩存。

每當調用方法的時候,會先去cache中查找是否有緩存的方法,若是沒有緩存,在去類對象方法列表中查找,以此類推直到找到方法以後,就會將方法直接存儲在cache中,下一次在調用這個方法的時候,就會在類對象的cache裏面找到這個方法,直接調用了。

cache_t 如何進行緩存

那麼cache_t是如何對方法進行緩存的呢?首先來看一下cache_t的內部結構。

struct cache_t {
    struct bucket_t *_buckets; // 散列表 數組
    mask_t _mask; // 散列表的長度 -1
    mask_t _occupied; // 已經緩存的方法數量
};
複製代碼

bucket_t是以數組的方式存儲方法列表的,看一下bucket_t內部結構

struct bucket_t {
private:
    cache_key_t _key; // SEL做爲Key
    IMP _imp; // 函數的內存地址
};
複製代碼

從源碼中能夠看出bucket_t中存儲着SEL_imp,經過key->value的形式,以SELkey函數實現的內存地址 _impvalue來存儲方法。

經過一張圖來展現一下cache_t的結構。

cache_t的結構

上述bucket_t列表咱們稱之爲散列表(哈希表) 散列表(Hash table,也叫哈希表),是根據關鍵碼值(Key value)而直接進行訪問的數據結構。也就是說,它經過把關鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。這個映射函數叫作散列函數,存放記錄的數組叫作散列表。

那麼apple如何在散列表中快速而且準確的找到對應的key以及函數實現呢?這就須要咱們經過源碼來看一下apple的散列函數是如何設計的。

散列函數及散列表原理

首先來看一下存儲的源碼,主要查看幾個函數,關鍵代碼都有註釋,不在贅述。

cache_fill 及 cache_fill_nolock 函數

void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
    mutex_locker_t lock(cacheUpdateLock);
    cache_fill_nolock(cls, sel, imp, receiver);
#else
    _collecting_in_critical();
    return;
#endif
}

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver)
{
    cacheUpdateLock.assertLocked();
    // 若是沒有initialize直接return
    if (!cls->isInitialized()) return;
    // 確保線程安全,沒有其餘線程添加緩存
    if (cache_getImp(cls, sel)) return;
    // 經過類對象獲取到cache 
    cache_t *cache = getCache(cls);
    // 將SEL包裝成Key
    cache_key_t key = getKey(sel);
   // 佔用空間+1
    mask_t newOccupied = cache->occupied() + 1;
   // 獲取緩存列表的緩存能力,能存儲多少個鍵值對
    mask_t capacity = cache->capacity();
    if (cache->isConstantEmptyCache()) {
        // 若是爲空的,則建立空間,這裏建立的空間爲4個。
        cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
    }
    else if (newOccupied <= capacity / 4 * 3) {
        // 若是所佔用的空間佔總數的3/4一下,則繼續使用如今的空間
    }
    else {
       // 若是佔用空間超過3/4則擴展空間
        cache->expand();
    }
    // 經過key查找合適的存儲空間。
    bucket_t *bucket = cache->find(key, receiver);
    // 若是key==0則說明以前未存儲過這個key,佔用空間+1
    if (bucket->key() == 0) cache->incrementOccupied();
    // 存儲key,imp 
    bucket->set(key, imp);
}
複製代碼

reallocate 函數

經過上述源碼看到reallocate函數負責分配散列表空間,來到reallocate函數內部。

void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
    // 舊的散列表可否被釋放
    bool freeOld = canBeFreed();
    // 獲取舊的散列表
    bucket_t *oldBuckets = buckets();
    // 經過新的空間需求量建立新的散列表
    bucket_t *newBuckets = allocateBuckets(newCapacity);

    assert(newCapacity > 0);
    assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
    // 設置Buckets和Mash,Mask的值爲散列表長度-1
    setBucketsAndMask(newBuckets, newCapacity - 1);
    // 釋放舊的散列表
    if (freeOld) {
        cache_collect_free(oldBuckets, oldCapacity);
        cache_collect(false);
    }
}
複製代碼

上述源碼中首次傳入reallocate函數的newCapacityINIT_CACHE_SIZEINIT_CACHE_SIZE是個枚舉值,也就是4。所以散列表最初建立的空間就是4個。

enum {
    INIT_CACHE_SIZE_LOG2 = 2,
    INIT_CACHE_SIZE      = (1 << INIT_CACHE_SIZE_LOG2)
};
複製代碼

expand ()函數

當散列表的空間被佔用超過3/4的時候,散列表會調用expand ()函數進行擴展,咱們來看一下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) {
        newCapacity = oldCapacity;
    }
    // 調用reallocate函數,從新建立存儲空間
    reallocate(oldCapacity, newCapacity);
}
複製代碼

上述源碼中能夠發現散列表進行擴容時會將容量增至以前的2倍。

find 函數

最後來看一下散列表中如何快速的經過key找到相應的bucket呢?咱們來到find函數內部

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
    assert(k != 0);
    // 獲取散列表
    bucket_t *b = buckets();
    // 獲取mask
    mask_t m = mask();
    // 經過key找到key在散列表中存儲的下標
    mask_t begin = cache_hash(k, m);
    // 將下標賦值給i
    mask_t i = begin;
    // 若是下標i中存儲的bucket的key==0說明當前沒有存儲相應的key,將b[i]返回出去進行存儲
    // 若是下標i中存儲的bucket的key==k,說明當前空間內已經存儲了相應key,將b[i]返回出去進行存儲
    do {
        if (b[i].key() == 0  ||  b[i].key() == k) {
            // 若是知足條件則直接reutrn出去
            return &b[i];
        }
    // 若是走到這裏說明上面不知足,那麼會往前移動一個空間從新進行斷定,知道能夠成功return爲止
    } while ((i = cache_next(i, m)) != begin);

    // hack
    Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
    cache_t::bad_cache(receiver, (SEL)k, cls);
}
複製代碼

函數cache_hash (k, m)用來經過key找到方法在散列表中存儲的下標,來到cache_hash (k, m)函數內部

static inline mask_t cache_hash(cache_key_t key, mask_t mask) 
{
    return (mask_t)(key & mask);
}
複製代碼

能夠發現cache_hash (k, m)函數內部僅僅是進行了key & mask的按位與運算,獲得下標即存儲在相應的位置上。按位與運算在上文中已詳細講解過,這裏不在贅述。

_mask

經過上面的分析咱們知道_mask的值是散列表的長度減一,那麼任何數經過與_mask進行按位與運算以後得到的值都會小於等於_mask,所以不會出現數組溢出的狀況。

舉個例子,假設散列表的長度爲8,那麼mask的值爲7

0101 1011  // 任意值
& 0000 0111  // mask = 7
------------
  0000 0011 //獲取的值始終等於或小於mask的值
複製代碼

總結

當第一次使用方法時,消息機制經過isa找到方法以後,會對方法以SEL爲keyIMP爲value的方式緩存在cache_buckets中,當第一次存儲的時候,會建立具備4個空間的散列表,並將_mask的值置爲散列表的長度減一,以後經過SEL & mask計算出方法存儲的下標值,並將方法存儲在散列表中。舉個例子,若是計算出下標值爲3,那麼就將方法直接存儲在下標爲3的空間中,前面的空間會留空。

當散列表中存儲的方法佔據散列表長度超過3/4的時候,散列表會進行擴容操做,將建立一個新的散列表而且空間擴容至原來空間的兩倍,並重置_mask的值,最後釋放舊的散列表,此時再有方法要進行緩存的話,就須要從新經過SEL & mask計算出下標值以後在按照下標進行存儲了。

若是一個類中方法不少,其中極可能會出現多個方法的SEL & mask獲得的值爲同一個下標值,那麼會調用cache_next函數往下標值-1位去進行存儲,若是下標值-1位空間中有存儲方法,而且key不與要存儲的key相同,那麼再到前面一位進行比較,直到找到一位空間沒有存儲方法或者key與要存儲的key相同爲止,若是到下標0的話就會到下標爲_mask的空間也就是最大空間處進行比較。

當要查找方法時,並不須要遍歷散列表,一樣經過SEL & mask計算出下標值,直接去下標值的空間取值便可,同上,若是下標值中存儲的key與要查找的key不相同,就去前面一位查找。這樣雖然佔用了少許控件,可是大大節省了時間,也就是說其實apple是使用空間換取了存取的時間。

經過一張圖更清晰的看一下其中的流程。

散列表內部存取邏輯

驗證上述流程

經過一段代碼演示一下 。一樣使用仿照objc_class結構體自定義一個結構體,並進行強制轉化來查看其內部數據,自定義結構體在以前的文章中使用過屢次這裏不在贅述。

咱們建立Person類繼承NSObjectStudent類繼承PersonCollegeStudent繼承Student。三個類分別有personTest,studentTest,colleaeStudentTest方法

經過打印斷點來看一下方法緩存的過程

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        CollegeStudent *collegeStudent = [[CollegeStudent alloc] init];
        xx_objc_class *collegeStudentClass = (__bridge xx_objc_class *)[CollegeStudent class];
        
        cache_t cache = collegeStudentClass->cache;
        bucket_t *buckets = cache._buckets;
        
        [collegeStudent personTest];
        [collegeStudent studentTest];
        
        NSLog(@"----------------------------");
        for (int i = 0; i <= cache._mask; i++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s %p", bucket._key, bucket._imp);
        }
        NSLog(@"----------------------------");
        
        [collegeStudent colleaeStudentTest];

        cache = collegeStudentClass->cache;
        buckets = cache._buckets;
        NSLog(@"----------------------------");
        for (int i = 0; i <= cache._mask; i++) {
            bucket_t bucket = buckets[i];
            NSLog(@"%s %p", bucket._key, bucket._imp);
        }
        NSLog(@"----------------------------");
        
        NSLog(@"%p",@selector(colleaeStudentTest));
        NSLog(@"----------------------------");
    }
    return 0;
}
複製代碼

咱們分別在collegeStudent實例對象調用personTest,studentTest,colleaeStudentTest方法處打斷點查看cache的變化。

personTest方法調用以前

personTest方法調用以前

從上圖中能夠發現,personTest方法調用以前,cache中僅僅存儲了init方法,上圖中能夠看出init方法剛好存儲在下標爲0的位置所以咱們能夠看到,_mask的值爲3驗證咱們上述源碼中提到的散列表第一次存儲時會分配4個內存空間,_occupied的值爲1證實此時_buckets中僅僅存儲了一個方法。

collegeStudent在調用personTest的時候,首先發現collegeStudent類對象cache中沒有personTest方法,就會去collegeStudent類對象的方法列表中查找,方法列表中也沒有,那麼就經過superclass指針找到Student類對象Studeng類對象cache和方法列表一樣沒有,再經過superclass指針找到Person類對象,最終在Person類對象方法列表中找到以後進行調用,並緩存在collegeStudent類對象cache中。

執行personTest方法以後查看cache方法的變化

personTest方法調用以後

上圖中能夠發現_occupied值爲2,說明此時personTest方法已經被緩存在collegeStudent類對象cache中。

同理執行過studentTest方法以後,咱們經過打印查看一下此時cache內存儲的信息

cache內存儲的信息

上圖中能夠看到cache中確實存儲了 init 、personTest 、studentTest三個方法。

那麼執行過colleaeStudentTest方法以後此時cache中應該對colleaeStudentTest方法進行緩存。上面源碼提到過,當存儲的方法數超過散列表長度的3/4時,系統會從新建立一個容量爲原來兩倍的新的散列表替代原來的散列表。過掉colleaeStudentTest方法,從新打印cache內存儲的方法查看。

_bucket列表擴容以後

能夠看出上圖中_bucket散列表擴容以後僅僅存儲了colleaeStudentTest方法,而且上圖中打印SEL & _mask 位運算得出下標的值確實是_bucket列表中colleaeStudentTest方法存儲的位置。

至此已經對Class的結構及方法緩存的過程有了新的認知,apple經過散列表的形式對方法進行緩存,以少許的空間節省了大量查找方法的時間。

底層原理文章專欄

底層原理文章專欄


文中若是有不對的地方歡迎指出。我是xx_cc,一隻長大好久但尚未二夠的傢伙。須要視頻一塊兒探討學習的coder能夠加我Q:2336684744

相關文章
相關標籤/搜索