1. 程式人生 > >iOS 運行時詳解

iOS 運行時詳解

序列 get not oci protocol caption 聲明 實現 att

註:本篇文章轉自:http://www.jianshu.com/p/adf0d566c887

一、運行時簡介

Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了運行時來處理。
對於Objective-C來說,這個運行時系統就像一個操作系統一樣:它讓所有的工作可以正常的運行。Runtime基本上是用C和匯編寫的,這個庫使得C語言有了面向對象的能力。
在Runtime中,對象可以用C語言中的結構體表示,而方法可以用C函數來實現,另外再加上了一些額外的特性。這些結構體和函數被runtime函數封裝後,讓OC的面向對象編程變為可能。
找出方法的最終執行代碼:當程序執行[object doSomething]時,會向消息接收者(object)發送一條消息(doSomething),runtime會根據消息接收者是否能響應該消息而做出不同的反應。

二、類與對象基礎數據結構

Objective-C類是由Class類型來表示的,它實際上是一個指
向objc_class結構體的指針。


typedef struct object_class *Class

它的定義如下:
查看objc/runtime.h中objc_class結構體的定義如下:

struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE;  // 父類
     const char *name                         OBJC2_UNAVAILABLE;  // 類名
     long version                             OBJC2_UNAVAILABLE;  // 類的版本信息,默認為0
     long info                                OBJC2_UNAVAILABLE;  // 類信息,供運行期使用的一些位標識
     long instance_size                       OBJC2_UNAVAILABLE;  // 該類的實例變量大小
     struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 該類的成員變量鏈表
     struct objc_method_list *methodLists     OBJC2_UNAVAILABLE;  // 方法定義的鏈表
     struct objc_cache *cache                 OBJC2_UNAVAILABLE;  // 方法緩存
     struct objc_protocol_list *protocols     OBJC2_UNAVAILABLE;  // 協議鏈表
#endif
}OBJC2_UNAVAILABLE;

說明其執行過程:
NSArray *array = [[NSArray alloc] init];

objc_object

objc_object是表示一個類的實例的結構體
它的定義如下(objc/objc.h):

struct objc_object{
     Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;

可以看到,這個結構體只有一個字體,即指向其類的isa指針。這
樣,當我們向一個Objective-C對象發送消息時,運行時庫會根據
實例對象的isa指針找到這個實例對象所屬的類。Runtime庫會在類
的方法列表及父類的方法列表中去尋找與消息對應的selector指向
的方法,找到後即運行這個方法。

元類(Meta Class)

meta-class是一個類對象的類。
在上面我們提到,所有的類自身也是一個對象,我們可以向這個對象發送消息(即調用類方法)。
既然是對象,那麽它也是一個objc_object指針,它包含一個指向其類的一個isa指針。那麽,這個isa指針指向什麽呢?

為了調用類方法,這個類的isa指針必須指向一個包含這些類方法的一個objc_class結構體。這就引出了meta-class的概念,meta-class中存儲著一個類的所有類方法。

所以,調用類方法的這個類對象的isa指針指向的就是meta-class
當我們向一個對象發送消息時,runtime會在這個對象所屬的這個類的方法列表中查找方法;而向一個類發送消息時,會在這個類的meta-class的方法列表中查找。

再深入一下,meta-class也是一個類,也可以向它發送一個消息,那麽它的isa又是指向什麽呢?為了不讓這種結構無限延伸下去,Objective-C的設計者讓所有的meta-class的isa指向基類的meta-class,以此作為它們的所屬類。

即,任何NSObject繼承體系下的meta-class都使用NSObject的meta-class作為自己的所屬類,而基類的meta-class的isa指針是指向它自己。

通過上面的描述,再加上對objc_class結構體中super_class指針的分析,我們就可以描繪出類及相應meta-class類的一個繼承體系了,如下代碼

技術分享
              Snip20160501_1.png

Category

Category是表示一個指向分類的結構體的指針,其定義如下:

typedef struct objc_category *Category
struct objc_category{
     char *category_name                         OBJC2_UNAVAILABLE; // 分類名
     char *class_name                            OBJC2_UNAVAILABLE;  // 分類所屬的類名
     struct objc_method_list *instance_methods   OBJC2_UNAVAILABLE;  // 實例方法列表
     struct objc_method_list *class_methods      OBJC2_UNAVAILABLE; // 類方法列表
     struct objc_protocol_list *protocols        OBJC2_UNAVAILABLE; // 分類所實現的協議列表
}

這個結構體主要包含了分類定義的實例方法與類方法,其中instance_methods列表是objc_class中方法列表的一個子集,而class_methods列表是元類方法列表的一個子集。
可發現,類別中沒有ivar成員變量指針,也就意味著:類別中不能夠添加實例變量和屬性

struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;  // 該類的成員變量鏈表

三、runtime關聯對象

我們先看看關聯API,只有這三個API,使用也是非常簡單的:

1.設置關聯值

參數說明:
object:與誰關聯,通常是傳self
key:唯一鍵,在獲取值時通過該鍵獲取,通常是使用static
const void *來聲明
value:關聯所設置的值
policy:內存管理策略,比如使用copy

void objc_setAssociatedObject(id object, const void *key, id value, objc _AssociationPolicy policy)

2.獲取關聯值

參數說明:
object:與誰關聯,通常是傳self,在設置關聯時所指定的與哪個對象關聯的那個對象
key:唯一鍵,在設置關聯時所指定的鍵

id objc_getAssociatedObject(id object, const void *key)

3.取消關聯

void objc_removeAssociatedObjects(id object)

關聯策略

使用場景:
可以在類別中添加屬性

typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0,             // 表示弱引用關聯,通常是基本數據類型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,   // 表示強引用關聯對象,是線程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,     // 表示關聯對象copy,是線程安全的
OBJC_ASSOCIATION_RETAIN = 01401,         // 表示強引用關聯對象,不是線程安全的
OBJC_ASSOCIATION_COPY = 01403            // 表示關聯對象copy,不是線程安全的
};

