Class的結(jié)構(gòu)
通過上一章中對isa本質(zhì)結(jié)構(gòu)有了新的認(rèn)識哈垢,今天來回顧Class的結(jié)構(gòu)赖捌,重新認(rèn)識Class內(nèi)部結(jié)構(gòu)案疲。
首先來看一下Class的內(nèi)部結(jié)構(gòu)代碼封恰,對探尋Class的本質(zhì)做簡單回顧。
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
中存儲著方法列表、屬性列表以及協(xié)議列表春贸,來看一下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; // 協(xié)議列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
};
上述源碼中混萝,method_array_t、property_array_t萍恕、protocol_array_t
其實都是二維數(shù)組逸嘀,來到method_array_t、property_array_t允粤、protocol_array_t
內(nèi)部看一下崭倘。這里以method_array_t
為例,method_array_t
本身就是一個數(shù)組类垫,數(shù)組里面存放的是數(shù)組method_list_t
司光,method_list_t
里面最終存放的是method_t
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
是二維數(shù)組残家,是可讀可寫的,其中包含了類的初始內(nèi)容以及分類的內(nèi)容售躁。
這里以method_array_t
為例坞淮,圖示其中的結(jié)構(gòu)。
class_ro_t
我們之前提到過class_ro_t
中也有存儲方法、屬性市袖、協(xié)議列表啡直,另外還有成員變量列表。
接著來看一下class_ro_t
部分代碼
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
是只讀的,內(nèi)部直接存儲的直接就是method_list_t付枫、protocol_list_t 烹玉、property_list_t
類型的一維數(shù)組,數(shù)組里面分別存放的是類的初始信息阐滩,以method_list_t
為例二打,method_list_t
中直接存放的就是method_t
,但是是只讀的掂榔,不允許增加刪除修改继效。
總結(jié)
以方法列表為例,class_rw_t
中的methods是二維數(shù)組的結(jié)構(gòu)装获,并且可讀可寫瑞信,因此可以動態(tài)的添加方法,并且更加便于分類方法的添加穴豫。因為我們在Category的本質(zhì)里面提到過凡简,attachList
函數(shù)內(nèi)通過memmove 和 memcpy
兩個操作將分類的方法列表合并在本類的方法列表中。那么此時就將分類的方法和本類的方法統(tǒng)一整合到一起了精肃。
其實一開始類的方法秤涩,屬性,成員變量屬性協(xié)議等等都是存放在class_ro_t
中的司抱,當(dāng)程序運行的時候筐眷,需要將分類中的列表跟類初始的列表合并在一起的時,就會將class_ro_t
中的列表和分類中的列表合并起來存放在class_rw_t
中习柠,也就是說class_rw_t
中有部分列表是從class_ro_t
里面拿出來的匀谣。并且最終和分類的方法合并∽世#可以通過源碼提現(xiàn)這里一點武翎。
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已經(jīng)初始化并且分配內(nèi)存空間
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函數(shù)溶锭,等于cls->data()重新指向rw
cls->setData(rw);
}
}
那么從上述源碼中就可以發(fā)現(xiàn)后频,類的初始信息本來其實是存儲在class_ro_t
中的,并且ro
本來是指向cls->data()
的暖途,也就是說bits.data()
得到的是ro
,但是在運行過程中創(chuàng)建了class_rw_t
膏执,并將cls->data
指向rw
驻售,同時將初始信息ro
賦值給rw
中的ro
。最后在通過setData(rw)設(shè)置data更米。那么此時bits.data()
得到的就是rw
欺栗,之后再去檢查是否有分類,同時將分類的方法,屬性迟几,協(xié)議列表整合存儲在class_rw_t
的方法消请,屬性及協(xié)議列表中。
通過上述對源碼的分析类腮,我們對class_rw_t
內(nèi)存儲方法臊泰、屬性、協(xié)議列表的過程有了更清晰的認(rèn)識蚜枢,那么接下來探尋class_rw_t
中是如何存儲方法的缸逃。
class_rw_t中是如何存儲方法的
method_t
我們知道method_array_t、property_array_t厂抽、protocol_array_t
中以method_array_t
為例需频,method_array_t
中最終存儲的是method_t
,method_t
是對方法筷凤、函數(shù)的封裝昭殉,每一個方法對象就是一個method_t
。通過源碼看一下method_t
的結(jié)構(gòu)體
struct method_t {
SEL name; // 函數(shù)名
const char *types; // 編碼(返回值類型藐守,參數(shù)類型)
IMP imp; // 指向函數(shù)的指針(函數(shù)地址)
};
method_t結(jié)構(gòu)體中可以看到三個成員變量挪丢,我們依次來看三個成員變量分別代表什么。
SEL
SEL代表方法\函數(shù)名吗伤,一般叫做選擇器吃靠,底層結(jié)構(gòu)跟char *
類似
typedef struct objc_selector *SEL;
,可以把SEL看做是方法名字符串足淆。
SEL可以通過@selector()
和sel_registerName()
獲得
SEL sel1 = @selector(test);
SEL sel2 = sel_registerName("test");
也可以通過sel_getName()
和NSStringFromSelector()
將SEL轉(zhuǎn)成字符串
char *string = sel_getName(sel1);
NSString *string2 = NSStringFromSelector(sel2);
不同類中相同名字的方法巢块,所對應(yīng)的方法選擇器是相同的。
NSLog(@"%p,%p", sel1,sel2);
Runtime-test[23738:8888825] 0x1017718a3,0x1017718a3
SEL僅僅代表方法的名字巧号,并且不同類中相同的方法名的SEL是全局唯一的族奢。
types
types
包含了函數(shù)返回值,參數(shù)編碼的字符串丹鸿。通過字符串拼接的方式將返回值和參數(shù)拼接成一個字符串越走,來代表函數(shù)返回值及參數(shù)。
我們通過代碼查看一下types是如何代表函數(shù)返回值及參數(shù)的靠欢,首先通過自己模擬Class的內(nèi)部實現(xiàn)廊敌,通過強制轉(zhuǎn)化來探尋內(nèi)部數(shù)據(jù),相關(guān)代碼在探尋Class的本質(zhì)中提到過门怪,這里不在贅述骡澈。
Person *person = [[Person alloc] init];
xx_objc_class *cls = (__bridge xx_objc_class *)[Person class];
class_rw_t *data = cls->data();
通過斷點可以在data中找到types的值
上圖中可以看出types
的值為v16@0:8
,那么這個值代表什么呢掷空?apple為了能夠清晰的使用字符串表示方法及其返回值肋殴,制定了一系列對應(yīng)規(guī)則囤锉,通過下表可以看到一一對應(yīng)關(guān)系
將types的值同表中的一一對照查看types
的值v16@0:8
代表什么
- (void) test;
v 16 @ 0 : 8
void id SEL
// 16表示參數(shù)的占用空間大小,id后面跟的0表示從0位開始存儲护锤,id占8位空間官地。
// SEL后面的8表示從第8位開始存儲,SEL同樣占8位空間
我們知道任何方法都默認(rèn)有兩個參數(shù)的烙懦,id
類型的self
驱入,和SEL
類型的_cmd
,而上述通過對types
的分析同時也驗證了這個說法修陡。
為了能夠看的更加清晰沧侥,我們?yōu)閠est添加返回值及參數(shù)之后重新查看types的值。
同樣通過上表找出一一對應(yīng)的值魄鸦,查看types的值代表的方法
- (int)testWithAge:(int)age Height:(float)height
{
return 0;
}
i 24 @ 0 : 8 i 16 f 20
int id SEL int float
// 參數(shù)的總占用空間為 8 + 8 + 4 + 4 = 24
// id 從第0位開始占據(jù)8位空間
// SEL 從第8位開始占據(jù)8位空間
// int 從第16位開始占據(jù)4位空間
// float 從第20位開始占據(jù)4位空間
iOS提供了@encode
的指令宴杀,可以將具體的類型轉(zhuǎn)化成字符串編碼。
NSLog(@"%s",@encode(int));
NSLog(@"%s",@encode(float));
NSLog(@"%s",@encode(id));
NSLog(@"%s",@encode(SEL));
// 打印內(nèi)容
Runtime-test[25275:9144176] i
Runtime-test[25275:9144176] f
Runtime-test[25275:9144176] @
Runtime-test[25275:9144176] :
上述代碼中可以看到拾因,對應(yīng)關(guān)系確實如上表所示旺罢。
IMP
IMP
代表函數(shù)的具體實現(xiàn),存儲的內(nèi)容是函數(shù)地址绢记。也就是說當(dāng)找到imp
的時候就可以找到函數(shù)實現(xiàn)扁达,進(jìn)而對函數(shù)進(jìn)行調(diào)用。
在上述代碼中打印IMP
的值
Printing description of data->methods->first.imp:
(IMP) imp = 0x000000010c66a4a0 (Runtime-test`-[Person testWithAge:Height:] at Person.m:13)
之后在test
方法內(nèi)部打印斷點蠢熄,并來到其方法內(nèi)部可以看出imp
中的存儲的地址也就是方法實現(xiàn)的地址跪解。
通過上面的學(xué)習(xí)我們知道了方法列表是如何存儲在Class類對象
中的,但是當(dāng)多次繼承的子類想要調(diào)用基類方法時签孔,就需要通過superclass
指針一層一層找到基類叉讥,在從基類方法列表中找到對應(yīng)的方法進(jìn)行調(diào)用。如果多次調(diào)用基類方法饥追,那么就需要多次遍歷每一層父類的方法列表图仓,這對性能來說無疑是傷害巨大的。
apple通過方法緩存的形式解決了這一問題但绕,接下來我們來探尋Class類對象
是如何進(jìn)行方法緩存的
方法緩存 cache_t
回到類對象結(jié)構(gòu)體救崔,成員變量cache
就是用來對方法進(jìn)行緩存的。
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;
用來緩存曾經(jīng)調(diào)用過的方法捏顺,可以提高方法的查找速度六孵。
回顧方法調(diào)用過程:調(diào)用方法的時候,需要去方法列表里面進(jìn)行遍歷查找幅骄。如果方法不在列表里面劫窒,就會通過superclass
找到父類的類對象,在去父類類對象方法列表里面遍歷查找昌执。
如果方法需要調(diào)用很多次的話烛亦,那就相當(dāng)于每次調(diào)用都需要去遍歷多次方法列表,為了能夠快速查找方法懂拾,apple
設(shè)計了cache_t
來進(jìn)行方法緩存煤禽。
每當(dāng)調(diào)用方法的時候,會先去cache
中查找是否有緩存的方法岖赋,如果沒有緩存檬果,在去類對象方法列表中查找,以此類推直到找到方法之后唐断,就會將方法直接存儲在cache
中选脊,下一次在調(diào)用這個方法的時候,就會在類對象的cache
里面找到這個方法脸甘,直接調(diào)用了恳啥。
cache_t 如何進(jìn)行緩存
那么cache_t
是如何對方法進(jìn)行緩存的呢?首先來看一下cache_t
的內(nèi)部結(jié)構(gòu)丹诀。
struct cache_t {
struct bucket_t *_buckets; // 散列表 數(shù)組
mask_t _mask; // 散列表的長度 -1
mask_t _occupied; // 已經(jīng)緩存的方法數(shù)量
};
bucket_t
是以數(shù)組的方式存儲方法列表的钝的,看一下bucket_t
內(nèi)部結(jié)構(gòu)
struct bucket_t {
private:
cache_key_t _key; // SEL作為Key
IMP _imp; // 函數(shù)的內(nèi)存地址
};
從源碼中可以看出bucket_t
中存儲著SEL
和_imp
,通過key->value
的形式铆遭,以SEL
為key
硝桩,函數(shù)實現(xiàn)的內(nèi)存地址 _imp
為value
來存儲方法。
通過一張圖來展示一下cache_t
的結(jié)構(gòu)枚荣。
上述bucket_t
列表我們稱之為散列表(哈希表)
散列表(Hash table碗脊,也叫哈希表),是根據(jù)關(guān)鍵碼值(Key value)而直接進(jìn)行訪問的數(shù)據(jù)結(jié)構(gòu)橄妆。也就是說衙伶,它通過把關(guān)鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度呼畸。這個映射函數(shù)叫做散列函數(shù)痕支,存放記錄的數(shù)組叫做散列表。
那么apple如何在散列表中快速并且準(zhǔn)確的找到對應(yīng)的key以及函數(shù)實現(xiàn)呢蛮原?這就需要我們通過源碼來看一下apple的散列函數(shù)是如何設(shè)計的卧须。
散列函數(shù)及散列表原理
首先來看一下存儲的源碼,主要查看幾個函數(shù)儒陨,關(guān)鍵代碼都有注釋花嘶,不在贅述。
cache_fill 及 cache_fill_nolock 函數(shù)
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()) {
// 如果為空的,則創(chuàng)建空間笛园,這里創(chuàng)建的空間為4個隘击。
cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE);
}
else if (newOccupied <= capacity / 4 * 3) {
// 如果所占用的空間占總數(shù)的3/4一下侍芝,則繼續(xù)使用現(xiàn)在的空間
}
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 函數(shù)
通過上述源碼看到reallocate
函數(shù)負(fù)責(zé)分配散列表空間,來到reallocate
函數(shù)內(nèi)部凶赁。
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
// 舊的散列表能否被釋放
bool freeOld = canBeFreed();
// 獲取舊的散列表
bucket_t *oldBuckets = buckets();
// 通過新的空間需求量創(chuàng)建新的散列表
bucket_t *newBuckets = allocateBuckets(newCapacity);
assert(newCapacity > 0);
assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1);
// 設(shè)置Buckets和Mash咧栗,Mask的值為散列表長度-1
setBucketsAndMask(newBuckets, newCapacity - 1);
// 釋放舊的散列表
if (freeOld) {
cache_collect_free(oldBuckets, oldCapacity);
cache_collect(false);
}
}
上述源碼中首次傳入reallocate
函數(shù)的newCapacity
為INIT_CACHE_SIZE
,INIT_CACHE_SIZE
是個枚舉值虱肄,也就是4致板。因此散列表最初創(chuàng)建的空間就是4個。
enum {
INIT_CACHE_SIZE_LOG2 = 2,
INIT_CACHE_SIZE = (1 << INIT_CACHE_SIZE_LOG2)
};
expand ()函數(shù)
當(dāng)散列表的空間被占用超過3/4的時候咏窿,散列表會調(diào)用expand ()
函數(shù)進(jìn)行擴展斟或,我們來看一下expand ()
函數(shù)內(nèi)散列表如何進(jìn)行擴展的。
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;
}
// 調(diào)用reallocate函數(shù)翰灾,重新創(chuàng)建存儲空間
reallocate(oldCapacity, newCapacity);
}
上述源碼中可以發(fā)現(xiàn)散列表進(jìn)行擴容時會將容量增至之前的2倍缕粹。
find 函數(shù)
最后來看一下散列表中如何快速的通過key
找到相應(yīng)的bucket
呢?我們來到find
函數(shù)內(nèi)部
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在散列表中存儲的下標(biāo)
mask_t begin = cache_hash(k, m);
// 將下標(biāo)賦值給i
mask_t i = begin;
// 如果下標(biāo)i中存儲的bucket的key==0說明當(dāng)前沒有存儲相應(yīng)的key纸淮,將b[i]返回出去進(jìn)行存儲
// 如果下標(biāo)i中存儲的bucket的key==k平斩,說明當(dāng)前空間內(nèi)已經(jīng)存儲了相應(yīng)key,將b[i]返回出去進(jìn)行存儲
do {
if (b[i].key() == 0 || b[i].key() == k) {
// 如果滿足條件則直接reutrn出去
return &b[i];
}
// 如果走到這里說明上面不滿足咽块,那么會往前移動一個空間重新進(jìn)行判定绘面,知道可以成功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);
}
函數(shù)cache_hash (k, m)
用來通過key
找到方法在散列表中存儲的下標(biāo),來到cache_hash (k, m)
函數(shù)內(nèi)部
static inline mask_t cache_hash(cache_key_t key, mask_t mask)
{
return (mask_t)(key & mask);
}
可以發(fā)現(xiàn)cache_hash (k, m)
函數(shù)內(nèi)部僅僅是進(jìn)行了key & mask
的按位與運算侈沪,得到下標(biāo)即存儲在相應(yīng)的位置上揭璃。按位與運算在上文中已詳細(xì)講解過,這里不在贅述亭罪。
_mask
通過上面的分析我們知道_mask
的值是散列表的長度減一瘦馍,那么任何數(shù)通過與_mask
進(jìn)行按位與運算之后獲得的值都會小于等于_mask
,因此不會出現(xiàn)數(shù)組溢出的情況应役。
舉個例子情组,假設(shè)散列表的長度為8,那么mask的值為7
0101 1011 // 任意值
& 0000 0111 // mask = 7
------------
0000 0011 //獲取的值始終等于或小于mask的值
總結(jié)
當(dāng)?shù)谝淮问褂梅椒〞r箩祥,消息機制通過isa找到方法之后院崇,會對方法以SEL為keyIMP為value
的方式緩存在cache
的_buckets
中,當(dāng)?shù)谝淮未鎯Φ臅r候袍祖,會創(chuàng)建具有4個空間的散列表底瓣,并將_mask
的值置為散列表的長度減一,之后通過SEL & mask
計算出方法存儲的下標(biāo)值蕉陋,并將方法存儲在散列表中捐凭。舉個例子拨扶,如果計算出下標(biāo)值為3,那么就將方法直接存儲在下標(biāo)為3的空間中茁肠,前面的空間會留空屈雄。
當(dāng)散列表中存儲的方法占據(jù)散列表長度超過3/4的時候,散列表會進(jìn)行擴容操作官套,將創(chuàng)建一個新的散列表并且空間擴容至原來空間的兩倍,并重置_mask
的值蚁孔,最后釋放舊的散列表奶赔,此時再有方法要進(jìn)行緩存的話,就需要重新通過SEL & mask
計算出下標(biāo)值之后在按照下標(biāo)進(jìn)行存儲了杠氢。
如果一個類中方法很多站刑,其中很可能會出現(xiàn)多個方法的SEL & mask
得到的值為同一個下標(biāo)值,那么會調(diào)用cache_next
函數(shù)往下標(biāo)值-1位去進(jìn)行存儲鼻百,如果下標(biāo)值-1位空間中有存儲方法绞旅,并且key不與要存儲的key相同,那么再到前面一位進(jìn)行比較温艇,直到找到一位空間沒有存儲方法或者key
與要存儲的key
相同為止因悲,如果到下標(biāo)0的話就會到下標(biāo)為_mask
的空間也就是最大空間處進(jìn)行比較。
當(dāng)要查找方法時勺爱,并不需要遍歷散列表晃琳,同樣通過SEL & mask
計算出下標(biāo)值,直接去下標(biāo)值的空間取值即可琐鲁,同上卫旱,如果下標(biāo)值中存儲的key與要查找的key不相同,就去前面一位查找围段。這樣雖然占用了少量控件顾翼,但是大大節(jié)省了時間,也就是說其實apple是使用空間換取了存取的時間奈泪。
通過一張圖更清晰的看一下其中的流程适贸。
驗證上述流程
通過一段代碼演示一下 。同樣使用仿照objc_class
結(jié)構(gòu)體自定義一個結(jié)構(gòu)體段磨,并進(jìn)行強制轉(zhuǎn)化來查看其內(nèi)部數(shù)據(jù)取逾,自定義結(jié)構(gòu)體在之前的文章中使用過多次這里不在贅述。
我們創(chuàng)建Person
類繼承NSObject
苹支,Student
類繼承Person
砾隅,CollegeStudent
繼承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
實例對象調(diào)用personTest究反,studentTest,colleaeStudentTest
方法處打斷點查看cache
的變化儒洛。
personTest
方法調(diào)用之前
從上圖中可以發(fā)現(xiàn)精耐,personTest
方法調(diào)用之前,cache
中僅僅存儲了init方法
琅锻,上圖中可以看出init方法
恰好存儲在下標(biāo)為0的位置因此我們可以看到卦停,_mask
的值為3驗證我們上述源碼中提到的散列表第一次存儲時會分配4個內(nèi)存空間,_occupied
的值為1證明此時_buckets
中僅僅存儲了一個方法恼蓬。
當(dāng)collegeStudent
在調(diào)用personTest
的時候惊完,首先發(fā)現(xiàn)collegeStudent類對象
的cache
中沒有personTest方法
,就會去collegeStudent類對象
的方法列表中查找处硬,方法列表中也沒有小槐,那么就通過superclass指針
找到Student類對象
,Studeng類對象
中cache
和方法列表同樣沒有荷辕,再通過superclass指針
找到Person類對象
凿跳,最終在Person類對象
方法列表中找到之后進(jìn)行調(diào)用,并緩存在collegeStudent類對象
的cache
中疮方。
執(zhí)行personTest
方法之后查看cache
方法的變化
上圖中可以發(fā)現(xiàn)_occupied
值為2控嗜,說明此時personTest
方法已經(jīng)被緩存在collegeStudent類對象
的cache
中。
同理執(zhí)行過studentTest
方法之后骡显,我們通過打印查看一下此時cache
內(nèi)存儲的信息
上圖中可以看到cache
中確實存儲了 init 躬审、personTest 、studentTest
三個方法蟆盐。
那么執(zhí)行過colleaeStudentTest方法
之后此時cache
中應(yīng)該對colleaeStudentTest方法
進(jìn)行緩存承边。上面源碼提到過,當(dāng)存儲的方法數(shù)超過散列表長度的3/4時石挂,系統(tǒng)會重新創(chuàng)建一個容量為原來兩倍的新的散列表替代原來的散列表博助。過掉colleaeStudentTest方法
,重新打印cache
內(nèi)存儲的方法查看痹愚。
可以看出上圖中_bucket
散列表擴容之后僅僅存儲了colleaeStudentTest方法
富岳,并且上圖中打印SEL & _mask
位運算得出下標(biāo)的值確實是_bucket
列表中colleaeStudentTest方法
存儲的位置。
至此已經(jīng)對Class的結(jié)構(gòu)及方法緩存的過程有了新的認(rèn)知拯腮,apple通過散列表的形式對方法進(jìn)行緩存窖式,以少量的空間節(jié)省了大量查找方法的時間。
底層原理相關(guān)文章
文中如果有不對的地方歡迎指出动壤。我是xx_cc萝喘,一只長大很久但還沒有二夠的家伙。