Runtime原理
Runtime是iOS核心運(yùn)行機(jī)制之一,iOS App加載庫、加載類、執(zhí)行方法調(diào)用,全靠Runtime,這一塊的知識(shí)個(gè)人認(rèn)為是最基礎(chǔ)的,基本面試必問。
Runtime消息發(fā)送機(jī)制
1)iOS調(diào)用一個(gè)方法時(shí),實(shí)際上會(huì)調(diào)用objc_msgSend(receiver, selector, arg1, arg2, ...),該方法第一個(gè)參數(shù)是消息接收者,第二個(gè)參數(shù)是方法名,剩下的參數(shù)是方法參數(shù);
2)iOS調(diào)用一個(gè)方法時(shí),會(huì)先去該類的方法緩存列表里面查找是否有該方法,如果有直接調(diào)用,否則走第3)步;
3)去該類的方法列表里面找,找到直接調(diào)用,把方法加入緩存列表;否則走第4)步;
4)沿著該類的繼承鏈繼續(xù)查找,找到直接調(diào)用,把方法加入緩存列表;否則消息轉(zhuǎn)發(fā)流程;
很多面試者大體知道這個(gè)流程,但是有關(guān)細(xì)節(jié)不是特別清楚。
- 問他/她objc_msgSend第一個(gè)參數(shù)、第二個(gè)參數(shù)、剩下的參數(shù)分別代表什么,不知道;
- 很多人只知道去方法列表里面查找,不知道還有個(gè)方法緩存列表。
通過這些細(xì)節(jié),可以了解一個(gè)人是否真正掌握了原理,而不是死記硬背。
Runtime消息轉(zhuǎn)發(fā)機(jī)制
如果在消息發(fā)送階段沒有找到方法,iOS會(huì)走消息轉(zhuǎn)發(fā)流程,流程圖如下所示:

