關(guān)于 Runtime 物独,網(wǎng)上已經(jīng)有很多很好的文章房揭,寫得很詳盡备闲。本篇主要是從新手的角度出發(fā)晌端,逐步介紹 Runtime 的原理、常用方法浅役、應(yīng)用場景等斩松。
相關(guān)鏈接:
蘋果維護(hù)的Runtime開源代碼
GNU維護(hù)一個開源的runtime 版本
官方Api
一、Runtime 是什么
在 C
語言中觉既,將代碼轉(zhuǎn)換為可執(zhí)行程序惧盹,一般要經(jīng)歷三個步驟,即編譯瞪讼、鏈接钧椰、運(yùn)行。在鏈接的時候符欠,對象的類型嫡霞、方法的實(shí)現(xiàn)就已經(jīng)確定好了。
而在 Objective-C
中希柿,卻將一些在編譯和鏈接過程中的工作诊沪,放到了運(yùn)行階段。也就是說曾撤,就算是一個編譯好的 .ipa 包端姚,在程序沒運(yùn)行的時候,也不知道調(diào)用一個方法會發(fā)生什么挤悉。這也為后來大行其道的「熱修復(fù)」提供了可能渐裸。因此我們稱 Objective-C
為一門動態(tài)語言。
這樣的設(shè)計(jì)使 Objective-C
變得靈活装悲,甚至可以讓我們在程序運(yùn)行的時候昏鹃,去動態(tài)修改一個方法的實(shí)現(xiàn)。而實(shí)現(xiàn)這一切的基礎(chǔ)就是 Runtime
诀诊。
簡單來說洞渤, Runtime
是一個庫,這個庫使我們可以在程序運(yùn)行時創(chuàng)建對象属瓣、檢查對象您宪,修改類和對象的方法。
至于這個庫是怎么實(shí)現(xiàn)的奠涌,請緊張刺激地往下看宪巨。
二、Runtime 是怎么工作的
要了解 Runtime
是怎么工作的溜畅,首先要知道類和對象在 Objective-C
中是怎么定義的捏卓。
注意:以下會用到
C
語言中結(jié)構(gòu)體的內(nèi)容,包括結(jié)構(gòu)體的定義、為結(jié)構(gòu)體定義別名等怠晴。如果你對這塊不熟悉遥金,建議先復(fù)習(xí)一下這塊的語法。傳送門
1. Class 和 Object
在 objc.h
中蒜田, Class
被定義為指向 objc_class
的指針稿械,定義如下:
typedef struct objc_class *Class;
而 objc_class
是一個結(jié)構(gòu)體,在 runtime.h
中的定義如下:
struct objc_class {
Class isa; // 實(shí)現(xiàn)方法調(diào)用的關(guān)鍵
Class super_class; // 父類
const char * name; // 類名
long version; // 類的版本信息冲粤,默認(rèn)為0
long info; // 類信息美莫,供運(yùn)行期使用的一些位標(biāo)識
long instance_size; // 該類的實(shí)例變量大小
struct objc_ivar_list * ivars; // 該類的成員變量鏈表
struct objc_method_list ** methodLists; // 方法定義的鏈表
struct objc_cache * cache; // 方法緩存
struct objc_protocol_list * protocols; // 協(xié)議鏈表
};
為了方便理解,我這里去掉了一些聲明梯捕,主要是和
Objective-C
語言版本相關(guān)厢呵,這里可以暫時忽略。完整的定義可以自己去runtime.h
中查看傀顾。
提示:在 Xcode 中襟铭,使用快捷鍵
command + shift + o
,可以打開搜索窗口短曾,輸入objc_class
即可看到頭文件定義寒砖。
可以看到,一個類保存了自身所有的成員變量( ivars
)嫉拐、所有的方法( methodLists
)哩都、所有實(shí)現(xiàn)的協(xié)議( objc_protocol_list
)。
比較重要的字段還有 isa
和 cache
椭岩,它們是什么東西茅逮,先不著急璃赡,我們來看下 Objective-C
中對象的定義判哥。
struct objc_object {
Class isa;
};
typedef struct objc_object *id;
這里看到了我們熟悉的 id
,一般我們用它來實(shí)現(xiàn)類似于 C++
中泛型的一些操作碉考,該類型的對象可以轉(zhuǎn)換為任意一種對象塌计。在這里 id
被定義為一個指向 objc_object
的指針。說明 objc_object
就是我們平時常用的對象的定義
侯谁,它只包含一個 isa
指針锌仅。
也就是說,一個對象唯一保存的信息就是它的 Class 的地址 isa
墙贱。當(dāng)我們調(diào)用一個對象的方法時热芹,它會通過 isa
去找到對應(yīng)的 objc_class
,然后再在 objc_class
的 methodLists
中找到我們調(diào)用的方法惨撇,然后執(zhí)行伊脓。
再說說 cache
,因?yàn)檎{(diào)用方法的過程是個查找 methodLists
的過程魁衙,如果每次調(diào)用都去查找报腔,效率會非常低株搔。所以對于調(diào)用過的方法,會以 map
的方式保存在 cache
中纯蛾,下次再調(diào)用就會快很多纤房。
2. Meta Class 元類
上一小節(jié)講了 Objective-C
中類和對象的定義,也講了調(diào)用對象方法的實(shí)現(xiàn)過程翻诉。但還留下了許多問題炮姨,比如調(diào)用一個對象的類方法的過程是怎么樣的?還有 objc_class
中也有一個 isa
指針米丘,它是干嘛用的剑令?
現(xiàn)在劃重點(diǎn),在 Objective-C
中拄查,類也被設(shè)計(jì)為一個對象吁津。
其實(shí)觀察 objc_class
和 objc_object
的定義,會發(fā)現(xiàn)兩者其實(shí)本質(zhì)相同(都包含 isa
指針)堕扶,只是 objc_class
多了一些額外的字段碍脏。相應(yīng)的,類也是一個對象稍算,只是保存了一些字段典尾。
既然說類也是對象,那么類的類型是什么呢糊探?這里就引出了另外一個概念 —— Meta Class
(元類)钾埂。
在 Objective-C
中,每一個類都有對應(yīng)的元類科平。而在元類的 methodLists
中褥紫,保存了類的方法鏈表,即所謂的「類方法」瞪慧。并且類的 isa
指針指向?qū)?yīng)的元類髓考。因此上面的問題答案就呼之欲出,調(diào)用一個對象的類方法的過程如下:
- 通過對象的
isa
指針找到對應(yīng)的類弃酌。 - 通過類的
isa
指針找到對應(yīng)元類氨菇。 - 在元類的
methodLists
中,找到對應(yīng)的方法妓湘,然后執(zhí)行查蓉。
注意:上面類方法的調(diào)用過程不考慮繼承的情況,這里只是說明一下類方法的調(diào)用原理榜贴,完整的調(diào)用流程在后面會提到豌研。
這么說來元類也有一個 isa
指針,元類也應(yīng)該是一個對象。的確是這樣聂沙。那么元類的 isa
指向哪里呢秆麸?為了不讓這種結(jié)構(gòu)無限延伸下去, Objective-C
的設(shè)計(jì)者讓所有的元類的 isa
指向基類(比如 NSObject
)的元類及汉。而基類的元類的 isa
指向自己沮趣。這樣就形成了一個完美的閉環(huán)。
下面這張圖可以清晰地表示出這種關(guān)系坷随。
同時注意 super_class
的指向房铭,基類的 super_class
指向 nil
。
3. Method
上面講到温眉,「找到對應(yīng)的方法缸匪,然后執(zhí)行」,那么這個「執(zhí)行」是怎樣進(jìn)行的呢类溢?下面就來介紹一下 Objective-C
中的方法調(diào)用凌蔬。
先來看一下 Method
在頭文件中的定義:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name;
char * method_types;
IMP method_imp;
};
Method
被定義為一個 objc_method
指針,在 objc_method
結(jié)構(gòu)體中闯冷,包含一個 SEL
和一個 IMP
砂心,同樣來看一下它們的定義:
// SEL
typedef struct objc_selector *SEL;
// IMP
typedef id (*IMP)(id, SEL, ...);
1、先說一下 SEL
蛇耀。 SEL
是一個指向 objc_selector
的指針辩诞,而 objc_selector
在頭文件中找不到明確的定義。
我們來測試以下代碼:
SEL sel = @selector(viewDidLoad);
NSLog(@"%s", sel); // 輸出:viewDidLoad
SEL sel1 = @selector(viewDidLoad1);
NSLog(@"%s", sel1); // 輸出:viewDidLoad1
可以看到纺涤, SEL
不過是保存了方法名的一串字符译暂。因此我們可以認(rèn)為, SEL
就是一個保存方法名的字符串撩炊。
由于一個 Method
只保存了方法的方法名外永,并最終要根據(jù)方法名來查找方法的實(shí)現(xiàn),因此在 Objective-C
中不支持下面這種定義衰抑。
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
2象迎、再來說 IMP
荧嵌∏河唬可以看到它是一個「函數(shù)指針」。簡單來說啦撮,「函數(shù)指針」就是用來找到函數(shù)地址谭网,然后執(zhí)行函數(shù)。(「函數(shù)指針」了解一下)
這里要注意赃春, IMP
指向的函數(shù)的前兩個參數(shù)是默認(rèn)參數(shù)愉择, id
和 SEL
。這里的 SEL
好理解,就是函數(shù)名锥涕。而 id
衷戈,對于實(shí)例方法來說, self
保存了當(dāng)前對象的地址层坠;對于類方法來說殖妇, self
保存了當(dāng)前對應(yīng)類對象的地址。后面的省略號即是參數(shù)列表破花。
3谦趣、到這里, Method
的結(jié)構(gòu)就很明了了座每。 Method
建立了 SEL
和 IMP
的關(guān)聯(lián)前鹅,當(dāng)對一個對象發(fā)送消息時,會通過給出的 SEL
去找到 IMP
峭梳,然后執(zhí)行舰绘。
在 Objective-C
中,所有的方法調(diào)用
葱椭,都會轉(zhuǎn)化成向?qū)ο蟀l(fā)送消息
除盏。發(fā)送消息主要是使用 objc_msgSend
函數(shù)〈煲裕看一下頭文件定義:
id objc_msgSend(id self, SEL op, ...);
可以看到參數(shù)列表和 IMP
指向的函數(shù)參數(shù)列表是相對應(yīng)的者蠕。 Runtime
會將方法調(diào)用做下面的轉(zhuǎn)換,所以一般也稱 Objective-C
中的調(diào)用方法為「發(fā)送消息」掐松。
[self doSomething];
objc_msgSend(self, @selector(doSomething));
4踱侣、上面看到 objc_msgSend
會默認(rèn)傳入 id
和 SEL
。這對應(yīng)了兩個隱含參數(shù)大磺, self
和 _cmd
抡句。這意味著我們可以在方法的實(shí)現(xiàn)過程中拿到它們,并使用它們杠愧。下面來看個例子:
- (void)testCmd:(NSNumber *)num {
NSLog(@"%ld", (long)num.integerValue);
num = [NSNumber numberWithInteger:num.integerValue-1];
if (num.integerValue > 0) {
[self performSelector:_cmd withObject:num];
}
}
嘗試調(diào)用:
[self testCmd:@(5)];
上面會按順序輸出 5, 4, 3, 2, 1
待榔,然后結(jié)束。即我們可以在方法內(nèi)部用 _cmd
來調(diào)用方法自身流济。
5锐锣、上面已經(jīng)介紹了方法調(diào)用的大致過程,下面來討論類之間繼承的情況绳瘟。重新回去看 objc_class
結(jié)構(gòu)體的定義雕憔,當(dāng)中包含一個指向父類的指針 super_class
。
即當(dāng)向一個對象發(fā)送消息時糖声,會去這個類的 methodLists
中查找相應(yīng)的 SEL
斤彼,如果查不到分瘦,則通過 super_class
指針找到父類,再去父類的 methodLists
中查找琉苇,層層遞進(jìn)嘲玫。最后仍然找不到,才走拋異常流程并扇。
下面的圖演示了一個基本的消息發(fā)送框架:
6趁冈、當(dāng)一個方法找不到的時候,會走攔截調(diào)用和消息轉(zhuǎn)發(fā)流程拜马。我們可以重寫 +resolveClassMethod:
和 +resolveInstanceMethod:
方法渗勘,在程序崩潰前做一些處理。通常的做法是動態(tài)添加一個方法俩莽,并返回 YES
告訴程序已經(jīng)成功處理消息旺坠。如果這兩個方法返回 NO
,這個流程會繼續(xù)往下走扮超,完整的流程如下圖所示:
4. Category
我們來看一下 Category
在頭文件中的定義:
typedef struct objc_category *Category;
struct objc_category {
char * category_name;
char * class_name;
struct objc_method_list * instance_methods;
struct objc_method_list * class_methods;
struct objc_protocol_list * protocols;
}
Category
是一個指向 objc_category
結(jié)構(gòu)體的指針取刃,在 objc_category
中包含對象方法列表、類方法列表出刷、協(xié)議列表璧疗。從這里我們也可以看出, Category 支持添加對象方法馁龟、類方法崩侠、協(xié)議,但不能保存成員變量坷檩。
注意:在
Category
中是可以添加屬性的却音,但不會生成對應(yīng)的成員變量、getter
和setter
矢炼。因此系瓢,調(diào)用Category
中聲明的屬性時會報(bào)錯。
我們可以通過「關(guān)聯(lián)對象」的方式來添加可用的屬性句灌。具體操作如下:
- 1夷陋、在
UIViewController+Tag.h
文件中聲明property
。
@property (nonatomic, strong) NSString *tag;
- 2胰锌、在
UIViewController+Tag.m
中實(shí)現(xiàn)getter
和setter
骗绕。記得添加頭文件#import <objc/runtime.h>
。主要是用到objc_setAssociatedObject
和objc_getAssociatedObject
這兩個方法匕荸。
static void *tag = &tag;
@implementation UIViewController (Tag)
- (void)setTag:(NSString *)t {
objc_setAssociatedObject(self, &tag, t, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)tag {
return objc_getAssociatedObject(self, &tag);
}
@end
- 3爹谭、在子類中調(diào)用枷邪。
// 子類 ViewController.m
- (void)testCategroy {
self.tag = @"TAG";
NSLog(@"%@", self.tag); // 這里輸出:TAG
}
注意:當(dāng)一個對象被釋放后榛搔,
Runtime
回去查找這個對象是否有關(guān)聯(lián)的對象诺凡,有的話,會將它們釋放掉践惑。因此不需要我們手動去釋放腹泌。