OC是一門動態(tài)語言颁独,它將很多靜態(tài)語言在編譯和鏈接時期做的事情推遲到了運行時來處理奖磁,這就意味著它不僅需要一個編譯器渐裂,還需要一個運行時系統(tǒng)來執(zhí)行編譯的代碼(動態(tài)得創(chuàng)建類和對象矮锈、進行消息的傳遞和轉(zhuǎn)發(fā))霉翔。這個運行時系統(tǒng)就是Objc Runtime。Objc Runtime其實是一個Runtime庫苞笨,它基本上是由c語言和匯編編寫的债朵。
RunTime庫主要做下面幾件事:
1.封裝:在這個庫中子眶,對象可以用C語言中的結(jié)構(gòu)體表示,而方法可以用C函數(shù)來實現(xiàn)葱弟,另外再加上了一些額外的特性壹店。這些結(jié)構(gòu)體和函數(shù)被runtime函數(shù)封裝后,我們就可以在程序運行時創(chuàng)建芝加,檢查硅卢,修改類、對象和它們的方法了藏杖。
2.找出方法的最終執(zhí)行代碼:當(dāng)程序執(zhí)行[object doSomething]時将塑,會向消息接收者(object)發(fā)送一條消息(doSomething),runtime會根據(jù)消息接收者是否能響應(yīng)該消息而做出不同的反應(yīng)蝌麸。這將在后面詳細介紹点寥。
Objc從三種不同的層級上與Runtime系統(tǒng)進行交互,分別是通過Objective-C源代碼来吩、通過Foundation框架的NSObject類定義的方法敢辩、通過對runtime的函數(shù)直接調(diào)用。
1)Objective-C源代碼
大部分情況下你就只管寫你的Objc代碼就行弟疆,runtime 系統(tǒng)自動在幕后辛勤勞作著戚长。
還記得引言中舉的例子吧,消息的執(zhí)行會使用到一些編譯器為實現(xiàn)動態(tài)語言特性而創(chuàng)建的數(shù)據(jù)結(jié)構(gòu)和函數(shù)怠苔,Objc中的類同廉、方法和協(xié)議等在 runtime 中都由一些數(shù)據(jù)結(jié)構(gòu)來定義,這些內(nèi)容在后面會講到柑司。(比如objc_msgSend函數(shù)及其參數(shù)列表中的id和SEL都是啥)
2)NSObject的方法
Cocoa 中大多數(shù)類都繼承于NSObject類迫肖,也就自然繼承了它的方法。最特殊的例外是NSProxy攒驰,它是個抽象超類蟆湖,它實現(xiàn)了一些消息轉(zhuǎn)發(fā)有關(guān)的方法,可以通過繼承它來實現(xiàn)一個其他類的替身類或是虛擬出一個不存在的類玻粪,說白了就是領(lǐng)導(dǎo)把自己展現(xiàn)給大家風(fēng)光無限隅津,但是把活兒都交給幕后小弟去干。
有的NSObject中的方法起到了抽象接口的作用奶段,比如description方法需要你重載它并為你定義的類提供描述內(nèi)容。NSObject還有些方法能在運行時獲得類的信息剥纷,并檢查一些特性痹籍,比如class返回對象的類;isKindOfClass:和isMemberOfClass:則檢查對象是否在指定的類繼承體系中晦鞋;respondsToSelector:檢查對象能否響應(yīng)指定的消息蹲缠;conformsToProtocol:檢查對象是否實現(xiàn)了指定協(xié)議類的方法棺克;methodForSelector:則返回指定方法實現(xiàn)的地址。
3)Runtime的函數(shù)
Runtime 系統(tǒng)是一個由一系列函數(shù)和數(shù)據(jù)結(jié)構(gòu)組成线定,具有公共接口的動態(tài)共享庫娜谊。頭文件存放于/usr/include/objc目錄下。許多函數(shù)允許你用純C代碼來重復(fù)實現(xiàn) Objc 中同樣的功能斤讥。雖然有一些方法構(gòu)成了NSObject類的基礎(chǔ)纱皆,但是你在寫 Objc 代碼時一般不會直接用到這些函數(shù)的,除非是寫一些 Objc 與其他語言的橋接或是底層的debug工作芭商。在Objective-C Runtime Reference中有對 Runtime 函數(shù)的詳細文檔派草。
[receiver message]會被編譯器轉(zhuǎn)化為:
objc_msgSend(receiver, selector)
如果消息含有參數(shù),則為:
objc_msgSend(receiver, selector, arg1, arg2, ...)
如果消息的接收者能夠找到對應(yīng)的selector铛楣,那么就相當(dāng)于直接執(zhí)行了接收者這個對象的特定方法近迁;否則,消息要么被轉(zhuǎn)發(fā)簸州,或是臨時向接收者動態(tài)添加這個selector對應(yīng)的實現(xiàn)內(nèi)容鉴竭,要么就干脆玩完崩潰掉。
現(xiàn)在可以看出[receiver message]真的不是一個簡簡單單的方法調(diào)用岸浑。因為這只是在編譯階段確定了要向接收者發(fā)送message這條消息搏存,而receive將要如何響應(yīng)這條消息,那就要看運行時發(fā)生的情況來決定了助琐。
Runtime術(shù)語
objc_msgSend:方法的真身是這樣的:
id objc_msgSend (id self, SEL op, ... );
下面將會逐漸展開介紹一些術(shù)語祭埂,其實它們都對應(yīng)著數(shù)據(jù)結(jié)構(gòu)。
SEL
objc_msgSend函數(shù)第二個參數(shù)類型為SEL兵钮,它是selector在Objc中的表示類型(Swift中是Selector類)蛆橡。selector是方法選擇器,可以理解為區(qū)分方法的 ID掘譬,而這個 ID 的數(shù)據(jù)結(jié)構(gòu)是SEL:typedf struct objc_selector *SEL
;
其實它就是個映射到方法的C字符串泰演,你可以用 Objc 編譯器命令@selector()或者 Runtime 系統(tǒng)的sel_registerName函數(shù)來獲得一個SEL類型的方法選擇器。
不同類中相同名字的方法所對應(yīng)的方法選擇器是相同的葱轩,即使方法名字相同而變量類型不同也會導(dǎo)致它們具有相同的方法選擇器睦焕,于是 Objc 中方法命名有時會帶上參數(shù)類型(NSNumber一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦靴拱。
id
objc_msgSend第一個參數(shù)類型為id垃喊,大家對它都不陌生,它是一個指向類實例的指針:
typedef struct objc_object * id
;
那objc_object又是啥呢:
struct objc_object{ Class isa; }
;
objc_object結(jié)構(gòu)體包含一個isa指針袜炕,根據(jù)isa指針就可以順藤摸瓜找到對象所屬的類本谜。
PS:isa指針不總是指向?qū)嵗龑ο笏鶎俚念悾荒芤揽克鼇泶_定類型偎窘,而是應(yīng)該用class方法來確定實例對象的類乌助。因為KVO的實現(xiàn)機理就是將被觀察對象的isa指針指向一個中間類而不是真實的類溜在,這是一種叫做isa-swizzling的技術(shù),詳見官方文檔
Class
之所以說isa是指針是因為Class其實是一個指向objc_class結(jié)構(gòu)體的指針:
typedef struct objc_class * Class
;
而objc_class就是我們摸到的那個瓜他托,里面的東西多著呢:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
constchar*name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
structobjc_ivar_list *ivars OBJC2_UNAVAILABLE;
structobjc_method_list **methodLists OBJC2_UNAVAILABLE;
structobjc_cache *cache OBJC2_UNAVAILABLE;
structobjc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
可以看到運行時一個類還關(guān)聯(lián)了它的超類指針掖肋,類名,成員變量赏参,方法志笼,緩存,還有附屬的協(xié)議登刺。
PS:OBJC2_UNAVAILABLE之類的宏定義是蘋果在 Objc 中對系統(tǒng)運行版本進行約束的黑魔法籽腕,為的是兼容非Objective-C 2.0的遺留邏輯,但我們?nèi)阅軓闹蝎@得一些有價值的信息纸俭,有興趣的可以查看源代碼皇耗。
Objective-C 2.0 的頭文件雖然沒暴露出objc_class結(jié)構(gòu)體更詳細的設(shè)計,我們依然可以從Objective-C 1.0 的定義中小窺端倪:
在objc_class結(jié)構(gòu)體中:ivars是objc_ivar_list指針揍很;methodLists是指向objc_method_list指針的指針郎楼。也就是說可以動態(tài)修改*methodLists的值來添加成員方法,這也是Category實現(xiàn)的原理窒悔,同樣解釋了Category不能添加屬性的原因呜袁。而最新版的 Runtime 源碼對這一塊的描述已經(jīng)有很大變化,可以參考下美團技術(shù)團隊的深入理解Objective-C:Category简珠。
PS:任性的話可以在Category中添加@dynamic的屬性阶界,并利用運行期動態(tài)提供存取方法或干脆動態(tài)轉(zhuǎn)發(fā);或者干脆使用關(guān)聯(lián)度對象(AssociatedObject)
其中objc_ivar_list和objc_method_list分別是成員變量列表和方法列表:
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
}
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
如果你C語言不是特別好聋庵,可以直接理解為objc_ivar_list結(jié)構(gòu)體存儲著objc_ivar數(shù)組列表膘融,而objc_ivar結(jié)構(gòu)體存儲了類的單個成員變量的信息;同理objc_method_list結(jié)構(gòu)體存儲著objc_method數(shù)組列表祭玉,而objc_method結(jié)構(gòu)體存儲了類的某個方法的信息氧映。
最后要提到的還有一個objc_cache,顧名思義它是緩存脱货,它在objc_class的作用很重要岛都,在后面會講到。
不知道你是否注意到了objc_class中也有一個isa對象振峻,這是因為一個 ObjC 類本身同時也是一個對象臼疫,為了處理類和對象的關(guān)系,runtime 庫創(chuàng)建了一種叫做元類 (Meta Class) 的東西扣孟,類對象所屬類型就叫做元類烫堤,它用來表述類對象本身所具備的元數(shù)據(jù)。類方法就定義于此處,因為這些方法可以理解成類對象的實例方法塔逃。每個類僅有一個類對象,而每個類對象僅有一個與之相關(guān)的元類料仗。當(dāng)你發(fā)出一個類似[NSObject alloc]的消息時湾盗,你事實上是把這個消息發(fā)給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例立轧,而這個元類同時也是一個根元類 (root meta class) 的實例格粪。所有的元類最終都指向根元類為其超類。所有的元類的方法列表都有能夠響應(yīng)消息的類方法氛改。所以當(dāng)[NSObject alloc]這條消息發(fā)給類對象的時候帐萎,objc_msgSend()會去它的元類里面去查找能夠響應(yīng)消息的方法,如果找到了胜卤,然后對這個類對象執(zhí)行方法調(diào)用疆导。
Method
Method
是一種代表類中某個方法的類型。
typedef struct objc_method *Method;
objc_method存儲了方法名葛躏,方法類型和方法實現(xiàn):
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
- 方法名類型是SEL
- 方法類型method_types 是個char指針澈段,其實存儲著方法的參數(shù)類型和返回值類型
- method_imp是一個IMP類型的函數(shù)指針:
typedef id (*IMP)(id, SEL, ...);
它指向了方法的實現(xiàn)。
Ivar
Ivar
是代表了類中的實例變量的類型:
typedef struct objc_ivar *Ivar;
objc_ivar的定義如下:
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
}
IMP
IMP的定義是:
typedef id (*IMP)(id, SEL, ...);
它就是一個指向函數(shù)的指針舰攒,這是由編譯器生成的败富,當(dāng)你發(fā)起一個ObjC消息之后,最終他會執(zhí)行的那段代碼摩窃,就是有這個函數(shù)指針指定的兽叮。
Cache
Cache的定義如下:
typedef struct objc_cache *Cache
objc_cache
的定義如下:
struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};
[obj message]
會被編譯成objc_msgSend(obj, message)
,這個函數(shù)要做的事情是:
- 首先,通過obj的isa指針找到他的class猾愿;
- 在class里的method list找message鹦聪;
3.如果class中沒有找到message,繼續(xù)在他的superclass中找匪蟀;
4.一旦找到message這個函數(shù)椎麦,就去執(zhí)行他的實現(xiàn)IMP
但這種實現(xiàn)有個問題,效率低材彪,一個class往往只有20%的函數(shù)會被經(jīng)常調(diào)用观挎。每個消息都要便利一次objc_method_list
并不合理。如果把經(jīng)常調(diào)用的函數(shù)緩存下來段化,那么可以大大提高函數(shù)查詢的效率嘁捷。這也就是objc_class
中一個重要成員objc_cache
做的事情。在找到message之后显熏,把message的method_name
作為key雄嚣,method_imp
作為value給結(jié)合起來。當(dāng)再次受到message消息時,可以直接在cache找缓升,避免遍歷objc_method_list鼓鲁。
Property
typedef struct objc_property *objc_property_t;
/**獲取Person類中的屬性**/
unsigned int outCount;
//返回了一個包含objc_property_t的數(shù)組,最后必須要釋放這個數(shù)組使用free()
objc_property_t *properties = class_copyPropertyList([Person class], &outCount);
for (unsigned int i = 0; i<outCount; i++) {
objc_property_t property = properties[i];
//獲取屬性名稱
const char * propertyName = property_getName(property);
//返回了C字符串包含了屬性名稱和@encode類型字符串
const char * propertyAttr = property_getAttributes(property);
fprintf(stdout, "%s %s\n",propertyName, propertyAttr);
}
free(properties);
使用class_copyPropertyList
只能獲取類的屬性,而不包含成員變量港谊,但此時獲取的屬性名是不包含下劃線的骇吭。
消息轉(zhuǎn)發(fā)
[obj message]
會被編譯成objc_msgSend(obj, message)
,如果obj的isa指針指向的class及其superclass沒有找到message,通常會拋出unrecognized selector sent to … 的異常歧寺,但在拋出異常前燥狰,runtime會給你三次拯救的機會:
1.method resolution
2.fast forwarding
3.normal forwarding
Method Resolution
NSObject在運行時會調(diào)用+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
,讓你有機會提供一個函數(shù)式先來添加方法。
void message(id obj, SEL _cmd)
{
NSLog(@"Doing thing");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if(aSEL == @selector(message)){
class_addMethod([self class], see, (IMP)message, "v@:");
return YES;
}
return [super resolveInstanceMethod];
}
Core Data 有用到這個方法斜筐。NSManagedObjects 中 properties 的 getter 和 setter 就是在運行時動態(tài)添加的龙致。
如果 resolve 方法返回 NO ,運行時就會移到下一步:消息轉(zhuǎn)發(fā)(Message Forwarding)顷链。
iOS4.3加入很多新的runtime方法目代,主要是以imp為前綴的方法,比如imp_implementationWithBlock()用block快速創(chuàng)建一個imp嗤练。
Fast forwarding
如果目標對象實現(xiàn)了- (id)forwardingTargetForSelector:(SEL)aSelector
像啼,runtime就會調(diào)用這個方法,把消息轉(zhuǎn)發(fā)給其他對象潭苞。
- (id)forwardingTargetForSelector:(SEL)aSelector{
if(aSelector == @selector(message)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
只要這個方法返回的不是nil或self忽冻,整個消息發(fā)送的過程就會被重啟,發(fā)送的對象會變成你返回的那個對象此疹,否則僧诚,就會繼續(xù)Normal Forwarding。
這里的fast是為了區(qū)別Normal Forwarding轉(zhuǎn)發(fā)機制蝗碎,因為這一步不會創(chuàng)建任何新的對象湖笨,但下一步轉(zhuǎn)發(fā)會創(chuàng)建一個NSInvocation對象,所以相對更快蹦骑。
Normal forwarding
這是runtime最后一次給你挽救的機會慈省,首先runtime會調(diào)用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法獲得函數(shù)的參數(shù)和返回值類型,如果返回值是nil眠菇,runtime會調(diào)用- (void)doesNotRecognizeSelector:(SEL)aSelector
的方法边败,程序就會掛掉;如果返回了一個函數(shù)簽名,runtime就會創(chuàng)建一個NSInvocation對象并發(fā)送- (void)forwardInvocation:(NSInvocation *)anInvocation
消息捎废。
NSInvocation實際上就是對一個消息的描述笑窜,包擴了selector以及參數(shù)等信息,所以可以在- (void)forwardInvocation:(NSInvocation *)anInvocation
里修改傳進來的NSInvocation對象登疗,然后發(fā)送- (void)invokeWithTarget:(id)target
消息給他排截,穿進去一個新的目標嫌蚤。
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = invocation.selector;
if([alternateObject respondsToSelector:sel]) {
[invocation invokeWithTarget:alternateObject];
} else {
[self doesNotRecognizeSelector:sel];
}
}