今天研究一下 OC
中方法的底層實(shí)現(xiàn)原理,在研究method
之前,我們先搞清楚Class
的底層數(shù)據(jù)結(jié)構(gòu).
先用一張圖說明類的底層數(shù)據(jù)結(jié)構(gòu),然后我們在從runtime
源碼中驗(yàn)證:
我們在runtime
源碼中搜索struct objc_class {
知道類的底層數(shù)據(jù)結(jié)構(gòu)主要如下:
struct objc_class {
Class ISA;
Class superclass;
cache_t cache; // 方法緩存
class_data_bits_t bits; // 獲取具體的類信息
}
class_data_bits_t bits
中存儲具體的類的信息,在class_data_bits_t
結(jié)構(gòu)體內(nèi)部仔細(xì)查找,發(fā)現(xiàn)有這么一句代碼:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
class_rw_t
是可讀可寫的表,里面存儲著類和分類的信息:
struct class_rw_t {
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;
}
class_ro_t
是只讀表,里面存儲著類的原始信息:
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;//instance對象占用的內(nèi)存空間,class_getInstanceSize
#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;
}
};
實(shí)際上,一開始的時(shí)候,類中是沒有class_rw_t
的,是runtime
后來創(chuàng)建的,這一點(diǎn)我們也可以從源碼中找到:
現(xiàn)在結(jié)合
rutime
源碼和截圖,我們總結(jié)一下class
底層結(jié)構(gòu)關(guān)系:
- 1:
Class
底層結(jié)構(gòu)體主要有4個成員變量:isa , superClass , catche , bits
. - 2:
catche
中存儲的調(diào)用過的方法的緩存,這個我們下面會講. - 3:
bits & FAST_DATA_MASK
會得到一個可讀可寫的數(shù)據(jù)表:class_rw_t
,class_rw_t
用來存儲類原始信息和分類附加的信息.需要注意的是,class_rw_t
這個表一開始是不存在的,后來需要的時(shí)候才創(chuàng)建的. - 4:
class_ro_t
是只可讀的數(shù)據(jù)表,它里面存儲著類原始的信息
OC
方法的調(diào)用順序是如果是調(diào)用實(shí)例方法,就通過實(shí)例對象的isa
指針找到類對象,從類對象的方法列表中查找,如果如果沒找到,在通過superClass
從父類的方法列表中查找,這樣一層一層往上找;如果是調(diào)用類方法,就通過類對象的isa
找到元類,從元類的方法列表中查找,如果沒找到再打通過superClass
到元類的父類中查找...
但是我們想想,如果一個方法調(diào)用的很頻繁,難道每次都要通過這種方式一遍遍查找嗎?顯然這種方式是低效的,runtime
采用了一種更高效的方式來處理這種情況:如果方法第一次被調(diào)用后,會緩存到 cache 中,下次再調(diào)用的時(shí)候直接從 cache 中查找.
cache
的底層結(jié)構(gòu)如下:
bucket_t
的底層結(jié)構(gòu)如下:buckets
就是一個數(shù)組,里面存放著一個一個的bucket_t
:那么
runtime
是如何從cache
中查找方法的呢?難道也是遍歷buckets
數(shù)組嗎?肯定不是,遍歷數(shù)組的那不就跟沒優(yōu)化一樣嗎?cache
的工作原理是:采用散列表的方式把方法插入到buckets
時(shí),會用 SEL & _mask
得到一個索引值
,直接把bucket_t
插入到索引值
所在的位置.這樣的話就不用每次一個個遍歷去查找方法,效率很高.但是這樣會有個問題:
SEL & _mask 得到索引值并不是按順序的,他是無序的.比如說:如果 SEL & _mask = 20,那么前面 19 個內(nèi)存單元就要置為 null 了,這就是 散列表的弊端,犧牲空間換時(shí)間.
這種方式雖然效率大大提升了,但是會有個弊端:如果兩個
SEL
按位與 _mask
得到的索引相同怎么辦?這種情況是很可能發(fā)生的.我們從runtime
源代碼中看看是怎么處理這種情況的.在
cache_t
結(jié)構(gòu)體中有一個struct bucket_t * find(cache_key_t key, id receiver)
方法,這個方法里面就是從buckets
查找方法的邏輯:繼續(xù)進(jìn)入
chche_next()
:從上圖可以看到,如果索引相等,從
buckets
中找到bucket_t
,然后取出bucket_t
中的key
和傳入的key
判斷,如果兩個key
不相等,在arm64
環(huán)境下,會先把i - 1
后再&_mask
得到一個新的索引,繼續(xù)查找,直到找到兩個key
相等位置.現(xiàn)在我們已經(jīng)知道了
Class
的底層數(shù)據(jù)機(jī)構(gòu)以及runtime
是如何存儲和查找方法的.下面我們將研究一下method_t
:-
SEL
: 方法名,函數(shù)名,一般叫做選擇器,底層結(jié)構(gòu)跟char *
類似
· 可以通過@selector
和sel_registerName()
獲得.
· 可以通過sel_getName()
和NSStringFromSelector()
轉(zhuǎn)成字符串.
· 不同類中相同名字的方法,所對應(yīng)的方法選擇器是相同的. IMP
: 函數(shù)的具體實(shí)現(xiàn)-
types
: 包含了函數(shù)的返回值類型,參數(shù)類型編碼的字符串
例如我們隨便聲明一個函數(shù)- (void)test;
,他的types
就是v16@0:8
.代表的意思是:
types 解釋圖
有人可能會覺得奇怪,- (void)test
方法并沒有參數(shù)呀,為什么types
會多出兩個參數(shù)?
實(shí)際上,每一個OC
方法都會默認(rèn)有兩個參數(shù),比如說- (void)test
的完整形式就是:- (void)test:(id self SEL sel)
,這也就是為什么我們能在每個方法中調(diào)用self
,其實(shí)是方法參數(shù).至于為什么void
用v
表示,id
用@
,這都是蘋果規(guī)定的,在蘋果官方文檔中有對照表:
對照表
另外iOS
中還提供了一個@encode()
指令,可以將具體類型轉(zhuǎn)換成字符串編碼.
總結(jié):
- OC 方法在第一次調(diào)用后,會被添加到
cache_t
緩存中,下次調(diào)用時(shí)直接從緩存中查找. -
cache_t
中有三個成員變量buckets
,_mask
,_occupied
:
buckets
是一個數(shù)組,存放著一個個的bucket_t
.
_mask
是buckets
數(shù)組的數(shù)量減 1,外部傳入的 SEL & _mask
得到buckets
數(shù)組中的索引(下標(biāo)).
_occupied
:已經(jīng)緩存的方法. -
bucket_t
有兩個成員key
,imp
key
就是SEL
;imp
就是方法的實(shí)現(xiàn)地址. -
OC 方法的底層是
method_t 結(jié)構(gòu)體
,主要有三個成員:
name
:函數(shù)名稱;
types
:編碼,(函數(shù)返回值類型和參數(shù)類型);
imp
:函數(shù)地址
驗(yàn)證:
上面講的都是從源碼中推測出來的理論,實(shí)際上是不是這樣呢?我們自己敲代碼驗(yàn)證一番.
我們創(chuàng)建3個類Son , Mother , Person
,他們之間的繼承關(guān)系是:Son : Mother : Person
,這3個類中都有一個- (void)personTest;
方法.
_buckets
擴(kuò)容:
現(xiàn)在我們更改一下代碼,創(chuàng)建一個Son
的實(shí)例對象son
,分別調(diào)用Son , Mother , Person
的test
方法:
我們過掉斷點(diǎn)看看會發(fā)生什么:
從結(jié)果中我們可以看到
_mask
的數(shù)量從4變成了8,并且_buckets
中之前緩存的方法也沒有了,只緩存了一個方法personTest
.這是由于
_buckets
的擴(kuò)容機(jī)制造成的.我們在objc-cache.mm
中查找void cache_t::expand()
方法:我們在進(jìn)入
reallocate
方法:OK,通過上面兩張圖我們知道了buckets
是如何擴(kuò)展容量的:如果 buckets 的容量不夠用了,就直接用舊容量 乘以 2 ,重新分配內(nèi)存空間.并且把舊的緩存方法都清除.