四、方法與消息

1、SEL

SEL又叫選擇器,是表示一個方法的selector的指針,其定義如下:

typedef struct objc_selector *SEL;

方法的selector用於表示運行時方法的名字。Objective-C在編譯時,會依據每一個方法的名字、參數序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL。
兩個類之間,只要方法名相同,那麽方法的SEL就是一樣的,每一個方法都對應著一個SEL。所以在Objective-C同一個類(及類的繼承體系)中,不能存在2個同名的方法,即使參數類型不同也不行
如在某一個類中定義以下兩個方法: 錯誤

- (void)setWidth:(int)width;
- (void)setWidth:(double)width;

當然,不同的類可以擁有相同的selector,這個沒有問題。不同類的實例對象執行相同的selector時,會在各自的方法列表中去根據selector去尋找自己對應的IMP。
工程中的所有的SEL組成一個Set集合,如果我們想到這個方法集合中查找某個方法時,只需要去找到這個方法對應的SEL就行了,SEL實際上就是根據方法名hash化了的一個字符串,而對於字符串的比較僅僅需要比較他們的地址就可以了,可以說速度上無語倫比!
本質上,SEL只是一個指向方法的指針(準確的說,只是一個根據方法名hash化了的KEY值,能唯一代表一個方法),它的存在只是為了加快方法的查詢速度。
通過下面三種方法可以獲取SEL:
a、sel_registerName函數
[email protected]()
c、NSSelectorFromString()方法

2、IMP

IMP實際上是一個函數指針,指向方法實現的地址。
其定義如下:

id (*IMP)(id, SEL,...)

第一個參數:是指向self的指針(如果是實例方法,則是類實例的內存地址;如果是類方法,則是指向元類的指針)
第二個參數:是方法選擇器(selector)
接下來的參數:方法的參數列表。

前面介紹過的SEL就是為了查找方法的最終實現IMP的。由於每個方法對應唯一的SEL,因此我們可以通過SEL方便快速準確地獲得它所對應的IMP,查找過程將在下面討論。取得IMP後,我們就獲得了執行這個方法代碼的入口點,此時,我們就可以像調用普通的C語言函數一樣來使用這個函數指針了。

3、Method

Method用於表示類定義中的方法,則定義如下:

typedef struct objc_method *Method
struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法實現
}

我們可以看到該結構體中包含一個SEL和IMP,實際上相當於在SEL和IMP之間作了一個映射。有了SEL,我們便可以找到對應的IMP,從而調用方法的實現代碼。

4、方法調用流程

技術分享
                  Snip20160501_2.png


在Objective-C中,消息直到運行時才綁定到方法實現上。編譯器會將消息表達式[receiver message]轉化為一個消息函數的調用,即objc_msgSend。這個函數將消息接收者和方法名作為其基礎參數,如以下所示

objc_msgSend(receiver, selector)

如果消息中還有其它參數,則該方法的形式如下所示:

objc_msgSend(receiver, selector, arg1, arg2,...)

這個函數完成了動態綁定的所有事情:

a、首先它找到selector對應的方法實現。因為同一個方法可
能在不同的類中有不同的實現,所以我們需要依賴於接收者的類
來找到的確切的實現。
b、調用方法實現,並將接收者對象及方法的所有參數傳給它。
c、最後,它將實現返回的值作為它自己的返回值。

消息的關鍵在於我們前面章節討論過的結構體objc_class,這個結構體有兩個字段是我們在分發消息的關註的:
-> 指向父類的指針
-> 個類的方法分發表,即methodLists。
當我們創建一個新對象時,先為其分配內存,並初始化其成員變量。其中isa指針也會被初始化,讓對象可以訪問類及類的繼承體系。

下圖演示了這樣一個消息的基本框架:
當消息發送給一個對象時首先從運行時系統緩存使用過的方法中尋找。
如果找到,執行該方法,如未找到繼續執行下面的步驟

