runtime 是什么?
- runtime 又叫做運行時,是一套底層的 C 語言API,其為 iOS 內部的核心之一,我們平時編寫 oc代碼,底層都是基于它來實現的。比如
[receiver message]
// 底層運行時會被編譯器轉化為:
objc_msgSend(receiver, message);
// 有參數的
[receiver message:(id)arg...];
objc_msgSend(receiver, seletor, arg1, arg2, ...);
為什么需要 runtime
- oc 是一門動態語言,它會將一些工作放在代碼運行時才處理并非編譯時。也就是說,有很多類和成員變量在我們編譯的時候是不知道的,而在運行時,我們編寫的代碼才會被轉換成完整的確定 的代碼運行。
- 因此,編譯器是不夠的,我們還需要一個運行時系統(runtime system)來處理編譯后的代碼。
- runtime 基本是用 c 和匯編寫成的,由此可見蘋果為了動態系統的高效做出的努力。蘋果的 GNU 各自維護一個開源的 runtime 版本,這兩個版本之間都在努力保持一致。
runtime 的作用
- oc 在3個層面上與 runtime 系統進行交互:
- 通過 oc 源碼,只要需要 oc 代碼,runtime 系統自動在幕后搞定一切,調用方法,編譯器會將 oc 代碼轉化成運行時代碼,在運行時確定數據結構和函數。
- 通過 Foundation 框架的 NSObject 類定義方法。cocoa程序中絕大多數都是 NSObject 的子類,所有都繼承了 NSObject 的行為。(NSProxy 類是個例外,它是一個抽象類)。
- 一些情況下 NSObject 類僅僅定義了完成某件事情的模板,并沒有提供所需要的代碼。例如:- description 方法,該類方法返回類內容的字符串表示,該方法主要用來調試程序。NSObject 類并不知道子類的內容,所以它只是返回類的名字和對象的地址,NSObject 的子類可以重新實現。
- 還有一些 NSObject 的方法可以通過 runtime 系統中獲取信息,允許對象進行自我檢查。例如:
- -class 方法返回對象的類:
- -isKindOfClass:和-IsMemberOfClass:方法檢查對象是否存在指定的類的繼承體系中(是否是其子類或者父類或者當前類的成員變量)
- -respondsToSelector:檢查對象是否響應指定的消息
- -conformsToProtocol:檢查對象是否實現了指定協議類的方法
- -methodForSelector:返回指定方法實現的地址
- 通過對 Runtime 庫函數的直接調用
- runtime 系統是具有公共接口的動態共享庫。
- 許多函數可以讓你使用純 C 代碼實現 objc 同樣的功能。除非是寫一些 objc 與其他語言橋接或者底層的 debug 工作,你在寫 objc 代碼時一般不會用到這些 c 語言函數。
runtime 的相關術語
-
SEL
- 它是selector 在 objc 中的表示。selector 是方法選擇器,其實作用和名字一樣,日常生活中,我們通過人名辨別誰是誰,注意 objc 在相同的類中不會有命名相同的兩個方法。selector 對方法進行包裝,以便找到對應的方法實現。他的數據結構是:typedef struct objc_selector *SEL; 我們可以看出它是一個映射到方法 C 字符串,你可以通過 objc 編譯器命令@selector()或者 runtime 系統的 sel_registerName 函數來獲取一個 SEL 類型的方法選擇器。
- 注意:不同類中相同名字的方法對應的 selector 是相同的,由于變量類型不同,所以不會導致他們調用方法實現混亂。
-
id
- id 是一個參數類型,他是指向某個類的實例指針。定義如下:
typedef struct objc_object *id; struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };
- 以上定義,看到 objc_object 結構體包含一個 isa 指針,根據 isa 指針就可以找到對應所屬的類。
- 注意:isa 指針在代碼運行時并不總指向實例對象所屬的類型,所以不能依靠它來確定類型,要響確定類型還是需要用對象的 -class 方法。PS:KVO 的實現原理就是將被觀察對象的 isa 指針指向一個中間類而不是真實類型。
-
Class
- typedef struct objc_class *Class;
- class 其實是指向 objc_class 的結構體的指針。objc_class 的數據結構如下
struct objc_class { Class _Nonnull isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class _Nullable super_class OBJC2_UNAVAILABLE; const char * _Nonnull name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE;、
- 從 objc_class 可以看出,一個運行時類中關聯了它的父類指針、類名、成員變量、方法、緩存以及附屬協議。
- 其中 objc_ivar_list 和 objc_method_list 分別是成員變量列表和方法列表:
// objc_ivar_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; } // objc_method_list的實現 struct objc_method_list { struct objc_method_list * _Nullable 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; }
- 由此可見,我們可以動態修改 *methodList 的值來添加成員方法,這也是 category 實現的原理,同樣解釋了 Category 不能添加屬性的原因
- objc_ivar_list 結構體用來存儲成員變量的列表,而 objc_ivar則是存儲了單個成員變量的信息;同理,objc_method_list 結構體存儲著方法數組的列表,而單個方法信息由 objc_method 結構體存儲。
- 值得注意的是,objc_class 中也有一個 isa 指針,這說明 objc 類本身也是一個對象。為了處理類和對象的關系,runtime 庫創建一個叫做 Meta Class(元類)的東西,類對象所屬的類叫做元類。meta Class 表述了對象本身所具備的元數據。
- 我們所熟悉的類方法,就源自于 meta Class。我們可以理解為類方法就是類對象的實例方法。每個類僅有一個類對象,而每個類對象僅有一個與之相關的元類。
- 當你發出一個類似[NSObject alloc](類方法)消息時,實際上,這個消息被發送給一個類對象(Class object),這個類對象丙戌是一個元類的實例,而這個元類同時也是一個根元類(root meta Class)的實例。所有元類的 isa 指針最終都指向根元類。
- 所以當[NSObject alloc];這條消息發送給類對象的時候,運行時代碼 objc_msgSend()會去元類中查找能夠響應的方法實現,如果找到了,就會對這個類對象執行方法調用。
- 最后 objc_class 中還有一個 objc_cahce,緩存。
-
method
- method 代表類中某個方法的類型
struct objc_method { SEL _Nonnull method_name OBJC2_UNAVAILABLE; char * _Nullable method_types OBJC2_UNAVAILABLE; IMP _Nonnull method_imp OBJC2_UNAVAILABLE; }
- 方法類型是 SEL
- 方法類型 method_types 是一個char 指針,存儲方法的參數類型和返回值類型
- method_imp 指向了方法實現,本質是一個函數指針
- Ivar 是表示成員變量的類型。
struct objc_ivar { char * _Nullable ivar_name OBJC2_UNAVAILABLE; char * _Nullable ivar_type OBJC2_UNAVAILABLE; int ivar_offset OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif }
- 其中 ivar_offset 是基地址便宜字節
-
IMP
- IMP 在 objc.h 中定義的是
typedef void (*IMP)(void /* id, SEL, ... */ );
- 他是一個函數指針,這是由編譯器生成的。當你發起一個 objc 消息之后,最終他會執行哪段代碼,就是由這個函數指針制定的。而 IMP 這個函數指針就指向了這個方法的實現。
- 如果得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法。
- 你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含了 id 和 SEL 類型。每個方法名都對應一個 SEL 類型的方法選擇器,而每個實例中的 SEL 對應的方法實現肯定是唯一的,通過一組 id 和 SEL 參數就能確定唯一的方法實現地址。
- 而一個確定方法也只有唯一一組 id 和 SEL 參數。
-
cache
- 定義如下
typedef struct objc_cache *Cache OBJC2_UNAVAILABLE; struct objc_cache { unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method _Nullable buckets[1] OBJC2_UNAVAILABLE; };
- cache 為方法調用的性能進行了優化,每當實例對象接收一個消息時,它不會直接在 isa 指針指向的類的方法類別中遍歷查找能夠響應的方法,因為每次都要查找的效率太低了,而是優先在 cache 中找。
- runtime 系統會吧調用到的方法 cache 中,如果一個方法被調用,那么他有可能今后還會被調用,下次查找的時候就會效率更高。就像計算機組成原理中 CPU 繞過主存先訪問 cache 一樣。
-
property
typedef struct objc_property *objc_property_t;
- 可以通過 class_copyPropertyList 和 protocol_copyPropertyList 方法獲取類和協議中的屬性
OBJC_EXPORT objc_property_t _Nonnull * _Nullable class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0); OBJC_EXPORT objc_property_t _Nonnull * _Nullable protocol_copyPropertyList(Protocol * _Nonnull proto, unsigned int * _Nullable outCount) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 返回的是屬性列表,列表中的每個元素都是一個 objc_property_t 指針
@interface Person () @property (nonatomic, strong) NSString *name; @property (nonatomic, assign) int age; @property (nonatomic, assign) double weight; @end // 寫 person 添加3個屬性。通過 runtime 獲取運行時屬性。 unsigned int outCount = 0; objc_property_t *properties = class_copyPropertyList([Person class], &outCount); NSLog(@"%d", outCount); for (NSInteger i = 0; i < outCount; ++i) { NSString *name = @(property_getName(properties[i])); NSString *attributes = @(property_getAttributes(properties[i])); NSLog(@"name:%@\nattributes:%@", name, attributes); } [10522:615669] 4 [10522:615669] name:name attributes:T@"NSString",&,N,V_name [10522:615669] name:age attributes:Ti,N,V_age [10522:615669] name:weight attributes:Td,N,V_weight
runtime 與消息
- 消息知道運行時才會與方法實現進行綁定。
- objc_msgSend 方法看起來好像返回了數據,其實 objc_msgSend 從不返回數據,而是你的方法在運行時實現被調用后才會返回數據。消息發送步驟:
- 首先你要檢測 selector 是不是要忽略。mac 開發有了垃圾回收舊不理會 retain、release 這些函數。
- 檢測這個 selector 的 target 是不是 nil。objc 允許我們對一個 nil 對象執行任何方法不會 crash,因為運行時會被忽略掉。
- 如果上面兩步都通過了,那么就開始查找這個類的實現 IMP,先從 cache 中找,如果找到了就運行對應的函數去執行相應的代碼。
- 如果 cache 找不到就找類的方法列表中是否有對應的方法。
- 如果累的方法列表中找不到就到父類的方法列表中找,一直找到 NSObject 類為止。
- 如果還沒找到,就要開始進入動態方法解析了。
- 在消息傳遞中,編譯器會根據情況在 objc_msgSend,objc_msgSend_stret,objc_msgSendSuper,objc_msgSendSuper——stret 這個四個方法中選擇一個調用。如果消息傳遞給父類,那么會調用名字帶有 Super 的函數,如果消息返回值是數據結構而不是簡單值時,會調用帶有 stret 的函數。
方法中的隱藏參數
- 疑問:我們經常用到關鍵字 self,但是 self 是如何獲取當前方法的對象的呢?其實這也是 runtime 系統的作用,self 是在方法運行時被動態傳入的。
- 當 objc_msgSend 找到方法對應實現時,他將直接調用該方法實現,并將消息中所有參數都傳遞給方法實現,同時還有兩個隱藏參數:
- 接受消息的對象(self 所指向的內容,當前方法的對象指針)
- 方法選擇器(_cmd 指向的內容,當前指針的 SEL 指針)
- 因為在源代碼方法的定義中,我們并沒有發現這兩個參數的聲明。它們實在代碼編譯階段被插入方法實現中的。盡管這些參數沒有被明確聲明,在源碼中我們仍然可以引用它們。
- 這兩個參數中,self 更實用。他是在方法實現中訪問消息接收者對象的實例變量的途徑。
- 這時我們會想到另一個關鍵字 Super,實際上 Super 關鍵字接收消息時,編譯器會創建一個 objc_super 結構體
消息轉發
- 重定向
- 消息轉發機制執行前,runtime 系統允許我們替換消息的接收者為其他對象。通過- (id)forwardingTargetForSelector:(SEL)aSelector 方法。
- 如果返回為 nil 或者 self,則會計入消息轉發機制(forwardInvocation:),否則向返回的對象重新發送消息。
- 轉發
- 當動態方法解析不做處理返回 NO 時,則會觸發消息轉發機制。
動態綁定
- 在運行時確定要調用的方法,動態綁定將調用方法的確定也推遲到運行時。在編譯
時,方法的調用并不和代碼綁定在一起,只有在消實發送出來之后,才確定被調用的
代碼。通過動態類型和動態綁定技術,代碼每次執行都可以得到不同的結果。運行時
因子負責確定消息的接收者和被調用的方法。運行時的消息分發機制為動態綁定提供
支持。當向一個動態類型確定了的對象發送消息時,運行環境系統會通過接收者的isa
指針定位對象的類,并以此為起點確定被調用的方法,方法和消息是動態綁定的。而
且,不必在0bjective-C 代碼中做任何工作,就可以自動獲取動態綁定的好處。在每次發送消息時,特別是當消息的接收者是動態類型已經確定的對象時,動態綁定就會例行而透明地發生