參考資料:ios模式詳解,runtime完整總結 類和對象 Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了運行時來處理。這種動態語言的優勢在於:我們寫代碼時更具靈活性,如我們可以把消息轉發給我們想要的對象,或者隨意交換一個方法的實現等。 這種特性意味著Obje ...
參考資料:ios模式詳解,runtime完整總結
類和對象
Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了運行時來處理。這種動態語言的優勢在於:我們寫代碼時更具靈活性,如我們可以把消息轉發給我們想要的對象,或者隨意交換一個方法的實現等。
這種特性意味著Objective-C不僅需要一個編譯器,還需要一個運行時系統來執行編譯的代碼。對於Objective-C來說,這個運行時系統就像一個操作系統一樣:它讓所有的工作可以正常的運行。這個運行時系統即Objc Runtime。Objc Runtime其實是一個Runtime庫,它基本上是用C和彙編寫的,這個庫使得C語言有了面向對象的能力。
runtime 概念
Objective-C 是基於 C 的,它為 C 添加了面向對象的特性。它將很多靜態語言在編譯和鏈接時期做的事放到了 runtime 運行時來處理,可以說 runtime 是我們 Objective-C 幕後工作者。
-
runtime(
簡稱運行時
),是一套 純C(C和彙編寫的) 的API。而 OC 就是 運行時機制,也就是在運行時候的一些機制,其中最主要的是 消息機制。 -
對於 C 語言,函數的調用在編譯的時候會決定調用哪個函數。
-
OC的函數調用成為消息發送,屬於 動態調用過程。在編譯的時候並不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。
-
事實證明:在編譯階段,OC 可以 調用任何函數,即使這個函數並未實現,只要聲明過就不會報錯,只有當運行的時候才會報錯,這是因為OC是運行時動態調用的。而 C 語言 調用未實現的函數 就會報錯
類與對象基礎數據結構
Class
Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針。它的定義如下:
typedef struct objc_class *Class;
查看objc/runtime.h中objc_class結構體的定義如下:
1 struct objc_class { 2 Class isa OBJC_ISA_AVAILABILITY; 3 4 #if !__OBJC2__ 5 Class super_class OBJC2_UNAVAILABLE; // 父類 6 const char *name OBJC2_UNAVAILABLE; // 類名 7 long version OBJC2_UNAVAILABLE; // 類的版本信息,預設為0 8 long info OBJC2_UNAVAILABLE; // 類信息,供運行期使用的一些位標識 9 long instance_size OBJC2_UNAVAILABLE; // 該類的實例變數大小 10 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變數鏈表 11 struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表 12 struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存 13 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協議鏈表 14 #endif 15 16 } OBJC2_UNAVAILABLE;
在這個定義中,下麵幾個欄位是我們感興趣的
-
isa:需要註意的是在Objective-C中,所有的類自身也是一個對象,這個對象的Class裡面也有一個isa指針,它指向metaClass(元類),我們會在後面介紹它。
-
super_class:指向該類的父類,如果該類已經是最頂層的根類(如NSObject或NSProxy),則super_class為NULL。
-
cache:用於緩存最近使用的方法。一個接收者對象接收到一個消息時,它會根據isa指針去查找能夠響應這個消息的對象。在實際使用中,這個對象只有一部分方法是常用的,很多方法其實很少用或者根本用不上。這種情況下,如果每次消息來時,我們都是methodLists中遍歷一遍,性能勢必很差。這時,cache就派上用場了。在我們每次調用過一個方法後,這個方法就會被緩存到cache列表中,下次調用的時候runtime就會優先去cache中查找,如果cache沒有,才去methodLists中查找方法。這樣,對於那些經常用到的方法的調用,但提高了調用的效率。
-
version:我們可以使用這個欄位來提供類的版本信息。這對於對象的序列化非常有用,它可是讓我們識別出不同類定義版本中實例變數佈局的改變。
Runtime庫主要做下麵幾件事:
封裝:在這個庫中,對象可以用C語言中的結構體表示,而方法可以用C函數來實現,另外再加上了一些額外的特性。這些結構體和函數被runtime函數封裝後,我們就可以在程式運行時創建,檢查,修改類、對象和它們的方法了。
找出方法的最終執行代碼:當程式執行[object doSomething]時,會向消息接收者(object)發送一條消息(doSomething),runtime會根據消息接收者是否能響應該消息而做出不同的反應。這將在後面詳細介紹。
Objective-C runtime目前有兩個版本:Modern runtime和Legacy runtime。Modern Runtime 覆蓋了64位的Mac OS X Apps,還有 iOS Apps,Legacy Runtime 是早期用來給32位 Mac OS X Apps 用的,也就是可以不用管就是了。
針對cache,我們用下麵例子來說明其執行過程:
1 NSArray *array = [[NSArray alloc] init];
其流程是:
-
[NSArray alloc]先被執行。因為NSArray沒有+alloc方法,於是去父類NSObject去查找。
-
檢測NSObject是否響應+alloc方法,發現響應,於是檢測NSArray類,並根據其所需的記憶體空間大小開始分配記憶體空間,然後把isa指針指向NSArray類。同時,+alloc也被加進cache列表裡面。
-
接著,執行-init方法,如果NSArray響應該方法,則直接將其加入cache;如果不響應,則去父類查找。
-
在後期的操作中,如果再以[[NSArray alloc] init]這種方式來創建數組,則會直接從cache中取出相應的方法,直接調用。
objc_object與id
objc_object是表示一個類的實例的結構體,它的定義如下(objc/objc.h):
1 struct objc_object 2 { 3 Class isa OBJC_ISA_AVAILABILITY; 4 }; 5 6 typedef struct objc_object *id;
可以看到,這個結構體只有一個字體,即指向其類的isa指針。這樣,當我們向一個Objective-C對象發送消息時,運行時庫會根據實例對象的isa指針找到這個實例對象所屬的類。Runtime庫會在類的方法列表及父類的方法列表中去尋找與消息對應的selector指向的方法。找到後即運行這個方法。
當創建一個特定類的實例對象時,分配的記憶體包含一個objc_object數據結構,然後是類的實例變數的數據。NSObject類的alloc和allocWithZone:方法使用函數class_createInstance來創建objc_object數據結構。
另外還有我們常見的id,它是一個objc_object結構類型的指針。它的存在可以讓我們實現類似於C++中泛型的一些操作。
元類(Meta Class)
在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發送消息(即調用類方法)。如:
1 NSArray *array = [NSArray array];
這個例子中,+array消息發送給了NSArray類,而這個NSArray也是一個對象。既然是對象,那麼它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那麼這些就有一個問題了,這個isa指針指向什麼呢?為了調用+array方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念
meta-class是一個類對象的類。
當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。
meta-class之所以重要,是因為它存儲著一個類的所有類方法。每個類都會有一個單獨的meta-class,因為每個類的類方法基本不可能完全相同。
再深入一下,meta-class也是一個類,也可以向它發送一個消息,那麼它的isa又是指向什麼呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。這樣就形成了一個完美的閉環。
講了這麼多,我們還是來寫個例子吧:

1 void TestMetaClass(id self, SEL _cmd) { 2 3 NSLog(@"This objcet is %p", self); 4 NSLog(@"Class is %@, super class is %@", [self class], [self superclass]); 5 6 Class currentClass = [self class]; 7 for (int i = 0; i < 4; i++) { 8 NSLog(@"Following the isa pointer %d times gives %p", i, currentClass); 9 currentClass = objc_getClass((__bridge void *)currentClass); 10 } 11 12 NSLog(@"NSObject's class is %p", [NSObject class]); 13 NSLog(@"NSObject's meta class is %p", objc_getClass((__bridge void *)[NSObject class])); 14 } 15 16 #pragma mark - 17 18 @implementation Test 19 20 - (void)ex_registerClassPair { 21 22 Class newClass = objc_allocateClassPair([NSError class], "TestClass", 0); 23 class_addMethod(newClass, @selector(testMetaClass), (IMP)TestMetaClass, "v@:"); 24 objc_registerClassPair(newClass); 25 26 id instance = [[newClass alloc] initWithDomain:@"some domain" code:0 userInfo:nil]; 27 [instance performSelector:@selector(testMetaClass)]; 28 } 29 30 @endView Code
這個例子是在運行時創建了一個NSError的子類TestClass,然後為這個子類添加一個方法testMetaClass,這個方法的實現是TestMetaClass函數。
運行後,列印結果是
2014-10-20 22:57:07.352 mountain[1303:41490] This objcet is 0x7a6e22b0 2014-10-20 22:57:07.353 mountain[1303:41490] Class is TestStringClass, super class is NSError 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 0 times gives 0x7a6e21b0 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 1 times gives 0x0 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 2 times gives 0x0 2014-10-20 22:57:07.353 mountain[1303:41490] Following the isa pointer 3 times gives 0x0 2014-10-20 22:57:07.353 mountain[1303:41490] NSObject's class is 0xe10000
2014-10-20 22:57:07.354 mountain[1303:41490] NSObject's meta class is 0x0
我們在for迴圈中,我們通過objc_getClass來獲取對象的isa,並將其列印出來,依此一直回溯到NSObject的meta-class。分析列印結果,可以看到最後指針指向的地址是0x0,即NSObject的meta-class的類地址。
這裡需要註意的是:我們在一個類對象調用class方法是無法獲取meta-class,它只是返回類而已。
runtime 常見作用
-
動態交換兩個方法的實現
-
動態添加屬性
-
實現字典轉模型的自動轉換
-
發送消息
-
動態添加方法 (面試用到)
-
攔截並替換方法
-
實現 NSCoding 的自動歸檔和解檔
動態添加方法
應用場景:如果一個類方法非常多,載入類到記憶體的時候也比較耗費資源,需要給每個方法生成映射表,可以使用動態給某個類,添加方法解決。
註解:OC 中我們很習慣的會用懶載入,當用到的時候才去載入它,但是實際上只要一個類實現了某個方法,就會被載入進記憶體。當我們不想載入這麼多方法的時候,就會使用到 runtime
動態的添加方法。
實現NSCoding的自動歸檔和解檔
如果你實現過自定義模型數據持久化的過程,那麼你也肯定明白,如果一個模型有許多個屬性,那麼我們需要對每個屬性都實現一遍encodeObject
和 decodeObjectForKey
方法,如果這樣的模型又有很多個,這還真的是一個十分麻煩的事情。下麵來看看簡單的實現方式。
runtime 消息機制
我們寫 OC 代碼,它在運行的時候也是轉換成了 runtime
方式運行的。任何方法調用本質:就是發送一個消息(用 runtime
發送消息,OC 底層實現通過 runtime
實現)。
消息機制原理:對象根據方法編號SEL去映射表查找對應的方法實現。
每一個 OC 的方法,底層必然有一個與之對應的 runtime
方法。
示例代碼:OC 方法-->runtime 方法
說明: eat(無參) 和 run(有參) 是 Person模型類中的私有方法「可以幫我調用私有方法」;

1 // Person *p = [Person alloc]; 2 // 底層的實際寫法 3 Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc")); 4 5 // p = [p init]; 6 p = objc_msgSend(p, sel_registerName("init")); 7 8 // 調用對象方法(本質:讓對象發送消息) 9 //[p eat]; 10 11 // 本質:讓類對象發送消息 12 objc_msgSend(p, @selector(eat)); 13 objc_msgSend([Person class], @selector(run:),20); 14 15 //--------------------------- <#我是分割線#> ------------------------------// 16 // 也許下麵這種好理解一點 17 18 // id objc = [NSObject alloc]; 19 id objc = objc_msgSend([NSObject class], @selector(alloc)); 20 21 // objc = [objc init]; 22 objc = objc_msgSend(objc, @selector(init));View Code
runtime 方法調用流程「消息機制」
面試:消息機制方法調用流程
- 怎麼去調用
eat
方法,對象方法:(保存到類對象的方法列表) ,類方法:(保存到元類(Meta Class
)中方法列表)。- 1.OC 在向一個對象發送消息時,
runtime
庫會根據對象的isa
指針找到該對象對應的類或其父類中查找方法。。 - 2.註冊方法編號(這裡用方法編號的好處,可以快速查找)。
- 3.根據方法編號去查找對應方法。
- 4.找到只是最終函數實現地址,根據地址去方法區調用對應函數。
- 1.OC 在向一個對象發送消息時,
- 補充:一個
objc
對象的isa
的指針指向什麼?有什麼作用?- 每一個對象內部都有一個isa指針,這個指針是指向它的真實類型,根據這個指針就能知道將來調用哪個類的方法。
runtime 下Class的各項操作
-
獲取屬性列表
1 objc_property_t *propertyList = class_copyPropertyList([self class], &count); 2 for (unsigned int i=0; i<count; i++) { 3 const char *propertyName = property_getName(propertyList[i]); 4 NSLog(@"property---->%@", [NSString stringWithUTF8String:propertyName]); 5 }
-
獲取方法列表
1 Method *methodList = class_copyMethodList([self class], &count); 2 for (unsigned int i; i<count; i++) { 3 Method method = methodList[i]; 4 NSLog(@"method---->%@", NSStringFromSelector(method_getName(method))); 5 }
-
獲取成員變數列表
-
1 Ivar *ivarList = class_copyIvarList([self class], &count); 2 for (unsigned int i; i<count; i++) { 3 Ivar myIvar = ivarList[i]; 4 const char *ivarName = ivar_getName(myIvar); 5 NSLog(@"Ivar---->%@", [NSString stringWithUTF8String:ivarName]); 6 }
-
獲取協議列表
-
1 __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count); 2 for (unsigned int i; i<count; i++) { 3 Protocol *myProtocal = protocolList[i]; 4 const char *protocolName = protocol_getName(myProtocal); 5 NSLog(@"protocol---->%@", [NSString stringWithUTF8String:protocolName]); 6 }
現在有一個Person類,和person創建的xiaoming對象,有test1和test2兩個方法
-
獲得類方法
1 Class PersonClass = object_getClass([Person class]); 2 SEL oriSEL = @selector(test1); 3 Method oriMethod = _class_getMethod(xiaomingClass, oriSEL);
-
獲得實例方法
-
1 Class PersonClass = object_getClass([xiaoming class]); 2 SEL oriSEL = @selector(test2); 3 Method cusMethod = class_getInstanceMethod(xiaomingClass, oriSEL);
-
添加方法
-
BOOL addSucc = class_addMethod(xiaomingClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
-
替換原方法實現
-
class_replaceMethod(toolClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
-
交換兩個方法的實現
method_exchangeImplementations(oriMethod, cusMethod);
runtime 幾個參數概念
1、objc_msgSend
這是個最基本的用於發送消息的函數。其實編譯器會根據情況在objc_msgSend
, objc_msgSend_stret
,,objc_msgSendSuper
, 或 objc_msgSendSuper_stret
四個方法中選擇一個來調用。如果消息是傳遞給超類,那麼會調用名字帶有 Super
的函數;如果消息返回值是數據結構而不是簡單值時,那麼會調用名字帶有stret
的函數。
2、SELobjc_msgSend
函數第二個參數類型為SEL
,它是selector
在Objc中的表示類型(Swift中是Selector類)。selector
是方法選擇器,可以理解為區分方法的 ID
,而這個 ID
的數據結構是SEL
:typedef struct objc_selector *SEL;
其實它就是個映射到方法的C字元串,你可以用 Objc 編譯器命令@selector()``或者 Runtime
系統的sel_registerName
函數來獲得一個SEL
類型的方法選擇器。
3、id
objc_msgSend
第一個參數類型為id
,大家對它都不陌生,它是一個指向類實例的指針:typedef struct objc_object *id;
那objc_object
又是啥呢:struct objc_object { Class isa; };
objc_object
結構體包含一個isa
指針,根據isa
指針就可以順藤摸瓜找到對象所屬的類。
4、runtime.h里Class的定義
1 struct objc_class { 2 Class isa OBJC_ISA_AVAILABILITY;//每個Class都有一個isa指針 3 4 #if !__OBJC2__ 5 Class super_class OBJC2_UNAVAILABLE;//父類 6 const char *name OBJC2_UNAVAILABLE;//類名 7 long version OBJC2_UNAVAILABLE;//類版本 8 long info OBJC2_UNAVAILABLE;//!*!供運行期使用的一些位標識。如:CLS_CLASS (0x1L)表示該類為普通class; CLS_META(0x2L)表示該類為metaclass等(runtime.h中有詳細列出) 9 long instance_size OBJC2_UNAVAILABLE;//實例大小 10 struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;//存儲每個實例變數的記憶體地址 11 struct objc_method_list **methodLists OBJC2_UNAVAILABLE;//!*!根據info的信息確定是類還是實例,運行什麼函數方法等 12 struct objc_cache *cache OBJC2_UNAVAILABLE;//緩存 13 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;//協議 14 #endif 15 16 } OBJC2_UNAVAILABLE; 17 18
可以看到運行時一個類還關聯了它的超類指針,類名,成員變數,方法,緩存,還有附屬的協議。
在objc_class
結構體中:`ivars是
objc_ivar_list指針;
methodLists是指向
objc_method_list指針的指針。也就是說可以動態修改
*methodLists的值來添加成員方法,這也是
Category`實現的原理。
什麼是 method swizzling(俗稱黑魔法)
-
簡單說就是進行方法交換
-
在
Objective-C
中調用一個方法,其實是向一個對象發送消息,查找消息的唯一依據是selector
的名字。利用Objective-C
的動態特性,可以實現在運行時偷換selector
對應的方法實現,達到給方法掛鉤的目的 -
每個類都有一個方法列表,存放著方法的名字和方法實現的映射關係,
selector
的本質其實就是方法名,IMP
有點類似函數指針,指向具體的Method
實現,通過selector
就可以找到對應的IMP
。

selector --> 對應的IMP
- 交換方法的幾種實現方式
- 利用
method_exchangeImplementations
交換兩個方法的實現 - 利用
class_replaceMethod
替換方法的實現 - 利用
method_setImplementation
來直接設置某個方法的IMP
。
交換方法
- 利用
類型編碼(Type Encoding)
作為對Runtime的補充,編譯器將每個方法的返回值和參數類型編碼為一個字元串,並將其與方法的selector關聯在一起。這種編碼方案在其它情況下也是非常有用的,因此我們可以使用@encode編譯器指令來獲取它。當給定一個類型時,@encode返回這個類型的字元串編碼。這些類型可以是諸如int、指針這樣的基本類型,也可以是結構體、類等類型。事實上,任何可以作為sizeof()操作參數的類型都可以用於@encode()。
在Objective-C Runtime Programming Guide中的Type Encoding一節中,列出了Objective-C中所有的類型編碼。需要註意的是這些類型很多是與我們用於存檔和分發的編碼類型是相同的。但有一些不能在存檔時使用。
註:Objective-C不支持long double類型。@encode(long double)返回d,與double是一樣的。
一個數組的類型編碼位於方括弧中;其中包含數組元素的個數及元素類型。如以下示例:
1 float a[] = {1.0, 2.0, 3.0}; 2 NSLog(@"array encoding type: %s", @encode(typeof(a)));
輸出是:
2014-10-28 11:44:54.731 RuntimeTest[942:50791] array encoding type: [3f]
其它類型可參考Type Encoding,在此不細說。
另外,還有些編碼類型,@encode雖然不會直接返回它們,但它們可以作為協議中聲明的方法的類型限定符。可以參考Type Encoding。
對於屬性而言,還會有一些特殊的類型編碼,以表明屬性是只讀、拷貝、retain等等,詳情可以參考Property Type String。
方法和消息
SEL又叫選擇器,是表示一個方法的selector的指針,其定義如下:
typedef struct objc_selector *SEL;
objc_selector結構體的詳細定義沒有在頭文件中找到。方法的selector用於表示運行時方 法的名字。Objective-C在編譯時,會依據每一個方法的名字、參數序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL。如下 代碼所示:
SEL sel1 = @selector(method1);
NSLog(@"sel : %p", sel1);
上面的輸出為:
2014-10-30 18:40:07.518 RuntimeTest[52734:466626] sel : 0x100002d72
兩個類之間,不管它們是父類與子類的關係,還是之間沒有這種關係,只要方法名相同,那麼方法的SEL就是一樣的。每一個方法都對應著一個SEL。所以在 Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數類型不同也不行。相同的方法只能對應一個SEL。這也就導致 Objective-C在處理相同方法名且參數個數相同但類型不同的方法方面的能力很差。如在某個類中定義以下兩個方法:
- (void)setWidth:(int)width;
- (void)setWidth:(double)width;
當然,不同的類可以擁有相同的selector,這個沒有問題。不同類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。
工程中的所有的SEL組成一個Set集合,Set的特點就是唯一,因此SEL是唯一的。因此,如果我們想到這個方法集合中查找某個方法時,只需要去 找到這個方法對應的SEL就行了,SEL實際上就是根據方法名hash化了的一個字元串,而對於字元串的比較僅僅需要比較他們的地址就可以了,可以說速度 上無語倫比!!但是,有一個問題,就是數量增多會增大hash衝突而導致的性能下降(或是沒有衝突,因為也可能用的是perfect hash)。但是不管使用什麼樣的方法加速,如果能夠將總量減少(多個方法可能對應同一個SEL),那將是最犀利的方法。那麼,我們就不難理解,為什麼 SEL僅僅是函數名了。
本質上,SEL只是一個指向方法的指針(準確的說,只是一個根據方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。這個查找過程我們將在下麵討論。
我們可以在運行時添加新的selector,也可以在運行時獲取已存在的selector,我們可以通過下麵三種方法來獲取SEL:
-
sel_registerName函數
-
Objective-C編譯器提供的@selector()
-
NSSelectorFromString()方法