1)動(dòng)態(tài)消息解析。檢查是否重寫了resolveInstanceMethod 方法,如果返回YES則可以通過class_addMethod 動(dòng)態(tài)添加方法來處理消息,否則走第2)步;
2)消息target轉(zhuǎn)發(fā)。forwardingTargetForSelector 用于指定哪個(gè)對(duì)象來響應(yīng)消息。如果返回nil 則走第3)步;
3)消息轉(zhuǎn)發(fā)。這步調(diào)用 methodSignatureForSelector 進(jìn)行方法簽名,這可以將函數(shù)的參數(shù)類型和返回值封裝。如果返回 nil 執(zhí)行第四步;否則返回 methodSignature,則進(jìn)入 forwardInvocation ,在這里可以修改實(shí)現(xiàn)方法,修改響應(yīng)對(duì)象等,如果方法調(diào)用成功,則結(jié)束。否則執(zhí)行第4)步;
4)報(bào)錯(cuò) unrecognized selector sent to instance。
很多人知道這四步,但是筆者一般會(huì)問:
- 怎么在項(xiàng)目里全局解決"unrecognized selector sent to instance"這類crash?本人發(fā)現(xiàn)很多人回答不出來,說明面試者肯定是在死記硬背,你都知道因?yàn)橄⑥D(zhuǎn)發(fā)那三步都沒處理才會(huì)報(bào)錯(cuò),為什么不知道在消息轉(zhuǎn)發(fā)里面處理呢?
- 如果面試者知道可以在消息轉(zhuǎn)發(fā)里面處理,防止崩潰,再問下面試者,你項(xiàng)目中是在哪一步處理的,看看其是否有真正實(shí)踐過?
消息緩存機(jī)制
- Runtime為每個(gè)類(不是每個(gè)類實(shí)例)緩存了一個(gè)方法列表,該方法列表采用hash表實(shí)現(xiàn),hash表的優(yōu)點(diǎn)是查找速度快,時(shí)間為O(1)。
- 父類方法的緩存只存在父類么,還是子類也會(huì)緩存父類的方法?
子類會(huì)緩存父類的方法。 - 類的方法緩存大小有沒有限制?
在objc-cache.mm有一個(gè)變量_class_slow_grow定義如下:
/* When _class_slow_grow is non-zero, any given cache is actually grown
* only on the odd-numbered times it becomes full; on the even-numbered
* times, it is simply emptied and re-used. When this flag is zero,
* caches are grown every time. */
static const int _class_slow_grow = 1;
注釋中說明,當(dāng)_class_slow_grow是非0值的時(shí)候,只有當(dāng)方法緩存第奇數(shù)次滿(使用的槽位超過3/4)的時(shí)候,方法緩存的大小才會(huì)增長(zhǎng)(會(huì)清空緩存,否則hash值就不對(duì)了);當(dāng)?shù)谂紨?shù)次滿的時(shí)候,方法緩存會(huì)被清空并重新利用。 如果_class_slow_grow值為0,那么每一次方法緩存滿的時(shí)候,其大小都會(huì)增長(zhǎng)。
所以單就問題而言,答案是沒有限制,雖然這個(gè)值被設(shè)置為1,方法緩存的大小增速會(huì)慢一點(diǎn),但是確實(shí)是沒有上限的。
- 為什么類的方法列表不直接做成散列表呢,做成list,還要單獨(dú)緩存?
1、散列表是沒有順序的,Objective-C的方法列表是一個(gè)list,是有順序的;Objective-C在查找方法的時(shí)候會(huì)順著list依次尋找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的順序就沒法保證。
2、list的方法還保存了除了selector和imp之外其他很多屬性
3、散列表是有空槽的,會(huì)浪費(fèi)空間
參考資料:深入理解 Objective-C:方法緩存
load與initialize
load與initialize調(diào)用時(shí)機(jī)
+load在main函數(shù)之前被Runtime調(diào)用,+initialize 方法是在類或它的子類收到第一條消息之前被調(diào)用的,這里所指的消息包括實(shí)例方法和類方法的調(diào)用。
load與initialize在分類、繼承鏈的調(diào)用順序
load方法調(diào)用順序
父類->主類->分類
- 主類的 +load 方法會(huì)在它的所有父類的 +load 方法之后執(zhí)行。如果主類沒有實(shí)現(xiàn) +load 方法,當(dāng)它被runtime加載時(shí) 是不會(huì)去調(diào)用父類的 +load 方法的。
- 分類的 +load 方法會(huì)在它的主類的 +load 方法之后執(zhí)行,當(dāng)一個(gè)類和它的分類都實(shí)現(xiàn)了 +load 方法時(shí),兩個(gè)方法都會(huì)被調(diào)用。當(dāng)有多個(gè)分類時(shí),根據(jù)編譯順序(Build Phases->Complie Sources中的順序)依次執(zhí)行。
- 在類的+load方法調(diào)用的時(shí)候,可以調(diào)用category中聲明的方法么?
可以調(diào)用,因?yàn)楦郊觕ategory到類的工作會(huì)先于+load方法的執(zhí)行
initialize的調(diào)用順序
+initialize 方法的調(diào)用與普通方法的調(diào)用是一樣的,走的都是消息發(fā)送的流程。如果子類沒有實(shí)現(xiàn) +initialize 方法,那么繼承自父類的實(shí)現(xiàn)會(huì)被調(diào)用;如果一個(gè)類的分類實(shí)現(xiàn)了 +initialize 方法,那么就會(huì)對(duì)這個(gè)類中的實(shí)現(xiàn)造成覆蓋。
確保在load和initialize的調(diào)用只執(zhí)行一次
由于initialize可能會(huì)調(diào)用多次,所以在這兩個(gè)方法里面做的初始化操作需要保證只初始化一次,用dispatch_once來控制
類別
OC不像C++等高級(jí)語言能直接繼承多個(gè)類,不過OC可以使用類別和協(xié)議來實(shí)現(xiàn)多繼承。
類別加載時(shí)機(jī)
- 在App加載時(shí),Runtime會(huì)把Category的實(shí)例方法、協(xié)議以及屬性添加到類上;把Category的類方法添加到類的metaclass上。
- category的方法沒有“完全替換掉”原來類已經(jīng)有的方法,如果category和原來類都有methodA,那么category附加完成之后,類的方法列表里會(huì)有兩個(gè)methodA。
- category的方法被放到了新方法列表的前面,而原來類的方法被放到了新方法列表的后面,這也就是我們平常所說的category的方法會(huì)“覆蓋”掉原來類的同名方法,這是因?yàn)檫\(yùn)行時(shí)在查找方法的時(shí)候是順著方法列表的順序查找的,它只要一找到對(duì)應(yīng)名字的方法,就會(huì)停止查找,殊不知后面可能還有一樣名字的方法。
類別和擴(kuò)展區(qū)別
- extension在編譯期決議,它是類的一部分,在編譯期和頭文件里的@interface以及實(shí)現(xiàn)文件里的@implement一起形成一個(gè)完整的類,它伴隨類的產(chǎn)生而產(chǎn)生,亦隨之一起消亡。extension一般用來隱藏類的私有信息,你必須有一個(gè)類的源碼才能為一個(gè)類添加extension,所以你無法為系統(tǒng)的類比如NSString添加extension。
- 但是category則完全不一樣,它是在運(yùn)行期決議的。 就category和extension的區(qū)別來看,我們可以推導(dǎo)出一個(gè)明顯的事實(shí),extension可以添加實(shí)例變量,而category是無法添加實(shí)例變量的(因?yàn)樵谶\(yùn)行期,對(duì)象的內(nèi)存布局已經(jīng)確定,如果添加實(shí)例變量就會(huì)破壞類的內(nèi)部布局,這對(duì)編譯型語言來說是災(zāi)難性的)。
- category附加到類的工作會(huì)先于+load方法的執(zhí)行。
類別添加屬性、方法
- 在類別中不能直接以@property的方式定義屬性,OC不會(huì)主動(dòng)給類別屬性生成setter和getter方法;需要通過objc_setAssociatedObject來實(shí)現(xiàn)。
@interface TestClass(ak)
@property(nonatomic,copy) NSString *name;
@end
@implementation TestClass (ak)
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_COPY);
}
- (NSString*)name{
NSString *nameObject = objc_getAssociatedObject(self, @selector(name));
return nameObject;
}
- objc_setAssociatedObject key的定義用@selector(屬性名)這樣能保證key的唯一。
- 關(guān)聯(lián)對(duì)象都由AssociationsManager管理。AssociationsManager里面是由一個(gè)靜態(tài)AssociationsHashMap來存儲(chǔ)所有的關(guān)聯(lián)對(duì)象的。這相當(dāng)于把所有對(duì)象的關(guān)聯(lián)對(duì)象都存在一個(gè)全局map里面。而map的的key是這個(gè)對(duì)象的指針地址(任意兩個(gè)不同對(duì)象的指針地址一定是不同的),而這個(gè)map的value又是另外一個(gè)AssociationsHashMap,里面保存了關(guān)聯(lián)對(duì)象的kv對(duì)。
runtime的銷毀對(duì)象函數(shù)objc_destructInstance里面會(huì)判斷這個(gè)對(duì)象有沒有關(guān)聯(lián)對(duì)象,如果有,會(huì)調(diào)用_object_remove_assocations做關(guān)聯(lián)對(duì)象的清理工作
類別同名方法覆蓋問題
- 如果類別和主類都有名叫funA的方法,那么在類別加載完成之后,類的方法列表里會(huì)有兩個(gè)funA;
- 類別的方法被放到了新方法列表的前面,而主類的方法被放到了新方法列表的后面,這就造成了類別方法會(huì)“覆蓋”掉原來類的同名方法,這是因?yàn)檫\(yùn)行時(shí)在查找方法的時(shí)候是順著方法列表的順序查找的,它只要一找到對(duì)應(yīng)名字的方法,就會(huì)停止查找;
- 如果多個(gè)類別定義了同名方法funA,具體調(diào)用哪個(gè)類別的實(shí)現(xiàn)由編譯順序決定(Build Phases->Complie Sources中的順序),后編譯的類別的實(shí)現(xiàn)將被調(diào)用。
- 在日常開發(fā)過程中,類別方法重名輕則造成調(diào)用不正確,重則造成crash,我們可以通過給類別方法名加前綴避免方法重名。
怎么調(diào)用被覆蓋掉的方法
category其實(shí)并不是完全替換掉原來類的同名方法,只是category在方法列表的前面而已,所以我們只要順著方法列表找到最后一個(gè)對(duì)應(yīng)名字的方法,就可以調(diào)用原來類的方法。
Class currentClass = [TestClass class];
TestClass *my = [[TestClass alloc] init];
if (currentClass) {
unsigned int methodCount;
Method *methodList = class_copyMethodList(currentClass, &methodCount);
IMP lastImp = NULL;
SEL lastSel = NULL;
for (NSInteger i = 0; i < methodCount; i++) {
Method method = methodList[i];
NSString *methodName = [NSString stringWithCString:sel_getName(method_getName(method))
encoding:NSUTF8StringEncoding];
if ([@"printName" isEqualToString:methodName]) {
lastImp = method_getImplementation(method);
lastSel = method_getName(method);
}
}
typedef void (*fn)(id,SEL);
if (lastImp != NULL) {
fn f = (fn)lastImp;
f(my, lastSel);
}
free(methodList);
}
關(guān)于類別更深入的解析可以參見美團(tuán)的技術(shù)文章深入理解Objective-C:Category
協(xié)議
定義
iOS中的協(xié)議類似于Java、C++中的接口類,協(xié)議在OC中可以用來實(shí)現(xiàn)多繼承和代理。
方法聲明
協(xié)議中的方法可以聲明為@required(要求實(shí)現(xiàn),如果沒有實(shí)現(xiàn),會(huì)發(fā)出警告,但編譯不報(bào)錯(cuò))或者@optional(不要求實(shí)現(xiàn),不實(shí)現(xiàn)也不會(huì)有警告)。如果不聲明,默認(rèn)為@required。
筆者經(jīng)常會(huì)問面試者如下兩個(gè)問題:
-怎么判斷一個(gè)類是否實(shí)現(xiàn)了某個(gè)協(xié)議?很多人不知道可以通過conformsToProtocol來判斷。
-假如你要求業(yè)務(wù)方實(shí)現(xiàn)一個(gè)delegate,你怎么判斷業(yè)務(wù)方有沒有實(shí)現(xiàn)dalegate的某個(gè)方法?很多人不知道可以通過respondsToSelector來判斷。
其他
Class的定義
在oc中打開objc.h
typedef struct objc_class *Class; //Class是指向結(jié)構(gòu)體objc_class的指針
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; //isa,代表的是該類類對(duì)象
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE; //父類
const char * _Nonnull name OBJC2_UNAVAILABLE; //類名
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE; //對(duì)象大小
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; //成員變量列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; //實(shí)例方法列表
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; //方法緩存列表(是個(gè)hash表),用來消息發(fā)送時(shí)候,快速查找方法
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; //類實(shí)現(xiàn)協(xié)議列表
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
怎么枚舉一個(gè)類的方法列表?
class_copyMethodList
怎么枚舉一個(gè)類的屬性列表?
class_copyPropertyList
怎么枚舉一個(gè)類的成員變量列表?
class_copyIvarList
怎么枚舉一個(gè)類實(shí)現(xiàn)的協(xié)議列表?
class_copyProtocolList
id和instancetype的區(qū)別
- id能用做返回值、參數(shù)。instancetype只能用做返回值。
- instancetype是類型相關(guān)的,如果把一個(gè)instancetype的對(duì)象賦值給另外類,編譯器會(huì)警告。id不會(huì)。
Runtime開源代碼
runtime是開源的,可以在Apple Github和Apple OpenSource下載來閱讀。
參考資料:
Objective-C中的Runtime