objc_msgSend通過對象的isa指針獲取到類的結構體,然後在方法分發表裏面查找方法的selector。
如果沒有找到selector,objc_msgSend結構體中的指向父類的指針找到其父類,並在父類的分發表裏面查找方法的selector。
依此,會一直沿著類的繼承體系到達NSObject類。一旦定位到selector,函數會就獲取到了實現的入口點,並傳入相應的參數來執行方法的具體實現,並將該方法添加進入緩存中如果最後沒有定位到selector,則會走消息轉發流程,這個我們在後面討論。

5、消息轉發

當一個對象能接收一個消息時,就會走正常的方法調用流程。但如果一個對象無法接收指定消息時,又會發生什麽事呢?默認情況下,如果是以[object message]的方式調用方法,如果object無法響應message消息時,編譯器會報錯。但如果是以perform…的形式來調用,則需要等到運行時才能確定object是否能接收message消息。如果不能,則程序崩潰。

技術分享
Snip20160501_3.png


通常,當我們不能確定一個對象是否能接收某個消息時,會先調用respondsToSelector:來判斷一下。如下代碼所示:

if([self respondsToSelector:@selector(method)]){
      [self performSelector:@selector(method)];
}

不過,我們這邊想討論下不使用respondsToSelector:判斷的情況。這才是我們這一節的重點。

當一個對象無法接收某一消息時,就會啟動所謂“消息轉發(message forwarding)”機制,通過這一機制,我們可以告訴對象如何處理未知的消息。默認情況下,對象接收到未知的消息,會導致程序崩潰,通過控制臺,我們可以看到以下異常信息:

這段異常信息實際上是由NSObject的“doesNotRecognizeSelector”方法拋出的。不過,我們可以采取一些措施,讓我們的程序執行特定的邏輯,而避免程序的崩潰。

消息轉發機制基本上分為三個步驟:

1>、動態方法解析
2>、備用接收者
3>、完整轉發
消息的轉發流程圖:

技術分享
Snip20160501_5.png

動態方法解析

對象在接收到未知的消息時,首先會調用所屬類的類方法
+resolveInstanceMethod:(實例方法)或者
+resolveClassMethod:(類方法)。

在這個方法中,我們有機會為該未知消息新增一個“處理方法”,通過運行時class_addMethod函數動態添加到類裏面就可以了。

[email protected]

備用接收者

- (id)forwardingTargetForSelector:(SEL)aSelector

如果在上一步無法處理消息,則Runtime會繼續調以下方法:
如果一個對象實現了這個方法,並返回一個非nil的結果,則這個對象會作為消息的新接收者,且消息會被分發到這個對象。當然這個對象不能是self自身,否則就是出現無限循環。當然,如果我們沒有指定相應的對象來處理aSelector,則應該調用父類的實現來返回結果。

這一步合適於我們只想將消息轉發到另一個能處理該消息的對象上。但這一步無法對消息進行處理,如操作消息的參數和返回值。

完整消息轉發

如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉發機制了。
我們首先要通過,指定方法簽名,若返回nil,則表示不處理。
如下代碼:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
   if ([NSStringFromSelector(aSelector) isEqualToString:@"testInstanceMethod"]){
     return [NSMethodSignature signatureWithObjcTypes:"v@:"];
  }  
return [super methodSignatureForSelector: aSelector];
}

若返回方法簽名,則會進入下一步調用以下方法,對象會創建一個表示消息的NSInvocation對象,把與尚未處理的消息有關的全部細節都封裝在anInvocation中,包括selector,目標(target)和參數。
我們可以在forwardInvocation方法中選擇將消息轉發給其它對象。我們可以通過anInvocation對象做很多處理,比如修改實現方法,修改響應對象等.
如下所示:

- (void)forwardInvovation:(NSInvocation)anInvocation
{
    [anInvocation invokeWithTarget:_helper];
    [anInvocation setSelector:@selector(run)];
    [anInvocation invokeWithTarget:self];
}

五、Method Swizzling

1.Swizzling原理

在Objective-C中調用一個方法,其實是向一個對象發送消息,而查找消息的唯一依據是selector的名字。所以,我們可以利用Objective-C的runtime機制,實現在運行時交換selector對應的方法實現以達到我們的目的。

每個類都有一個方法列表,存放著selector的名字和方法實現的映射關系。IMP有點類似函數指針,指向具體的Method實現

我們先看看SEL與IMP之間的關系圖:

技術分享
Snip20160501_6.png


從上圖可以看出來,每一個SEL與一個IMP一一對應,正常情況下通過SEL可以查找到對應消息的IMP實現。

但是,現在我們要做的就是把鏈接線解開,然後連到我們自定義的函數的IMP上。當然,交換了兩個SEL的IMP,還是可以再次交換回來了。交換後變成這樣的,如下圖

技術分享
                Snip20160501_7.png


從圖中可以看出,我們通過swizzling特性,將selectorC的方法實現IMPc與selectorN的方法實現IMPn交換了,當我們調用selectorC,也就是給對象發送selectorC消息時,所查找到的對應的方法實現就是IMPn而不是IMPc了。

iOS 運行時詳解