Runtime是OC里面非常重要的一個概念,它是OC的底層實現(xiàn)两残,也正是因為Runtime,OC成為一個動態(tài)語言把跨,并且擁有了面向對象的能力人弓。這篇文章,將詳細說明Runtime的各種知識着逐,并且能夠實際運用崔赌。
-
什么是Runtime
Runtime即運行時,也就是程序在運行的時候做的事情耸别。在iOS中健芭,有一套底層的C語言API,它是iOS內部核心之一秀姐,我們編寫的OC代碼慈迈,底層都是基于它實現(xiàn)的,OC語言在編譯后省有,都是Runtime形式的C語言代碼痒留。
學習Runtime可以使我們更加清楚地了解OC語言的底層實現(xiàn),從而可以運用它去實現(xiàn)很多OC語言實現(xiàn)不了的功能(比如給Category添加屬性)锥咸。
-
類和對象
在沒有接觸Runtime之前狭瞎,我們對OC的類和對象只有概念上的理解,并不知道它本質上是什么〔瑁現(xiàn)在我們來看看它們的底層定義熊锭,我們先從我們經(jīng)常使用的id
入手,在<objc/objc.h>
中,我們找到這一段定義:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
這里說明id
是一個指向objc_object
結構體的指針碗殷,而注釋又說精绎,這個指針指向一個類的實例對象,所以我們知道了锌妻,objc_object
結構體就代表了一個類的實例對象:
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
objc_object
這個結構體里面代乃,只有一個isa
,這個isa
的類型是Class
仿粹,并且是不能為空的搁吓。顧名思義,這個肯定就是這個對象的類型了吭历。
然后我們再去找找Class
又是什么東西:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
這里我們可以看到堕仔,Class
是一個指向objc_class
結構體的指針。所以我們知道了晌区,isa
是一個指向objc_class
的指針摩骨,即通過它可以找到一個對象的類。所以朗若,id
類型其實就是一個指向任意類型實例的指針恼五。接著,我們再去看看objc_class
是什么東西:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息哭懈,默認為0
long info OBJC2_UNAVAILABLE; // 類信息灾馒,供運行期使用的一些位標識
long instance_size OBJC2_UNAVAILABLE; // 類的實例變量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 類的成員變量鏈表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協(xié)議鏈表
#endif
} OBJC2_UNAVAILABLE;
我們先從第一行可以得知,OC的類里面银伟,也有一個isa
指針你虹,而這個指針指向了這個類的類型,由此我們可以推斷出來彤避,其實OC的類本質上也是一個對象,因為它也有自己的類夯辖。
在OC里面琉预,每一個類的isa
指針都指向它的元類,最終指向NSObject
蒿褂,NSObject
的元類是它自己圆米。而NSObject
的父類則是nil。這張圖很好的說明了isa
和super_class
的區(qū)別:
接著啄栓,我們可以看到娄帖,一個類的結構體里面,還有它的父類昙楚,類名近速,版本號,類信息,變量大小削葱,變量列表奖亚,方法列表,方法緩存析砸,協(xié)議列表這些東西昔字。
這里也解釋了為什么前面的
objc_object
里面只有一個isa
指針,是因為只要有了這個指針首繁,就能夠找到這個類里面的所有方法和屬性作郭。看了這些,我們就對OC的類和對象有了更深刻的理解弦疮。接下來所坯,我們再去探究一下OC是怎么調用方法的。
-
OC的消息發(fā)送
OC的方法調用底層是給某個對象發(fā)送某個方法挂捅。并且芹助,在編譯的時候并沒有確定具體調用哪個方法,只有在運行時才能確定闲先。我們來驗證一下状土,首先隨便創(chuàng)建一個空的命令行項目:
然后創(chuàng)建一個Person類,然后寫一個空的方法:
.h
@interface Person : NSObject
- (void)run;
@end
.m
- (void)run {
}
然后打開終端伺糠,cd到剛剛創(chuàng)建的main.m
文件所在的文件夾下蒙谓,執(zhí)行clang -rewrite-objc main.m
這時候,就可以在文件夾里面看到一個main.cpp
文件训桶,打開累驮,拉到最下面,就可以看到這一段代碼:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person *p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
}
return 0;
}
可以看到我們剛剛寫的方法編譯過之后全部變成了objc_msgSend
的方式舵揭,甚至alloc
和init
方法也變成了這樣谤专。這就說明OC方法的底層調用都是objc_msgSend
實現(xiàn)的,而objc_msgSend
就是消息發(fā)送午绳。同時置侍,也驗證了OC底層就是用Runtime實現(xiàn)的C語言代碼。
接下來我們來研究一下這個objc_msgSend
拦焚。要使用它蜡坊,得首先導入#import <objc/message.h>
,然后就可以使用了赎败。但是秕衙,我們打出來這個發(fā)現(xiàn)沒有任何參數(shù)提示:
這是因為蘋果公司不建議我們這么用了。我們可以在
Build Setting
里面僵刮,找到改成
NO
据忘,再回去敲入objc_msgSend
鹦牛,就可以看到提示了:具體的參數(shù)含義是:
id _Nullable self
:id
類型我們前面知道,它可以指向任意OC對象若河,這地方就代表著給誰發(fā)消息能岩,也就是調用誰的方法。...
:三個點代表參數(shù)列表/可擴展參數(shù)萧福。SEL _Nonnull op
:SEL
又是什么呢拉鹃?到這里,我們就得提一下OC里面的方法了鲫忍。老規(guī)矩膏燕,我們先去找定義,在<objc/runtime.h>
中:
struct objc_method {
SEL _Nonnull method_name OBJC2_UNAVAILABLE;
char * _Nullable method_types OBJC2_UNAVAILABLE;
IMP _Nonnull method_imp OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
這里我們可以看到悟民,OC的方法里面坝辫,有三個東西,一個是SEL
射亏,一個是char *
近忙,一個是IMP
,這個char *
我們知道是C語言的字符串智润,這個地方是一組描述方法的參數(shù)類型的字符數(shù)組及舍,后面我們會詳細了解這個東西,這里先不管它窟绷。SEL
這個地方通過命名我們可以看出來是方法名锯玛,IMP
我們就完全看不出了,它們具體是怎么定義的呢兼蜈?還是在<objc/objc.h>
中:
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;
/// A pointer to the function of a method implementation.
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
這里攘残,我們可以看到,IMP
其實就是一個指向方法實現(xiàn)的指針为狸,而SEL
則是一個objc_selector
結構體歼郭,源碼中我們找不到SEL
的定義,經(jīng)過查閱資料得知钥平,它完全可以理解為一個char *
实撒,也就是說,其實它就是方法名的字符串涉瘾,也就是一個方法的標簽。
知道這些以后捷兰,我們就可以用消息發(fā)送來改寫以前的OC代碼立叛,比如:
前面的Person
類的run
方法的調用:
objc_msgSend(p, @selector(run));
為了方便測試,我們給run
方法寫一個簡單的實現(xiàn):
- (void)run {
NSLog(@"跑了");
}
然后運行一下:
完美贡茅!
甚至Person
類對象的聲明都可以用發(fā)送消息的方式來完成(解耦合):
Person *p = objc_msgSend([Person class], @selector(alloc));
p = objc_msgSend(p,@selector(init));
再運行一下秘蛇,一樣可以得到之前的結果其做,這里就不貼圖了,跟上面那個圖一樣赁还。
Runtime為我們提供了直接通過類名獲取類的函數(shù):
objc_getClass(char * _Nonnull name);
和得到一個SEL
的函數(shù):
sel_registerName(const char * _Nonnull str);
這樣我們可以繼續(xù)改進之前的代碼:
id p = objc_msgSend(objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")),sel_registerName("init"));
objc_msgSend(p,sel_registerName("run"));;
一樣可以運行得到結果妖泄。
到了這一步,是不是就跟我們之前看到的編譯后的OC代碼一樣了艘策?我們甚至不需要導入Person.h
頭文件蹈胡,就可以直接獲取創(chuàng)建它的實例,并且執(zhí)行方法朋蔫,完成了解耦罚渐。
-
OC的消息轉發(fā)(message forwarding)
看了上面的一些代碼,不知道你有沒有考慮過一個問題驯妄。發(fā)送消息的時候荷并,我們只需要填一些字符串參數(shù)之類的就可以了,完全不知道有沒有這個方法青扔,如果沒有這個方法會發(fā)生什么事情呢源织?
接下來,我們做個試驗:
直接crash掉了微猖。調用方法時谈息,如果在方法在對象的類繼承體系中沒有找到,那怎么辦励两?一般情況下黎茎,程序在運行時就會Crash掉,拋出 unrecognized selector sent to …類似這樣的異常信息当悔。但在拋出異常之前傅瞻,還有三次機會按以下順序讓你拯救程序。這就涉及到以下4個方法:
- 動態(tài)方法解析(dynamic method resolution)
首先會調用+ resolveInstanceMethod:
(對應實例方法)或+ resolveClassMethod:
(對應類方法)方法盲憎,讓你添加方法的實現(xiàn)嗅骄。如果你添加方法并返回YES,那系統(tǒng)在運行時就會重新啟動一次消息發(fā)送的過程饼疙。
我們這里測試一下溺森,增加一個wahaha
方法的實現(xiàn),看看是否可以順利運行窑眯,首先屏积,我們在Person.m
中導入#import <objc/message.h>
,然后磅甩,利用
class_addMethod(Class _Nullable __unsafe_unretained cls,SEL _Nonnull name,IMP _Nonnull imp, const char * _Nullable types)
來增加一個方法及實現(xiàn)炊林,其中,第一個參數(shù)填self
卷要,第二個參數(shù)填wahaha
的SEL渣聚,可以用@selector(wahaha)
,也可以用之前用過的sel_registerName("wahaha")
,第三個參數(shù)需要一個imp独榴,我們知道IMP是指向方法實現(xiàn)的指針,這里我們可以用imp_implementationWithBlock(id _Nonnull block)
來實現(xiàn)奕枝,最后一個參數(shù)我們之前也見過棺榔,就是方法定義里面的的method_types,這個東西該怎么寫呢隘道?我們先去查一下官方文檔:
types
An array of characters that describe the types of the arguments to >the method. For possible values, see Objective-C Runtime >Programming Guide > Type Encodings. >Since the function must take at least two arguments—self and _cmd, the second and third characters must be “@:” (the first character is the return type).
這里面說明這個是一組描述方法的參數(shù)類型的字符數(shù)組症歇,并且,每個方法都有兩個被隱含的參數(shù)薄声,一個是self
(代表當前對象)当船,一個是_cmd
(代表當前對象的SEL),所以第二個和第三個字符必須是@
和:
默辨,而第一個字符是返回值德频,所以一個沒有參數(shù)的方法,它的types
就是"v@:"
缩幸。至于什么類型對應什么字符壹置,可以去上面的鏈接中找。所以我們這里可以直接用"v@:"
表谊。
//如果增加了方法并返回YES钞护,就會重新發(fā)送消息并處理,返回NO爆办,則進入下一步
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == sel_registerName("wahaha")) {
class_addMethod(self, sel_registerName("wahaha"), imp_implementationWithBlock(^(){
NSLog(@"wahaha");
}), "v@:");
}
return YES;
}
運行結果:
怎么樣难咕,是不是很神奇?如果上面返回NO距辆,則會進入完整的消息轉發(fā)機制(full forwarding mechanism)余佃,這里又分為兩個步驟:
- 快速消息轉發(fā) (Fast Forwarding)
這個時候,如果實現(xiàn)了- forwardingTargetForSelector:
方法跨算,系統(tǒng)就會進入該方法繼續(xù)處理消息爆土,這個方法的作用是把之前沒辦法處理的消息轉發(fā)給別的對象去處理:
//返回一個對象繼續(xù)處理消息
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == sel_registerName("wahaha")) {
return [Dog new];
}
return nil;
}
這里我們新建了一個Dog
類,實現(xiàn)了wahaha
方法诸蚕,所以我們直接返回一個Dog
的實例步势,最后運行結果如上,這里就不貼圖了背犯。
- 普通消息轉發(fā)(Normal Forwarding)
如果上一步也沒有對消息進行處理坏瘩,則會進入最后一步,這里涉及到兩個方法漠魏。它首先調用methodSignatureForSelector:
方法來獲取函數(shù)的參數(shù)和返回值桑腮,如果返回為nil
,程序會Crash掉蛉幸,并拋出unrecognized selector sent to instance
異常信息破讨。如果返回一個函數(shù)簽名,系統(tǒng)就會創(chuàng)建一個NSInvocation
對象并調用-forwardInvocation:
方法奕纫。我們同樣在這里對之前的消息進行處理一次:
//返回方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == sel_registerName("wahaha")) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
//轉發(fā)消息
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
Dog *dog = [Dog new];
if ([dog respondsToSelector:sel]) {
[anInvocation invokeWithTarget:dog];
}
}
以上就是OC消息傳遞過程中發(fā)生的事情提陶,利用這些我們可以在很多地方對一個消息做處理。但是我們該怎么選擇呢匹层?
- 動態(tài)方法解析:由于Method Resolution不能像消息轉發(fā)那樣可以交給其他對象來處理隙笆,所以只適用于在原來的類中代替掉。
- 快速消息轉發(fā):其他對象升筏,使用范圍更廣撑柔,不只是限于原來的對象。
- 普通消息轉發(fā):它一樣可以消息轉發(fā)您访,但它能通過NSInvocation對象獲取更多消息發(fā)送的信息铅忿,例如:target、selector灵汪、arguments和返回值等信息檀训。
同時需要注意的是,消息轉發(fā)過程中享言,步驟越往后峻凫,處理消息的代價就越大,最好能在第一步就處理完览露,這樣的話荧琼,運行期系統(tǒng)可以將此方法緩存。如果這個類的實例還會再接收到同名選擇子差牛,那么根本無須再次啟動消息轉發(fā)流程命锄。
通過消息發(fā)送,我們其實已經(jīng)對Runtime做了一個簡單的運用了多糠。接下來累舷,我們再多一些探討。
-
Runtime可以做什么夹孔?
- 在程序運行的時候動態(tài)添加一個類
- 在程序運行的時候動態(tài)的修改一個類的屬性和方法
- 在程序運行的時候遍歷一個類的所有屬性
Runtime有很多方法被盈,可以在文檔中一一查看,不同功能的方法通過前綴區(qū)分搭伤,比如說class_
就是對類的操作只怎,objc_
就是對對象的操作,等等怜俐,都比較好理解身堡。
了解這些以后,我們再回頭看之前提過的一個問題拍鲤,就是給Category增加屬性贴谎。
我們先看一下汞扎,如果直接給Category增加屬性會發(fā)生什么,我們給Person
類創(chuàng)建一個Play
分類擅这,然后添加一個屬性:
@interface Person (Play)
@property (nonatomic, strong) NSString *gameName;
@end
但是我們使用的時候會發(fā)現(xiàn)澈魄,直接就Crash了:
我們現(xiàn)在應該就能明白,這是因為找不到Getter仲翎,同時也沒有Setter痹扇,我們可以給它添加這兩個方法,但是我們在給它添加的時候溯香,發(fā)現(xiàn)在Category里面根本就沒有
_gameName
這個變量鲫构,所以沒辦法像別的類那樣直接添加,這時候玫坛,我們就可以利用Runtime的objc_setAssociatedObject
和objc_getAssociatedObject
來實現(xiàn):
- (NSString *)gameName{
return objc_getAssociatedObject(self, _cmd);
}
- (void)setGameName:(NSString *)gameName {
objc_setAssociatedObject(self, @selector(gameName), gameName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
再次運行结笨,就可以正常使用這個屬性了:
通過以上簡單的例子,我們可以看得出來昂秃,Runtime是OC代碼的底層實現(xiàn)禀梳,所以很多OC代碼不支持的事情,我們都可以通過Runtime自己去實現(xiàn)肠骆,具體在什么場景下使用要根據(jù)實際需求做具體分析算途。接下來屁使,我會用Runtime和之前Block種提到的相關技術自己實現(xiàn)KVO舌菜,并且進行一些改造。另外轴猎,本篇文章的代碼可以在我的github上查看莉钙,點擊前往廓脆。