runtime 和 runloop 作為一個程序員進階是必須的,也是非常重要的, 在面試過程中是經常會被問到的, 所以大家有必要進行研究,有能力的童鞋可以和下面作者一樣, 親歷實踐一下。
在簡書里發現了兩篇非常好的文章介紹 runtime和runloop的,在這里合二為一了, 把原版作者的東西拿了過來, 為了尊重作者,在這里注明一下 @sam_lau 是runtime的作者, @tripleCC是runloop的作者?
RunTime
Objective-C是基于C語言加入了面向對象特性和消息轉發機制的動態語言,這意味著它不僅需要一個編譯器,還需要Runtime系統來動態創建類和對象,進行消息發送和轉發。下面通過分析Apple開源的Runtime代碼(我使用的版本是objc4-646.tar)來深入理解Objective-C的Runtime機制。
Runtime數據結構
在Objective-C中,使用[receiver message]語法并不會馬上執行receiver對象的message方法的代碼,而是向receiver發送一條message消息,這條消息可能由receiver來處理,也可能由轉發給其他對象來處理,也有可能假裝沒有接收到這條消息而沒有處理。其實[receiver message]被編譯器轉化為:
idobjc_msgSend (idself, SEL op, ... );
下面從兩個數據結構id和SEL來逐步分析和理解Runtime有哪些重要的數據結構。
SEL
SEL是函數objc_msgSend第二個參數的數據類型,表示方法選擇器,按下面路徑打開objc.h文件
SEL Data Structure
查看到SEL數據結構如下:
typedefstructobjc_selector *SEL;
其實它就是映射到方法的C字符串,你可以通過Objc編譯器命令@selector()或者Runtime系統的sel_registerName函數來獲取一個SEL類型的方法選擇器。
如果你知道selector對應的方法名是什么,可以通過NSString* NSStringFromSelector(SEL aSelector)方法將SEL轉化為字符串,再用NSLog打印。
id
接下來看objc_msgSend第一個參數的數據類型id,id是通用類型指針,能夠表示任何對象。按下面路徑打開objc.h文件
id Data Structure.png
查看到id數據結構如下:
/// 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結構體指針,它包含一個Class isa成員,根據isa指針就可以順藤摸瓜找到對象所屬的類。
注意:根據Apple的官方文檔Key-Value Observing Implementation Details提及,key-value observing是使用isa-swizzling的技術實現的,isa指針在運行時被修改,指向一個中間類而不是真正的類。所以,你不應該使用isa指針來確定類的關系,而是使用class方法來確定實例對象的類。
Class
isa指針的數據類型是Class,Class表示對象所屬的類,按下面路徑打開objc.h文件
Class Data Structure
/// An opaque type that represents an Objective-C class.typedefstructobjc_class *Class;
可以查看到Class其實就是一個objc_class結構體指針,但這個頭文件找不到它的定義,需要在runtime.h才能找到objc_class結構體的定義。
按下面路徑打開runtime.h文件
objc_class Data Structure
查看到objc_class結構體定義如下:
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系統運行版本進行約束的宏定義,主要為了兼容非Objective-C 2.0的遺留版本,但我們仍能從中獲取一些有用信息。
讓我們分析一些重要的成員變量表示什么意思和對應使用哪些數據結構。
isa表示一個Class對象的Class,也就是Meta Class。在面向對象設計中,一切都是對象,Class在設計中本身也是一個對象。我們會在objc-runtime-new.h文件找到證據,發現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......}
由此可見,結構體objc_class也是繼承objc_object,說明Class在設計中本身也是一個對象。
其實Meta Class也是一個Class,那么它也跟其他Class一樣有自己的isa和super_class指針,關系如下:
Class isa and superclass relationship from Google
上圖實線是super_class指針,虛線是isa指針。有幾個關鍵點需要解釋以下:
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表示實例對象對應的父類
name表示類名
ivars表示多個成員變量,它指向objc_ivar_list結構體。在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結構體存儲類的單個成員變量信息。
methodLists表示方法列表,它指向objc_method_list結構體的二級指針,可以動態修改*methodLists的值來添加成員方法,也是Category實現原理,同樣也解釋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結構體存儲類的某個方法的信息。
cache用來緩存經常訪問的方法,它指向objc_cache結構體,后面會重點講到。
protocols表示類遵循哪些協議
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結構體指針,它存儲了方法名(method_name)、方法類型(method_types)和方法實現(method_imp)等信息。而method_imp的數據類型是IMP,它是一個函數指針,后面會重點提及。
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結構體指針,它包含了變量名(ivar_name)、變量類型(ivar_type)等信息。
IMP
在上面講Method時就說過,IMP本質上就是一個函數指針,指向方法的實現,在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
當你向某個對象發送一條信息,可以由這個函數指針來指定方法的實現,它最終就會執行那段代碼,這樣可以繞開消息傳遞階段而去執行另一個方法實現。
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的鏈表,主要是為了優化方法調用的性能。當對象receiver調用方法message時,首先根據對象receiver的isa指針查找到它對應的類,然后在類的methodLists中搜索方法,如果沒有找到,就使用super_class指針到父類中的methodLists查找,一旦找到就調用方法。如果沒有找到,有可能消息轉發,也可能忽略它。但這樣查找方式效率太低,因為往往一個類大概只有20%的方法經常被調用,占總調用次數的80%。所以使用Cache來緩存經常調用的方法,當調用方法時,優先在Cache查找,如果沒有找到,再到methodLists查找。
消息發送
前面從objc_msgSend作為入口,逐步深入分析Runtime的數據結構,了解每個數據結構的作用和它們之間關系后,我們正式轉入消息發送這個正題。
objc_msgSend函數
在前面已經提過,當某個對象使用語法[receiver message]來調用某個方法時,其實[receiver message]被編譯器轉化為:
idobjc_msgSend (idself, SEL op, ... );
現在讓我們看一下objc_msgSend它具體是如何發送消息:
首先根據receiver對象的isa指針獲取它對應的class
優先在class的cache查找message方法,如果找不到,再到methodLists查找
如果沒有在class找到,再到super_class查找
一旦找到message這個方法,就執行它實現的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調用class方法時,是在子類Son中查找方法,而super調用class方法時,是在父類Father中查找方法。
當調用[self class]方法時,會轉化為objc_msgSend函數,這個函數定義如下:
idobjc_msgSend(idself, SEL op, ...)
這時會從當前Son類的方法列表中查找,如果沒有,就到Father類查找,還是沒有,最后在NSObject類查找到。我們可以從NSObject.mm文件中看到- (Class)class的實現:
- (Class)class{returnobject_getClass(self);}
所以NSLog(@"%@", NSStringFromClass([self class]));會輸出Son。
當調用[super class]方法時,會轉化為objc_msgSendSuper,這個函數定義如下:
idobjc_msgSendSuper(structobjc_super *super, SEL op, ...)
objc_msgSendSuper函數第一個參數super的數據類型是一個指向objc_super的結構體,從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
結構體包含兩個成員,第一個是receiver,表示某個類的實例。第二個是super_class表示當前類的父類。
這時首先會構造出objc_super結構體,這個結構體第一個成員是self,第二個成員是(id)class_getSuperclass(objc_getClass("Son")),實際上該函數會輸出Father。然后在Father類查找class方法,查找不到,最后在NSObject查到。此時,內部使用objc_msgSend(objc_super->receiver, @selector(class))去調用,與[self class]調用相同,所以結果還是Son。
隱藏參數self和_cmd
當[receiver message]調用方法時,系統會在運行時偷偷地動態傳入兩個隱藏參數self和_cmd,之所以稱它們為隱藏參數,是因為在源代碼中沒有聲明和定義這兩個參數。至于對于self的描述,上面已經解釋非常清楚了,下面我們重點講解_cmd。
_cmd表示當前調用方法,其實它就是一個方法選擇器SEL。一般用于判斷方法名或在Associated Objects中唯一標識鍵名,后面在Associated Objects會講到。
方法解析與消息轉發
[receiver message]調用方法時,如果在message方法在receiver對象的類繼承體系中沒有找到方法,那怎么辦?一般情況下,程序在運行時就會Crash掉,拋出unrecognized selector sent to …類似這樣的異常信息。但在拋出異常之前,還有三次機會按以下順序讓你拯救程序。
Method Resolution
Fast Forwarding
Normal Forwarding
Message Forward from Google
Method Resolution
首先Objective-C在運行時調用+ resolveInstanceMethod:或+ resolveClassMethod:方法,讓你添加方法的實現。如果你添加方法并返回YES,那系統在運行時就會重新啟動一次消息發送的過程。
舉一個簡單例子,定義一個類Message,它主要定義一個方法sendMessage,下面就是它的設計與實現:
@interfaceMessage:NSObject- (void)sendMessage:(NSString*)word;@end
@implementationMessage- (void)sendMessage:(NSString*)word{NSLog(@"normal way : send message = %@", word);}@end
如果我在viewDidLoad方法中創建Message對象并調用sendMessage方法:
- (void)viewDidLoad {? ? [superviewDidLoad];? ? Message *message = [Messagenew];? ? [message sendMessage:@"Sam Lau"];}
控制臺會打印以下信息:
normal way :sendmessage = Sam Lau
但現在我將原來sendMessage方法實現給注釋掉,覆蓋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@*,它表示方法的參數和返回值,詳情請參考Type Encodings
如果resolveInstanceMethod方法返回NO,運行時就跳轉到下一步:消息轉發(Message Forwarding)
Fast Forwarding
如果目標對象實現- forwardingTargetForSelector:方法,系統就會在運行時調用這個方法,只要這個方法返回的不是nil或self,也會重啟消息發送的過程,把這消息轉發給其他對象來處理。否則,就會繼續Normal Fowarding。
繼續上面Message類的例子,將sendMessage和resolveInstanceMethod方法注釋掉,然后添加forwardingTargetForSelector方法的實現:
#pragma mark - Fast Forwarding- (id)forwardingTargetForSelector:(SEL)aSelector{if(aSelector ==@selector(sendMessage:)) {return[MessageForwarding new];? ? }returnnil;}
此時還缺一個轉發消息的類MessageForwarding,這個類的設計與實現如下:
@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,是因為這一步不會創建NSInvocation對象,但Normal Forwarding會創建它,所以相對于更快點。
Normal Forwarding
如果沒有使用Fast Forwarding來消息轉發,最后只有使用Normal Forwarding來進行消息轉發。它首先調用methodSignatureForSelector:方法來獲取函數的參數和返回值,如果返回為nil,程序會Crash掉,并拋出unrecognized selector sent to instance異常信息。如果返回一個函數簽名,系統就會創建一個NSInvocation對象并調用-forwardInvocation:方法。
繼續前面的例子,將forwardingTargetForSelector方法注釋掉,添加methodSignatureForSelector和forwardInvocation方法的實現:
#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];}}
關于這個例子的示例代碼請到github下載。
三種方法的選擇
Runtime提供三種方式來將原來的方法實現代替掉,那該怎樣選擇它們呢?
Method Resolution:由于Method Resolution不能像消息轉發那樣可以交給其他對象來處理,所以只適用于在原來的類中代替掉。
Fast Forwarding:它可以將消息處理轉發給其他對象,使用范圍更廣,不只是限于原來的對象。
Normal Forwarding:它跟Fast Forwarding一樣可以消息轉發,但它能通過NSInvocation對象獲取更多消息發送的信息,例如: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屬性,但在實現文件中編譯器是無法synthesize任何實例變量和屬性訪問方法。這時需要自定義屬性訪問方法并且使用Associated Objects來給已存在的類Category添加自定義的屬性。Associated Objects提供三個API來向對象添加、獲取和刪除關聯值:
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內存管理的引用計數機制。
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. */};
下面有個關于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是滿足這個要求的,所以上面的采用隱藏參數_cmd作為key。
Method Swizzling
Method Swizzling就是在運行時將一個方法的實現代替為另一個方法的實現。如果能夠利用好這個技巧,可以寫出簡潔、有效且維護性更好的代碼。可以參考兩篇關于Method Swizzling技巧的文章:
Aspect-Oriented Programming(AOP)
類似記錄日志、身份驗證、緩存等事務非常瑣碎,與業務邏輯無關,很多地方都有,又很難抽象出一個模塊,這種程序設計問題,業界給它們起了一個名字叫橫向關注點(Cross-cutting concern),AOP作用就是分離橫向關注點(Cross-cutting concern)來提高模塊復用性,它可以在既有的代碼添加一些額外的行為(記錄日志、身份驗證、緩存)而無需修改代碼。
危險性
Method Swizzling就像一把瑞士小刀,如果使用得當,它會有效地解決問題。但使用不當,將帶來很多麻煩。在stackoverflow上有人已經提出這樣一個問題:What are the Dangers of Method Swizzling in Objective C?,它的危險性主要體現以下幾個方面:
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
總結
雖然在平時項目不是經常用到Objective-C的Runtime特性,但當你閱讀一些iOS開源項目時,你就會發現很多時候都會用到。所以深入理解Objective-C的Runtime數據結構、消息轉發機制有助于你更容易地閱讀和學習開源項目。
擴展閱讀
What are the Dangers of Method Swizzling in Objective C?
RunLoop
深入理解RunLoop這篇文章寫的很好!
簡介
RunLoop顧名思義,就是運行循環的意思。
基本作用:
保持程序的持續運行
處理App中的各類事件(觸摸事件、定時器事件、Selector事件)
節省CPU資源,提高程序性能:沒有事件時就進行睡眠狀態
內部實現:
do-while循環,在這個循環內部不斷地處理各種任務(Source\Timeer\Observer)
注意點:
一個線程對應一個RunLoop(采用字典存儲,線程號為key,RunLoop為value)
主線程的RunLoop默認已經啟動,子線程的RunLoop需要手動啟動
RunLoop只能選擇一個Mode啟動,如果當前Mode沒有任何Source、Timer、Observer,那么就不會進入RunLoop
RunLoop的主要函數調用順序為:CFRunLoopRun->CFRunLoopRunSpecific->__CFRunLoopRun
注意特殊情況,事實上,在只有Observer的情況,也不一定會進入循環,因為源代碼里面只會顯式地檢測兩個東西:Source和Timer(這兩個是主動向RunLoop發送消息的);Observer是被動接收消息的
RunLoop在第一次獲取時創建,在線程結束時銷毀
RunLoop循環示意圖:(針對上面的__CFRunLoopRun函數,Mode已經判斷非空前提)
圖1
RunLoop循環示意圖
圖2
接觸過微處理器編程的基本上都知道,在編寫微處理器程序時,我通常會在main函數中寫一個無限循環,然后在這個循環里面對外部事件進行監聽,比如外部中斷,一些傳感器的數據等,在沒有外部中斷時,就讓CPU進入低功耗模式。如果接收到了外部中斷,就恢復到正常模式,對中斷進行處理。
while(1) {// 根據中斷決定是否切換模式執行任務}// 或者for(;;) {}
RunLoop和這個相似,也是在線程的main中增加了一個循環:
intmain(intargc,char* argv[]) {BOOLrunning =YES;do{// 執行各種任務,處理各種事件// ......}while(running);return0;}
所以線程在這種情況下,便不會退出。
關于MainRunLoop:
intmain(intargc,char* argv[]) {@autoreleasepool{returnUIApplicationMain(argc, argv,nil,NSStringFromClass([AppDelegate class]));? ? }}
在viewDidLoad中設置斷電,然后得到以下主線程棧信息:
可以看到,UIApplicationMain內部啟動了一個和主線程相關聯的RunLoop(_CFRunLoopRun)。在這里也可以推斷,程序進入UIApplicationMain就不會退出了。我稍微對主函數進行了如下修改,并在return語句上打印了斷點:
運行程序后,并不會在斷點處停下,證實了上面的推斷。
上面涉及了一個_CFRunLoopRun函數,接下來說明下iOS中訪問和使用RunLoop的API:
Foundation--NSRunLoop
Core Foundation--CFRunLoopRef(開源)
因為后者是開源的,且前者是在后者上針對OC的封裝,所以一般是對CFRunLoopRef進行研究。
兩套API對應獲取RunLoop對象的方式:
Foundation
[NSRunLoop currentRunLoop]; // 當前runloop
[NSRunLoop mainRunLoop];// 主線程runloop
Core Foundation
CFRunLoopGetCurrent();// 當前runloop
CFRunLoopGetMain();// 主線程runloop
值得注意的是,獲取當前RunLoop都是進行懶加載的,也就是調用時自動創建線程對應的RunLoop。
RunLoop相關類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
類之間的關系
以上圖片說明了各個類之間的關系。
CFRunLoopModeRef說明:
代表RunLoop的運行模式,一個RunLoop可以包含多個Mode,每個Mode可以包含多個Source、Timer、Observer
每次RunLoop啟動時,只能指定其中一個Mode,這個Mode就變成了CurrentMode
當啟動RunLoop時,如果所在Mode中沒有Source、Timer、Observer,那么將不會進入RunLoop,會直接結束
如果要切換Mode,只能退出Loop,再重新制定一個Mode進入
系統默認注冊了5個Mode:
NSDefaultRunLoopMode:App的默認Mode,通常主線程是在這個Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
NSRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode
關于NSRunLoopCommonModes:
一個Mode可以將自己標記為“Common”屬性,每當 RunLoop 的內容發生變化時,RunLoop會對標記有“Common”屬性的Mode進行相適應的切換,并同步Source/Observer/Timer
在主線程中,kCFRunLoopDefaultMode 和 UITrackingRunLoopMode這兩個Mode都是被默認標記為“Common”屬性的,從輸出的主線程RunLoop可以查看。
- 結合上面兩點,當使用NSRunLoopCommonModes占位時,會表明使用標記為“Common”屬性的Mode,在一定層度上,可以說是“擁有了兩個Mode”,可以在這兩個Mode中的其中任意一個進行工作
CFRunLoopTimerRef說明:
CFRunLoopTimerRef是基于時間的觸發器,它包含了一個時間長度和一個回調函數指針。當它加入到RunLoop時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調
CFRunLoopTimerRef大部分指的是NSTimer,它受RunLoop的Mode影響
由于NSTimer在RunLoop中處理,所以受其影響較大,有時可能會不準確。還有一種定時器是GCD定時器,它并不在RunLoop中,所以不受其影響,也就比較精確
接下來說明各種Mode下,NSTimer的工作情況:
情況1
在對創建的定時器進行模式修改前,scheduledTimerWithTimeInterval創建的定時器只在NSDefaultRunLoopMode模式下可以正常運行,當滾動UIScroolView時,模式轉換成UITrackingRunLoopMode,定時器就失效了。
修改成NSRunLoopCommonModes后,定時器在兩個模式下都可以正常運行
// 創建的定時器默認添加到當前的RunLoop中(沒有就創建),而且是NSDefaultRunLoopMode模式NSTimer*timer = [NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 可以通過以下方法對模型進行修改[[NSRunLoopmainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
情況2
timerWithTimeInterval創建的定時器并沒有手動添加進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
按照函數調用棧
Source0:非基于Port的
Source0本身不能主動觸發事件,只包含了一個回調函數指針
Source1:基于Port的,通過內核和其他線程通信,接收、分發系統事件
包含了mach_port和一個回調函數指針,接收到相關消息后,會分發給Source0進行處理
CFRunLoopObserverRef說明:
CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變
能夠監聽的狀態
typedefCF_OPTIONS(CFOptionFlags, CFRunLoopActivity){? ? ? ? kCFRunLoopEntry = (1UL <<0),// 進入RunLoopkCFRunLoopBeforeTimers = (1UL <<1),//即將處理timerkCFRunLoopBeforeSources = (1UL <<2),//即將處理SourceskCFRunLoopBeforeWaiting = (1UL <<5),//即將進入休眠kCFRunLoopAfterWaiting = (1UL <<6),//即將喚醒kCFRunLoopExit = (1UL <<7),//即將退出RunLoopkCFRunLoopAllActivities =0x0FFFFFFFU//所有活動};
添加監聽者步驟
// 創建監聽著CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {NSLog(@"%ld", activity);? ? });//? ? [[NSRunLoop currentRunLoop] getCFRunLoop]// 向當前runloop添加監聽者CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 釋放內存CFRelease(observer);
CF的內存管理(Core Foundation):
1.凡是帶有Create、Copy、Retain等字眼的函數,創建出來的對象,都需要在最后做一次release
比如CFRunLoopObserverCreate
2.release函數:CFRelease(對象);
自動釋放池釋放的時間和RunLoop的關系:
注意,這里的自動釋放池指的是主線程的自動釋放池,我們看不見它的創建和銷毀。自己手動創建@autoreleasepool {}是根據代碼塊來的,出了這個代碼塊就釋放了。
App啟動后,蘋果在主線程 RunLoop 里注冊了兩個 Observer,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一個 Observer 監視的事件是 Entry(即將進入Loop),其回調內會調用 _objc_autoreleasePoolPush()創建自動釋放池。其 order 是-2147483647,優先級最高,保證創建釋放池發生在其他所有回調之前。
第二個 Observer 監視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()釋放舊的池并創建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池。這個 Observer 的 order 是 2147483647,優先級最低,保證其釋放池子發生在其他所有回調之后。
在主線程執行的代碼,通常是寫在諸如事件回調、Timer回調內的。這些回調會被 RunLoop 創建好的 AutoreleasePool 環繞著,所以不會出現內存泄漏,開發者也不必顯示創建 Pool 了。
在自己創建線程時,需要手動創建自動釋放池AutoreleasePool
綜合上面,可以得到以下結論:
@autoreleasepool {}內部實現
有以下代碼:
intmain(intargc,constchar* argv[]){@autoreleasepool{? ? }return0;}
查看編譯轉換后的代碼:
intmain(intargc,constchar* argv[]){/* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool;? ? }return0;}
__AtAutoreleasePool是什么呢?找到其定義:
struct__AtAutoreleasePool {? __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}? ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}void* atautoreleasepoolobj;};
可以看到__AtAutoreleasePool是一個類:
其構造函數使用objc_autoreleasePoolPush創建了一個線程池,并保存給成員變量atautoreleasepoolobj。
其析構函數使用objc_autoreleasePoolPop銷毀了線程池
結合以上信息,main函數里面的__autoreleasepool是一個局部變量。當其創建時,會調用構造函數創建線程池,出了{}代碼塊時,局部變量被銷毀,調用其析構函數銷毀線程池。
RunLoop實際應用
常駐線程
當創建一個線程,并且希望它一直存在時,就需要使用到RunLoop,否則線程一執行完任務就會停止。
要向線程存在,需要有強指針引用他,其他的代碼如下:
// 屬性@property(strong,nonatomic)NSThread*thread;// 創建線程_thread = [[NSThreadalloc] initWithTarget:selfselector:@selector(test) object:nil];[_thread start];- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event{// 點擊時使線程_thread執行test方法[selfperformSelector:@selector(test) onThread:_thread withObject:nilwaitUntilDone:NO];}//- (void)test{NSLog(@"__test__");}
就單單以上代碼,是不起效果的,因為線程沒有RunLoop,執行完test后就停止了,無法再讓其執行任務(強制start會崩潰)。
通過在子線程中給RunLoop添加監聽者,可以了解下performSelector:onThread:內部做的事情:
調用performSelector:onThread: 時,實際上它會創建一個Source0加到對應線程的RunLoop里去,所以,如果對應的線程沒有RunLoop,這個方法就會失效
// 這句在主線程中調用// _thread就是下面的線程[selfperformSelector:@selector(run) onThread:_thread withObject:nilwaitUntilDone:NO];
performSelecter:afterDelay:也是一樣的內部操作方法,只是創建的Timer添加到當前線程的RunLoop中了
// 創建RunLoop即將喚醒監聽者CFRunLoopObserverRefobserver =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers,YES,0, ^(CFRunLoopObserverRefobserver,CFRunLoopActivityactivity) {// 打印喚醒前的RunLoopNSLog(@"%ld--%@", activity, [NSRunLoopcurrentRunLoop]);? ? });// 向當前runloop添加監聽者CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);// 釋放內存CFRelease(observer);? ? [selfperformSelector:@selector(setView:) withObject:nilafterDelay:2.0];// 使model不為空[[NSRunLoopcurrentRunLoop] addPort:[NSPortport] forMode:NSDefaultRunLoopMode];? ? [[NSRunLoopcurrentRunLoop] run];
綜合上面的解釋,可以知道performSelector:onThread:沒有起作用,是因為_thread線程內部沒有RunLoop,所以需要在線程內部創建RunLoop。
創建RunLoop并使對應線程成為常駐線程的常見方式有2:
方式1
向創建的RunLoop添加NSPort(Sources),讓Mode不為空,RunLoop能進入循環不會退出
[[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop]run];
方式2
讓RunLoop一直嘗試運行,判斷Mode是否為空,不是為空就進入RunLoop循環
while(1) {? ? [[NSRunLoopcurrentRunLoop] run];}
AFNetWorking就使用到了常駐線程:
創建常駐線程
+ (void)networkRequestThreadEntryPoint:(id)__unused object {@autoreleasepool{? ? ? ? [[NSThreadcurrentThread] setName:@"AFNetworking"];// 創建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{// 默認創建RunLoop并向其model添加timer,所以后續只需要讓RunLoop run起來即可[NSTimerscheduledTimerWithTimeInterval:1.0target:selfselector:@selector(run) userInfo:nilrepeats:YES];// 貌似source1不為空,source0就不為空//? ? [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoopcurrentRunLoop] run];}
讓某些事件(行為、任務)在特定模式下執行
比如圖片的設置,在UIScrollView滾動的情況下,我不希望設置圖片,等停止滾動了再設置圖片,可以用以下代碼:
// 圖片只在NSDefaultRunLoopMode模式下會進行設置顯示[self.imageViewperformSelector:@selector(setImage:) withObject:[UIImageimageNamed:@"Snip20150712_39"] afterDelay:2.0inModes:@[NSDefaultRunLoopMode]];
先設置任務在NSDefaultRunLoopMode模式在執行,這樣,在滾動使RunLoop進入UITrackingRunLoopMode時,就不會進行圖片的設置了。
控制定時器在特定模式下執行
上文的《CFRunLoopTimerRef說明:》中已經指出
添加Observer監聽RunLoop的狀態
監聽點擊事件的處理(在所有點擊事件之前做一些事情)
具體步驟在《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是宏定義,需要的就是方法調用,而不是變量//? ? ? ? ? ? dispatch_cancel(self.timer);dispatch_source_cancel(_timer);? ? ? ? }? ? });// 定時器默認是停止的,需要手動恢復dispatch_resume(timer);// 需要一個強引用保證timer不被釋放_timer = timer;
最后一點需要說明的是,SDWebImage框架的下載圖片業務中也使用到了RunLoop,老確保圖片下載成功后才關閉任務子線程。
參考文檔
如果你喜歡這里的專題, 請直接添加關注哦, 如果你喜歡這里的總結, 可以打賞作者哦
一塊錢是你小小的心意,也是作者無悔的付出, 這些總結的價值講一直持續給你,每天
三篇文章,歡迎你來關注哦