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_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
是二維陣列,是可讀可寫的,其中包含了類的初始內容以及分類的內容。
這裡以method_array_t
為例,圖示其中的結構。
class_ro_t
我們之前提到過class_ro_t
中也有儲存方法、屬性、協議列表,另外還有成員變數列表。
接著來看一下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
是隻讀的,內部直接儲存的直接就是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_t
,method_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的值
上圖中可以看出types
的值為v16@0:8
,那麼這個值代表什麼呢?apple為了能夠清晰的使用字串表示方法及其返回值,制定了一系列對應規則,通過下表可以看到一一對應關係
將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的值代表的方法
- (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
中的儲存的地址也就是方法實現的地址。
通過上面的學習我們知道了方法列表是如何儲存在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
的形式,以SEL
為key
,函式實現的記憶體地址 _imp
為value
來儲存方法。
通過一張圖來展示一下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
函式的newCapacity
為INIT_CACHE_SIZE
,INIT_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
類繼承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
例項物件呼叫personTest,studentTest,colleaeStudentTest
方法處打斷點檢視cache
的變化。
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
方法的變化
上圖中可以發現_occupied
值為2,說明此時personTest
方法已經被快取在collegeStudent類物件
的cache
中。
同理執行過studentTest
方法之後,我們通過列印檢視一下此時cache
記憶體儲的資訊
上圖中可以看到cache
中確實儲存了 init 、personTest 、studentTest
三個方法。
那麼執行過colleaeStudentTest方法
之後此時cache
中應該對colleaeStudentTest方法
進行快取。上面原始碼提到過,當儲存的方法數超過雜湊表長度的3/4時,系統會重新建立一個容量為原來兩倍的新的雜湊表替代原來的雜湊表。過掉colleaeStudentTest方法
,重新列印cache
記憶體儲的方法檢視。
可以看出上圖中_bucket
雜湊表擴容之後僅僅儲存了colleaeStudentTest方法
,並且上圖中列印SEL & _mask
位運算得出下標的值確實是_bucket
列表中colleaeStudentTest方法
儲存的位置。
至此已經對Class的結構及方法快取的過程有了新的認知,apple通過雜湊表的形式對方法進行快取,以少量的空間節省了大量查詢方法的時間。
底層原理文章專欄
文中如果有不對的地方歡迎指出。我是xx_cc,一隻長大很久但還沒有二夠的傢伙。需要視訊一起探討學習的coder可以加我Q:2336684744