runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經(jīng)常會被問到的, 所以大家有必要進行研究,有能力的童鞋可以和下面作者一樣, 親歷實踐一下。
在簡書里發(fā)現(xiàn)了兩篇非常好的文章介紹 runtime和runloop的,在這里合二為一了, 把原版作者的東西拿了過來, 為了尊重作者,在這里注明一下 @sam_lau 是runtime的作者, @tripleCC是runloop的作者?
RunTime
Objective-C是基于C語言加入了面向?qū)ο筇匦?/b>和消息轉(zhuǎn)發(fā)機制的動態(tài)語言,這意味著它不僅需要一個編譯器,還需要Runtime系統(tǒng)來動態(tài)創(chuàng)建類和對象,進行消息發(fā)送和轉(zhuǎn)發(fā)。下面通過分析Apple開源的Runtime代碼(我使用的版本是objc4-646.tar)來深入理解Objective-C的Runtime機制。
Runtime數(shù)據(jù)結(jié)構(gòu)
在Objective-C中,使用[receiver message]語法并不會馬上執(zhí)行receiver對象的message方法的代碼,而是向receiver發(fā)送一條message消息,這條消息可能由receiver來處理,也可能由轉(zhuǎn)發(fā)給其他對象來處理,也有可能假裝沒有接收到這條消息而沒有處理。其實[receiver message]被編譯器轉(zhuǎn)化為:
idobjc_msgSend (idself, SEL op, ... );
下面從兩個數(shù)據(jù)結(jié)構(gòu)id和SEL來逐步分析和理解Runtime有哪些重要的數(shù)據(jù)結(jié)構(gòu)。
SEL
SEL是函數(shù)objc_msgSend第二個參數(shù)的數(shù)據(jù)類型,表示方法選擇器,按下面路徑打開objc.h文件

SEL Data Structure
查看到SEL數(shù)據(jù)結(jié)構(gòu)如下:
typedefstructobjc_selector *SEL;
其實它就是映射到方法的C字符串,你可以通過Objc編譯器命令@selector()或者Runtime系統(tǒng)的sel_registerName函數(shù)來獲取一個SEL類型的方法選擇器。
如果你知道selector對應(yīng)的方法名是什么,可以通過NSString* NSStringFromSelector(SEL aSelector)方法將SEL轉(zhuǎn)化為字符串,再用NSLog打印。
id
接下來看objc_msgSend第一個參數(shù)的數(shù)據(jù)類型id,id是通用類型指針,能夠表示任何對象。按下面路徑打開objc.h文件

id Data Structure.png
查看到id數(shù)據(jù)結(jié)構(gòu)如下:
/// Represents an instance of a class.structobjc_object {? ? Class isa? OBJC_ISA_AVAILABILITY;};/// A pointer to an instance of a class.typedefstructobjc_object *id;
id其實就是一個指向objc_object結(jié)構(gòu)體指針,它包含一個Class isa成員,根據(jù)isa指針就可以順藤摸瓜找到對象所屬的類。
注意:根據(jù)Apple的官方文檔Key-Value Observing Implementation Details提及,key-value observing是使用isa-swizzling的技術(shù)實現(xiàn)的,isa指針在運行時被修改,指向一個中間類而不是真正的類。所以,你不應(yīng)該使用isa指針來確定類的關(guān)系,而是使用class方法來確定實例對象的類。
Class
isa指針的數(shù)據(jù)類型是Class,Class表示對象所屬的類,按下面路徑打開objc.h文件

Class Data Structure
/// An opaque type that represents an Objective-C class.typedefstructobjc_class *Class;
可以查看到Class其實就是一個objc_class結(jié)構(gòu)體指針,但這個頭文件找不到它的定義,需要在runtime.h才能找到objc_class結(jié)構(gòu)體的定義。
按下面路徑打開runtime.h文件

objc_class Data Structure
查看到objc_class結(jié)構(gòu)體定義如下:
structobjc_class {? ? Class isa? OBJC_ISA_AVAILABILITY;#if !__OBJC2__Class super_class? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;constchar*name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;longversion? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;longinfo? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;longinstance_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;/* Use `Class` instead of `struct objc_class *` */
注意:OBJC2_UNAVAILABLE是一個Apple對Objc系統(tǒng)運行版本進行約束的宏定義,主要為了兼容非Objective-C 2.0的遺留版本,但我們?nèi)阅軓闹蝎@取一些有用信息。
讓我們分析一些重要的成員變量表示什么意思和對應(yīng)使用哪些數(shù)據(jù)結(jié)構(gòu)。
isa表示一個Class對象的Class,也就是Meta Class。在面向?qū)ο笤O(shè)計中,一切都是對象,Class在設(shè)計中本身也是一個對象。我們會在objc-runtime-new.h文件找到證據(jù),發(fā)現(xiàn)objc_class有以下定義:
structobjc_class : objc_object {// Class ISA;Class superclass;cache_tcache;// formerly cache pointer and vtableclass_data_bits_tbits;// class_rw_t * plus custom rr/alloc flags......}
由此可見,結(jié)構(gòu)體objc_class也是繼承objc_object,說明Class在設(shè)計中本身也是一個對象。
其實Meta Class也是一個Class,那么它也跟其他Class一樣有自己的isa和super_class指針,關(guān)系如下:

Class isa and superclass relationship from Google
上圖實線是super_class指針,虛線是isa指針。有幾個關(guān)鍵點需要解釋以下:
Root class (class)其實就是NSObject,NSObject是沒有超類的,所以Root class(class)的superclass指向nil。
每個Class都有一個isa指針指向唯一的Meta class
Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一個回路。
每個Meta class的isa指針都指向Root class (meta)。
super_class表示實例對象對應(yīng)的父類
name表示類名
ivars表示多個成員變量,它指向objc_ivar_list結(jié)構(gòu)體。在runtime.h可以看到它的定義:
structobjc_ivar_list {intivar_count? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;#ifdef __LP64__intspace? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;#endif/* variable length structure */structobjc_ivar ivar_list[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;}
objc_ivar_list其實就是一個鏈表,存儲多個objc_ivar,而objc_ivar結(jié)構(gòu)體存儲類的單個成員變量信息。
methodLists表示方法列表,它指向objc_method_list結(jié)構(gòu)體的二級指針,可以動態(tài)修改*methodLists的值來添加成員方法,也是Category實現(xiàn)原理,同樣也解釋Category不能添加實例變量的原因。在runtime.h可以看到它的定義:
structobjc_method_list {structobjc_method_list *obsolete? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;intmethod_count? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;#ifdef __LP64__intspace? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;#endif/* variable length structure */structobjc_method method_list[1]? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;}
同理,objc_method_list也是一個鏈表,存儲多個objc_method,而objc_method結(jié)構(gòu)體存儲類的某個方法的信息。
cache用來緩存經(jīng)常訪問的方法,它指向objc_cache結(jié)構(gòu)體,后面會重點講到。
protocols表示類遵循哪些協(xié)議
Method
Method表示類中的某個方法,在runtime.h文件中找到它的定義:
/// An opaque type that represents a method in a class definition.typedefstructobjc_method *Method;structobjc_method {? ? SEL method_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;char*method_types? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;? ? IMP method_imp? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;}
其實Method就是一個指向objc_method結(jié)構(gòu)體指針,它存儲了方法名(method_name)、方法類型(method_types)和方法實現(xiàn)(method_imp)等信息。而method_imp的數(shù)據(jù)類型是IMP,它是一個函數(shù)指針,后面會重點提及。
Ivar
Ivar表示類中的實例變量,在runtime.h文件中找到它的定義:
/// An opaque type that represents an instance variable.typedefstructobjc_ivar *Ivar;structobjc_ivar {char*ivar_name? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;char*ivar_type? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;intivar_offset? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;#ifdef __LP64__intspace? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;#endif}
Ivar其實就是一個指向objc_ivar結(jié)構(gòu)體指針,它包含了變量名(ivar_name)、變量類型(ivar_type)等信息。
IMP
在上面講Method時就說過,IMP本質(zhì)上就是一個函數(shù)指針,指向方法的實現(xiàn),在objc.h找到它的定義:
/// A pointer to the function of a method implementation.#if!OBJC_OLD_DISPATCH_PROTOTYPEStypedefvoid(*IMP)(void/* id, SEL, ... */);#elsetypedefid(*IMP)(id, SEL, ...);#endif
當你向某個對象發(fā)送一條信息,可以由這個函數(shù)指針來指定方法的實現(xiàn),它最終就會執(zhí)行那段代碼,這樣可以繞開消息傳遞階段而去執(zhí)行另一個方法實現(xiàn)。
Cache
顧名思義,Cache主要用來緩存,那它緩存什么呢?我們先在runtime.h文件看看它的定義:
typedefstructobjc_cache *Cache? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;structobjc_cache {unsignedintmask/* total = mask + 1 */OBJC2_UNAVAILABLE;unsignedintoccupied? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;? ? Method buckets[1]? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;};
Cache其實就是一個存儲Method的鏈表,主要是為了優(yōu)化方法調(diào)用的性能。當對象receiver調(diào)用方法message時,首先根據(jù)對象receiver的isa指針查找到它對應(yīng)的類,然后在類的methodLists中搜索方法,如果沒有找到,就使用super_class指針到父類中的methodLists查找,一旦找到就調(diào)用方法。如果沒有找到,有可能消息轉(zhuǎn)發(fā),也可能忽略它。但這樣查找方式效率太低,因為往往一個類大概只有20%的方法經(jīng)常被調(diào)用,占總調(diào)用次數(shù)的80%。所以使用Cache來緩存經(jīng)常調(diào)用的方法,當調(diào)用方法時,優(yōu)先在Cache查找,如果沒有找到,再到methodLists查找。
消息發(fā)送
前面從objc_msgSend作為入口,逐步深入分析Runtime的數(shù)據(jù)結(jié)構(gòu),了解每個數(shù)據(jù)結(jié)構(gòu)的作用和它們之間關(guān)系后,我們正式轉(zhuǎn)入消息發(fā)送這個正題。
objc_msgSend函數(shù)
在前面已經(jīng)提過,當某個對象使用語法[receiver message]來調(diào)用某個方法時,其實[receiver message]被編譯器轉(zhuǎn)化為:
idobjc_msgSend (idself, SEL op, ... );
現(xiàn)在讓我們看一下objc_msgSend它具體是如何發(fā)送消息:
首先根據(jù)receiver對象的isa指針獲取它對應(yīng)的class
優(yōu)先在class的cache查找message方法,如果找不到,再到methodLists查找
如果沒有在class找到,再到super_class查找
一旦找到message這個方法,就執(zhí)行它實現(xiàn)的IMP。

Objc Message.gif
self與super
為了讓大家更好地理解self和super,借用sunnyxx博客的ios程序員6級考試一道題目:下面的代碼分別輸出什么?
@implementationSon:Father- (id)init{self= [superinit];if(self)? ? {NSLog(@"%@",NSStringFromClass([selfclass]));NSLog(@"%@",NSStringFromClass([superclass]));? ? }returnself;}@end
self表示當前這個類的對象,而super是一個編譯器標示符,和self指向同一個消息接受者。在本例中,無論是[self class]還是[super class],接受消息者都是Son對象,但super與self不同的是,self調(diào)用class方法時,是在子類Son中查找方法,而super調(diào)用class方法時,是在父類Father中查找方法。
當調(diào)用[self class]方法時,會轉(zhuǎn)化為objc_msgSend函數(shù),這個函數(shù)定義如下:
idobjc_msgSend(idself, SEL op, ...)
這時會從當前Son類的方法列表中查找,如果沒有,就到Father類查找,還是沒有,最后在NSObject類查找到。我們可以從NSObject.mm文件中看到- (Class)class的實現(xiàn):
- (Class)class{returnobject_getClass(self);}
所以NSLog(@"%@", NSStringFromClass([self class]));會輸出Son。
當調(diào)用[super class]方法時,會轉(zhuǎn)化為objc_msgSendSuper,這個函數(shù)定義如下:
idobjc_msgSendSuper(structobjc_super *super, SEL op, ...)
objc_msgSendSuper函數(shù)第一個參數(shù)super的數(shù)據(jù)類型是一個指向objc_super的結(jié)構(gòu)體,從message.h文件中查看它的定義:
///Specifies the superclass of an instance.structobjc_super {///Specifies an instance of a class.__unsafe_unretained id receiver;///Specifies the particular superclass of the instance to message.#if!defined(__cplusplus)? &&? !__OBJC2__/* For compatibility with old objc-runtime.h header */__unsafe_unretained Classclass;#else__unsafe_unretained Class super_class;#endif/* super_class is the first class to search */};#endif
結(jié)構(gòu)體包含兩個成員,第一個是receiver,表示某個類的實例。第二個是super_class表示當前類的父類。
這時首先會構(gòu)造出objc_super結(jié)構(gòu)體,這個結(jié)構(gòu)體第一個成員是self,第二個成員是(id)class_getSuperclass(objc_getClass("Son")),實際上該函數(shù)會輸出Father。然后在Father類查找class方法,查找不到,最后在NSObject查到。此時,內(nèi)部使用objc_msgSend(objc_super->receiver, @selector(class))去調(diào)用,與[self class]調(diào)用相同,所以結(jié)果還是Son。
隱藏參數(shù)self和_cmd
當[receiver message]調(diào)用方法時,系統(tǒng)會在運行時偷偷地動態(tài)傳入兩個隱藏參數(shù)self和_cmd,之所以稱它們?yōu)殡[藏參數(shù),是因為在源代碼中沒有聲明和定義這兩個參數(shù)。至于對于self的描述,上面已經(jīng)解釋非常清楚了,下面我們重點講解_cmd。
_cmd表示當前調(diào)用方法,其實它就是一個方法選擇器SEL。一般用于判斷方法名或在Associated Objects中唯一標識鍵名,后面在Associated Objects會講到。
方法解析與消息轉(zhuǎn)發(fā)
[receiver message]調(diào)用方法時,如果在message方法在receiver對象的類繼承體系中沒有找到方法,那怎么辦?一般情況下,程序在運行時就會Crash掉,拋出unrecognized selector sent to …類似這樣的異常信息。但在拋出異常之前,還有三次機會按以下順序讓你拯救程序。
Method Resolution
Fast Forwarding
Normal Forwarding

Message Forward from Google
Method Resolution
首先Objective-C在運行時調(diào)用+ resolveInstanceMethod:或+ resolveClassMethod:方法,讓你添加方法的實現(xiàn)。如果你添加方法并返回YES,那系統(tǒng)在運行時就會重新啟動一次消息發(fā)送的過程。
舉一個簡單例子,定義一個類Message,它主要定義一個方法sendMessage,下面就是它的設(shè)計與實現(xiàn):
@interfaceMessage:NSObject- (void)sendMessage:(NSString*)word;@end
@implementationMessage- (void)sendMessage:(NSString*)word{NSLog(@"normal way : send message = %@", word);}@end
如果我在viewDidLoad方法中創(chuàng)建Message對象并調(diào)用sendMessage方法:
- (void)viewDidLoad {? ? [superviewDidLoad];? ? Message *message = [Messagenew];? ? [message sendMessage:@"Sam Lau"];}
控制臺會打印以下信息:
normal way :sendmessage = Sam Lau
但現(xiàn)在我將原來sendMessage方法實現(xiàn)給注釋掉,覆蓋resolveInstanceMethod方法:
#pragma mark - Method Resolution/// override resolveInstanceMethod or resolveClassMethod for changing sendMessage method implementation+ (BOOL)resolveInstanceMethod:(SEL)sel{if(sel ==@selector(sendMessage:)) {? ? ? ? class_addMethod([selfclass], sel, imp_implementationWithBlock(^(idself,NSString*word) {NSLog(@"method resolution way : send message = %@", word);? ? ? ? }),"v@*");? ? }returnYES;}
控制臺就會打印以下信息:
method resolution way :sendmessage = Sam Lau
注意到上面代碼有這樣一個字符串"v@*,它表示方法的參數(shù)和返回值,詳情請參考Type Encodings
如果resolveInstanceMethod方法返回NO,運行時就跳轉(zhuǎn)到下一步:消息轉(zhuǎn)發(fā)(Message Forwarding)
Fast Forwarding
如果目標對象實現(xiàn)- forwardingTargetForSelector:方法,系統(tǒng)就會在運行時調(diào)用這個方法,只要這個方法返回的不是nil或self,也會重啟消息發(fā)送的過程,把這消息轉(zhuǎn)發(fā)給其他對象來處理。否則,就會繼續(xù)Normal Fowarding。
繼續(xù)上面Message類的例子,將sendMessage和resolveInstanceMethod方法注釋掉,然后添加forwardingTargetForSelector方法的實現(xiàn):
#pragma mark - Fast Forwarding- (id)forwardingTargetForSelector:(SEL)aSelector{if(aSelector ==@selector(sendMessage:)) {return[MessageForwarding new];? ? }returnnil;}
此時還缺一個轉(zhuǎn)發(fā)消息的類MessageForwarding,這個類的設(shè)計與實現(xiàn)如下:
@interfaceMessageForwarding:NSObject- (void)sendMessage:(NSString*)word;@end
@implementationMessageForwarding- (void)sendMessage:(NSString*)word{NSLog(@"fast forwarding way : send message = %@", word);}@end
此時,控制臺會打印以下信息:
fast forwarding way :sendmessage = Sam Lau
這里叫Fast,是因為這一步不會創(chuàng)建NSInvocation對象,但Normal Forwarding會創(chuàng)建它,所以相對于更快點。
Normal Forwarding
如果沒有使用Fast Forwarding來消息轉(zhuǎn)發(fā),最后只有使用Normal Forwarding來進行消息轉(zhuǎn)發(fā)。它首先調(diào)用methodSignatureForSelector:方法來獲取函數(shù)的參數(shù)和返回值,如果返回為nil,程序會Crash掉,并拋出unrecognized selector sent to instance異常信息。如果返回一個函數(shù)簽名,系統(tǒng)就會創(chuàng)建一個NSInvocation對象并調(diào)用-forwardInvocation:方法。
繼續(xù)前面的例子,將forwardingTargetForSelector方法注釋掉,添加methodSignatureForSelector和forwardInvocation方法的實現(xiàn):
#pragma mark - Normal Forwarding-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];if(!methodSignature){? ? ? ? methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];}? ? return methodSignature;}-(void)forwardInvocation:(NSInvocation*)anInvocation
{
MessageForwarding *messageForwarding = [MessageForwarding new];if([messageForwarding respondsToSelector:anInvocation.selector]){? ? ? ? [anInvocation invokeWithTarget:messageForwarding];}}
關(guān)于這個例子的示例代碼請到github下載。
三種方法的選擇
Runtime提供三種方式來將原來的方法實現(xiàn)代替掉,那該怎樣選擇它們呢?
Method Resolution:由于Method Resolution不能像消息轉(zhuǎn)發(fā)那樣可以交給其他對象來處理,所以只適用于在原來的類中代替掉。
Fast Forwarding:它可以將消息處理轉(zhuǎn)發(fā)給其他對象,使用范圍更廣,不只是限于原來的對象。
Normal Forwarding:它跟Fast Forwarding一樣可以消息轉(zhuǎn)發(fā),但它能通過NSInvocation對象獲取更多消息發(fā)送的信息,例如:target、selector、arguments和返回值等信息。
Associated Objects
Categories can be used to declare either instance methods or class methods but are not usually suitable for declaring additional properties. It’s valid syntax to include a property declaration in a category interface, but it’s not possible to declare an additional instance variable in a category. This means the compiler won’t synthesize any instance variable, nor will it synthesize any property accessor methods. You can write your own accessor methods in the category implementation, but you won’t be able to keep track of a value for that property unless it’s already stored by the original class. (Programming with Objective-C)
當想使用Category對已存在的類進行擴展時,一般只能添加實例方法或類方法,而不適合添加額外的屬性。雖然可以在Category頭文件中聲明property屬性,但在實現(xiàn)文件中編譯器是無法synthesize任何實例變量和屬性訪問方法。這時需要自定義屬性訪問方法并且使用Associated Objects來給已存在的類Category添加自定義的屬性。Associated Objects提供三個API來向?qū)ο?b>添加、獲取和刪除關(guān)聯(lián)值:
void objc_setAssociatedObject (id object, const void *key, id value, objc_AssociationPolicy policy )
id objc_getAssociatedObject (id object, const void *key )
void objc_removeAssociatedObjects (id object )
其中objc_AssociationPolicy是個枚舉類型,它可以指定Objc內(nèi)存管理的引用計數(shù)機制。
typedefOBJC_ENUM(uintptr_t, objc_AssociationPolicy){? ? OBJC_ASSOCIATION_ASSIGN =0,/**< Specifies a weak reference to the associated object. */OBJC_ASSOCIATION_RETAIN_NONATOMIC =1,/**< Specifies a strong reference to the associated object.
*? The association is not made atomically. */OBJC_ASSOCIATION_COPY_NONATOMIC =3,/**< Specifies that the associated object is copied.
*? The association is not made atomically. */OBJC_ASSOCIATION_RETAIN =01401,/**< Specifies a strong reference to the associated object.
*? The association is made atomically. */OBJC_ASSOCIATION_COPY =01403/**< Specifies that the associated object is copied.
*? The association is made atomically. */};
下面有個關(guān)于NSObject+AssociatedObjectCategory添加屬性associatedObject的示例代碼:
NSObject+AssociatedObject.h
@interfaceNSObject(AssociatedObject)@property(strong,nonatomic)idassociatedObject;@end
NSObject+AssociatedObject.m
@implementationNSObject(AssociatedObject)- (void)setAssociatedObject:(id)associatedObject{? ? objc_setAssociatedObject(self,@selector(associatedObject), associatedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC);}- (id)associatedObject{returnobjc_getAssociatedObject(self, _cmd);}@end
Associated Objects的key要求是唯一并且是常量,而SEL是滿足這個要求的,所以上面的采用隱藏參數(shù)_cmd作為key。
Method Swizzling
Method Swizzling就是在運行時將一個方法的實現(xiàn)代替為另一個方法的實現(xiàn)。如果能夠利用好這個技巧,可以寫出簡潔、有效且維護性更好的代碼??梢詤⒖純善P(guān)于Method Swizzling技巧的文章:
Aspect-Oriented Programming(AOP)
類似記錄日志、身份驗證、緩存等事務(wù)非常瑣碎,與業(yè)務(wù)邏輯無關(guān),很多地方都有,又很難抽象出一個模塊,這種程序設(shè)計問題,業(yè)界給它們起了一個名字叫橫向關(guān)注點(Cross-cutting concern),AOP作用就是分離橫向關(guān)注點(Cross-cutting concern)來提高模塊復用性,它可以在既有的代碼添加一些額外的行為(記錄日志、身份驗證、緩存)而無需修改代碼。
危險性
Method Swizzling就像一把瑞士小刀,如果使用得當,它會有效地解決問題。但使用不當,將帶來很多麻煩。在stackoverflow上有人已經(jīng)提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?,它的危險性主要體現(xiàn)以下幾個方面:
Method swizzling is not atomic
Changes behavior of un-owned code
Possible naming conflicts
Swizzling changes the method's arguments
The order of swizzles matters
Difficult to understand (looks recursive)
Difficult to debug
總結(jié)
雖然在平時項目不是經(jīng)常用到Objective-C的Runtime特性,但當你閱讀一些iOS開源項目時,你就會發(fā)現(xiàn)很多時候都會用到。所以深入理解Objective-C的Runtime數(shù)據(jù)結(jié)構(gòu)、消息轉(zhuǎn)發(fā)機制有助于你更容易地閱讀和學習開源項目。
擴展閱讀
What are the Dangers of Method Swizzling in Objective C?
RunLoop
深入理解RunLoop這篇文章寫的很好!
簡介
RunLoop顧名思義,就是運行循環(huán)的意思。
基本作用:
保持程序的持續(xù)運行
處理App中的各類事件(觸摸事件、定時器事件、Selector事件)
節(jié)省CPU資源,提高程序性能:沒有事件時就進行睡眠狀態(tài)
內(nèi)部實現(xiàn):
do-while循環(huán),在這個循環(huán)內(nèi)部不斷地處理各種任務(wù)(Source\Timeer\Observer)
注意點:
一個線程對應(yīng)一個RunLoop(采用字典存儲,線程號為key,RunLoop為value)
主線程的RunLoop默認已經(jīng)啟動,子線程的RunLoop需要手動啟動
RunLoop只能選擇一個Mode啟動,如果當前Mode沒有任何Source、Timer、Observer,那么就不會進入RunLoop
RunLoop的主要函數(shù)調(diào)用順序為:CFRunLoopRun->CFRunLoopRunSpecific->__CFRunLoopRun

注意特殊情況,事實上,在只有Observer的情況,也不一定會進入循環(huán),因為源代碼里面只會顯式地檢測兩個東西:Source和Timer(這兩個是主動向RunLoop發(fā)送消息的);Observer是被動接收消息的

RunLoop在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀
RunLoop循環(huán)示意圖:(針對上面的__CFRunLoopRun函數(shù),Mode已經(jīng)判斷非空前提)
圖1

RunLoop循環(huán)示意圖
圖2

接觸過微處理器編程的基本上都知道,在編寫微處理器程序時,我通常會在main函數(shù)中寫一個無限循環(huán),然后在這個循環(huán)里面對外部事件進行監(jiān)聽,比如外部中斷,一些傳感器的數(shù)據(jù)等,在沒有外部中斷時,就讓CPU進入低功耗模式。如果接收到了外部中斷,就恢復到正常模式,對中斷進行處理。
while(1) {// 根據(jù)中斷決定是否切換模式執(zhí)行任務(wù)}// 或者for(;;) {}
RunLoop和這個相似,也是在線程的main中增加了一個循環(huán):
intmain(intargc,char* argv[]) {BOOLrunning =YES;do{// 執(zhí)行各種任務(wù),處理各種事件// ......}while(running);return0;}
所以線程在這種情況下,便不會退出。
關(guān)于MainRunLoop:
intmain(intargc,char* argv[]) {@autoreleasepool{returnUIApplicationMain(argc, argv,nil,NSStringFromClass([AppDelegate class]));? ? }}
在viewDidLoad中設(shè)置斷電,然后得到以下主線程棧信息:

可以看到,UIApplicationMain內(nèi)部啟動了一個和主線程相關(guān)聯(lián)的RunLoop(_CFRunLoopRun)。在這里也可以推斷,程序進入UIApplicationMain就不會退出了。我稍微對主函數(shù)進行了如下修改,并在return語句上打印了斷點:

運行程序后,并不會在斷點處停下,證實了上面的推斷。
上面涉及了一個_CFRunLoopRun函數(shù),接下來說明下iOS中訪問和使用RunLoop的API:
Foundation--NSRunLoop
Core Foundation--CFRunLoopRef(開源)
因為后者是開源的,且前者是在后者上針對OC的封裝,所以一般是對CFRunLoopRef進行研究。
兩套API對應(yīng)獲取RunLoop對象的方式:
Foundation
[NSRunLoop currentRunLoop]; // 當前runloop
[NSRunLoop mainRunLoop];// 主線程runloop
Core Foundation
CFRunLoopGetCurrent();// 當前runloop
CFRunLoopGetMain();// 主線程runloop
值得注意的是,獲取當前RunLoop都是進行懶加載的,也就是調(diào)用時自動創(chuàng)建線程對應(yīng)的RunLoop。
RunLoop相關(guān)類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

類之間的關(guān)系
以上圖片說明了各個類之間的關(guān)系。
CFRunLoopModeRef說明:
代表RunLoop的運行模式,一個RunLoop可以包含多個Mode,每個Mode可以包含多個Source、Timer、Observer
每次RunLoop啟動時,只能指定其中一個Mode,這個Mode就變成了CurrentMode
當啟動RunLoop時,如果所在Mode中沒有Source、Timer、Observer,那么將不會進入RunLoop,會直接結(jié)束
如果要切換Mode,只能退出Loop,再重新制定一個Mode進入
系統(tǒng)默認注冊了5個Mode:
NSDefaultRunLoopMode:App的默認Mode,通常主線程是在這個Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
NSRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode
關(guān)于NSRunLoopCommonModes:
一個Mode可以將自己標記為“Common”屬性,每當 RunLoop 的內(nèi)容發(fā)生變化時,RunLoop會對標記有“Common”屬性的Mode進行相適應(yīng)的切換,并同步Source/Observer/Timer
在主線程中,kCFRunLoopDefaultMode 和 UITrackingRunLoopMode這兩個Mode都是被默認標記為“Common”屬性的,從輸出的主線程RunLoop可以查看。

- 結(jié)合上面兩點,當使用NSRunLoopCommonModes占位時,會表明使用標記為“Common”屬性的Mode,在一定層度上,可以說是“擁有了兩個Mode”,可以在這兩個Mode中的其中任意一個進行工作
CFRunLoopTimerRef說明:
CFRunLoopTimerRef是基于時間的觸發(fā)器,它包含了一個時間長度和一個回調(diào)函數(shù)指針。當它加入到RunLoop時,RunLoop會注冊對應(yīng)的時間點,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)
CFRunLoopTimerRef大部分指的是NSTimer,它受RunLoop的Mode影響
由于NSTimer在RunLoop中處理,所以受其影響較大,有時可能會不準確。還有一種定時器是GCD定時器,它并不在RunLoop中,所以不受其影響,也就比較精確
接下來說明各種Mode下,NSTimer的工作情況:
情況1
在對創(chuàng)建的定時器進行模式修改前,scheduledTimerWithTimeInterval創(chuàng)建的定時器只在NSDefaultRunLoopMode模式下可以正常運行,當滾動UIScroolView時,模式轉(zhuǎn)換成UITrackingRunLoopMode,定時器就失效了。
修改成NSRunLoopCommonModes后,定時器在兩個模式下都可以正常運行
// 創(chuàng)建的定時器默認添加到當前的RunLoop中(沒有就創(chuàng)建),而且是NSDefaultRunLoopMode模式NSTimer*timer = [NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 可以通過以下方法對模型進行修改[[NSRunLoopmainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
情況2
timerWithTimeInterval創(chuàng)建的定時器并沒有手動添加進RunLoop,所以需要手動進行添加。當添加為以下模式時,定時器只在UITrackingRunLoopMode模式下進行工作,也就是滑動UIScrollView時就會工作,停止滑動時就不工作
如果把UITrackingRunLoopMode換成NSDefaultRunLoopMode,那么效果就和情況1沒修改Mode前的效果一樣
NSTimer*timer = [NSTimertimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 在UITrackingRunLoopMode模式下定時器才會運行[[NSRunLoopmainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
CFRunLoopSourceRef說明:
Source分類
按官方文檔
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
按照函數(shù)調(diào)用棧
Source0:非基于Port的
Source0本身不能主動觸發(fā)事件,只包含了一個回調(diào)函數(shù)指針
Source1:基于Port的,通過內(nèi)核和其他線程通信,接收、分發(fā)系統(tǒng)事件
包含了mach_port和一個回調(diào)函數(shù)指針,接收到相關(guān)消息后,會分發(fā)給Source0進行處理
CFRunLoopObserverRef說明:
CFRunLoopObserverRef是觀察者,能夠監(jiān)聽RunLoop的狀態(tài)改變
能夠監(jiān)聽的狀態(tài)
typedefCF_OPTIONS(CFOptionFlags, CFRunLoopActivity){? ? ? ? kCFRunLoopEntry = (1UL <<0),// 進入RunLoopkCFRunLoopBeforeTimers = (1UL <<1),//即將處理timerkCFRunLoopBeforeSources = (1UL <<2),//即將處理SourceskCFRunLoopBeforeWaiting = (1UL <<5),//即將進入休眠kCFRunLoopAfterWaiting = (1UL <<6),//即將喚醒kCFRunLoopExit = (1UL <<7),//即將退出RunLoopkCFRunLoopAllActivities =0x0FFFFFFFU//所有活動};
添加監(jiān)聽者步驟
// 創(chuàng)建監(jiān)聽著CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {NSLog(@"%ld", activity);? ? });//? ? [[NSRunLoop currentRunLoop] getCFRunLoop]// 向當前runloop添加監(jiān)聽者CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 釋放內(nèi)存CFRelease(observer);
CF的內(nèi)存管理(Core Foundation):
1.凡是帶有Create、Copy、Retain等字眼的函數(shù),創(chuàng)建出來的對象,都需要在最后做一次release
比如CFRunLoopObserverCreate
2.release函數(shù):CFRelease(對象);
自動釋放池釋放的時間和RunLoop的關(guān)系:
注意,這里的自動釋放池指的是主線程的自動釋放池,我們看不見它的創(chuàng)建和銷毀。自己手動創(chuàng)建@autoreleasepool {}是根據(jù)代碼塊來的,出了這個代碼塊就釋放了。
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush()創(chuàng)建自動釋放池。其 order 是-2147483647,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。

第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優(yōu)先級最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。

在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏,開發(fā)者也不必顯示創(chuàng)建 Pool 了。
在自己創(chuàng)建線程時,需要手動創(chuàng)建自動釋放池AutoreleasePool
綜合上面,可以得到以下結(jié)論:

@autoreleasepool {}內(nèi)部實現(xiàn)
有以下代碼:
intmain(intargc,constchar* argv[]){@autoreleasepool{? ? }return0;}
查看編譯轉(zhuǎn)換后的代碼:
intmain(intargc,constchar* argv[]){/* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool;? ? }return0;}
__AtAutoreleasePool是什么呢?找到其定義:
struct__AtAutoreleasePool {? __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}? ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}void* atautoreleasepoolobj;};
可以看到__AtAutoreleasePool是一個類:
其構(gòu)造函數(shù)使用objc_autoreleasePoolPush創(chuàng)建了一個線程池,并保存給成員變量atautoreleasepoolobj。
其析構(gòu)函數(shù)使用objc_autoreleasePoolPop銷毀了線程池
結(jié)合以上信息,main函數(shù)里面的__autoreleasepool是一個局部變量。當其創(chuàng)建時,會調(diào)用構(gòu)造函數(shù)創(chuàng)建線程池,出了{}代碼塊時,局部變量被銷毀,調(diào)用其析構(gòu)函數(shù)銷毀線程池。
RunLoop實際應(yīng)用
常駐線程
當創(chuàng)建一個線程,并且希望它一直存在時,就需要使用到RunLoop,否則線程一執(zhí)行完任務(wù)就會停止。
要向線程存在,需要有強指針引用他,其他的代碼如下:
// 屬性@property(strong,nonatomic)NSThread*thread;// 創(chuàng)建線程_thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(test) object:nil];[_thread start];- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event{// 點擊時使線程_thread執(zhí)行test方法[selfperformSelector:@selector(test) onThread:_thread withObject:nilwaitUntilDone:NO];}//- (void)test{NSLog(@"__test__");}
就單單以上代碼,是不起效果的,因為線程沒有RunLoop,執(zhí)行完test后就停止了,無法再讓其執(zhí)行任務(wù)(強制start會崩潰)。
通過在子線程中給RunLoop添加監(jiān)聽者,可以了解下performSelector:onThread:內(nèi)部做的事情:
調(diào)用performSelector:onThread: 時,實際上它會創(chuàng)建一個Source0加到對應(yīng)線程的RunLoop里去,所以,如果對應(yīng)的線程沒有RunLoop,這個方法就會失效

// 這句在主線程中調(diào)用// _thread就是下面的線程[selfperformSelector:@selector(run) onThread:_thread withObject:nilwaitUntilDone:NO];
performSelecter:afterDelay:也是一樣的內(nèi)部操作方法,只是創(chuàng)建的Timer添加到當前線程的RunLoop中了

// 創(chuàng)建RunLoop即將喚醒監(jiān)聽者CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {// 打印喚醒前的RunLoopNSLog(@"%ld--%@", activity, [NSRunLoopcurrentRunLoop]);? ? });// 向當前runloop添加監(jiān)聽者CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 釋放內(nèi)存CFRelease(observer);? ? [selfperformSelector:@selector(setView:) withObject:nilafterDelay:2.0];// 使model不為空[[NSRunLoopcurrentRunLoop] addPort:[NSPortport] forMode:NSDefaultRunLoopMode];? ? [[NSRunLoopcurrentRunLoop] run];
綜合上面的解釋,可以知道performSelector:onThread:沒有起作用,是因為_thread線程內(nèi)部沒有RunLoop,所以需要在線程內(nèi)部創(chuàng)建RunLoop。
創(chuàng)建RunLoop并使對應(yīng)線程成為常駐線程的常見方式有2:
方式1
向創(chuàng)建的RunLoop添加NSPort(Sources),讓Mode不為空,RunLoop能進入循環(huán)不會退出
[[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];
方式2
讓RunLoop一直嘗試運行,判斷Mode是否為空,不是為空就進入RunLoop循環(huán)
while(1) {? ? [[NSRunLoopcurrentRunLoop] run];}
AFNetWorking就使用到了常駐線程:
創(chuàng)建常駐線程
+ (void)networkRequestThreadEntryPoint:(id)__unused object {@autoreleasepool{? ? ? ? [[NSThreadcurrentThread] setName:@"AFNetworking"];// 創(chuàng)建RunLoop并向Mode添加NSMachPort,使RunLoop不會退出NSRunLoop*runLoop = [NSRunLoopcurrentRunLoop];? ? ? ? [runLoop addPort:[NSMachPortport] forMode:NSDefaultRunLoopMode];? ? ? ? [runLoop run];? ? }}+ (NSThread*)networkRequestThread {staticNSThread*_networkRequestThread =nil;staticdispatch_once_toncePredicate;dispatch_once(&oncePredicate, ^{? ? ? ? _networkRequestThread = [[NSThreadalloc] initWithTarget:selfselector:@selector(networkRequestThreadEntryPoint:) object:nil];? ? ? ? [_networkRequestThread start];? ? });return_networkRequestThread;}
使用常駐線程
- (void)start {? ? [self.locklock];if([selfisCancelled]) {? ? ? ? [selfperformSelector:@selector(cancelConnection) onThread:[[selfclass] networkRequestThread] withObject:nilwaitUntilDone:NOmodes:[self.runLoopModesallObjects]];? ? }elseif([selfisReady]) {self.state= AFOperationExecutingState;? ? ? ? [selfperformSelector:@selector(operationDidStart) onThread:[[selfclass] networkRequestThread] withObject:nilwaitUntilDone:NOmodes:[self.runLoopModesallObjects]];? ? }? ? [self.lockunlock];}
給子線程開啟定時器
_thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(test) object:nil];[_thread start];// 子線程添加定時器- (void)subTimer{// 默認創(chuàng)建RunLoop并向其model添加timer,所以后續(xù)只需要讓RunLoop run起來即可[NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 貌似source1不為空,source0就不為空//? ? [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoopcurrentRunLoop] run];}
讓某些事件(行為、任務(wù))在特定模式下執(zhí)行
比如圖片的設(shè)置,在UIScrollView滾動的情況下,我不希望設(shè)置圖片,等停止?jié)L動了再設(shè)置圖片,可以用以下代碼:
// 圖片只在NSDefaultRunLoopMode模式下會進行設(shè)置顯示[self.imageViewperformSelector:@selector(setImage:) withObject:[UIImageimageNamed:@"Snip20150712_39"] afterDelay:2.0inModes:@[NSDefaultRunLoopMode]];
先設(shè)置任務(wù)在NSDefaultRunLoopMode模式在執(zhí)行,這樣,在滾動使RunLoop進入UITrackingRunLoopMode時,就不會進行圖片的設(shè)置了。
控制定時器在特定模式下執(zhí)行
上文的《CFRunLoopTimerRef說明:》中已經(jīng)指出
添加Observer監(jiān)聽RunLoop的狀態(tài)
監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)
具體步驟在《CFRunLoopObserverRef說明:》中已寫明
GCD定時器
注意:
dispatch_source_t是個類,這點比較特殊
//? ? dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());dispatch_source_ttimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0,0, dispatch_get_global_queue(0,0));? ? dispatch_source_set_timer(timer, DISPATCH_TIME_NOW,1.0* NSEC_PER_SEC,0* NSEC_PER_SEC);? ? dispatch_source_set_event_handler(timer, ^{? ? ? ? NSLog(@"__");? ? ? ? NSLog(@"%@", [NSThread currentThread]);staticNSInteger count =0;if(count++ ==3) {// 為什么dispatch_cancel不能用_timer?/// Controlling expression type '__strong dispatch_source_t' (aka 'NSObject *__strong') not compatible with any generic association type// 類型錯誤,可能dispatch_cancel是宏定義,需要的就是方法調(diào)用,而不是變量//? ? ? ? ? ? dispatch_cancel(self.timer);dispatch_source_cancel(_timer);? ? ? ? }? ? });// 定時器默認是停止的,需要手動恢復dispatch_resume(timer);// 需要一個強引用保證timer不被釋放_timer = timer;
最后一點需要說明的是,SDWebImage框架的下載圖片業(yè)務(wù)中也使用到了RunLoop,老確保圖片下載成功后才關(guān)閉任務(wù)子線程。
參考文檔
如果你喜歡這里的專題, 請直接添加關(guān)注哦, 如果你喜歡這里的總結(jié), 可以打賞作者哦
一塊錢是你小小的心意,也是作者無悔的付出, 這些總結(jié)的價值講一直持續(xù)給你,每天
三篇文章,歡迎你來關(guān)注哦