Runtime初探

簡介

Runtime 又叫運行時,是一套底層的 C 語言 API,其爲 iOS 內部的核心之一,我們平時編寫的 OC 代碼,底層都是基於它來實現的。比如:

[receiver message];
// 底層運行時會被編譯器轉化爲:
objc_msgSend(receiver, selector);
我們需要了解的是 Objective-C 是一門動態語言,什麼是動態語言?動態語言就是在運行時來執行靜態語言的編譯鏈接的工作。這就要求除了編譯器之外還要有一種運行時系統來執行編譯等功能。OC中這個系統就是runtime。它會將一些工作放在代碼運行時才處理而並非編譯時。也就是說,有很多類和成員變量在我們編譯的時是不知道的,而在運行時,最終都是轉成了runtime的C語言代碼, runtime算是OC的幕後工作者。

Runtime 基本是用 C 和彙編寫的,由此可見蘋果爲了動態系統的高效而做出的努力。蘋果和 GNU 各自維護一個開源的 Runtime 版本,這兩個版本之間都在努力保持一致。

runtime可以用來做什麼

1.在程序運行過程中,動態創建一個類(比如KVO的底層實現)
2.在程序運行過程中,動態地爲某個類添加屬性/方法。可以用於封裝框架(想怎麼改就怎麼改) 這就是我們runtime機制的主要運用方向
3.遍歷一個類中所有的成員變量(屬性)/所有方法。(比如字典–>模型:利用runtime遍歷模型對象的所有屬性, 根據屬性名從字典中取出對應的值, 設置到模型的屬性上;還有歸檔和接檔,利用runtime遍歷模型對象的所有屬性)

runtime相關頭文件和函數

利用頭文件,我們可以查看到runtime中的各個方法!

 objc_msgSend : 給對象發送消息
 class_copyMethodList : 遍歷某個類所有的方法
 class_copyIvarList : 遍歷某個類所有的成員變量

必備常識

Ivar : 成員變量
Method : 成員方法

一些 Runtime 的術語的數據結構

id

id 是一個參數類型,它是指向某個類的實例的指針。定義如下:

typedef struct objc_object *id;
struct objc_object { Class isa; };
以上定義,看到 objc_object 結構體包含一個 isa 指針,根據 isa 指針就可以找到對象所屬的類。

Class

typedef struct objc_class *Class;
Class 其實是指向 objc_class 結構體的指針。objc_class 的數據結構如下:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    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;

從 objc_class 可以看到,一個運行時類中關聯了它的父類指針、類名、成員變量、方法、緩存以及附屬的協議。

IMP

IMP在objc.h中的定義是:

typedef id (*IMP)(id, SEL, …);
它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之後,最終它會執行的那段代碼,就是由這個函數指針指定的。而 IMP 這個函數指針就指向了這個方法的實現。

如果得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在後面 Cache 中會提到。

你會發現 IMP 指向的方法與 objc_msgSend 函數類型相同,參數都包含 id 和 SEL 類型。每個方法名都對應一個 SEL 類型的方法選擇器,而每個實例對象中的 SEL 對應的方法實現肯定是唯一的,通過一組 id和 SEL 參數就能確定唯一的方法實現地址。

而一個確定的方法也只有唯一的一組 id 和 SEL 參數。

Cache

Cache 定義如下:

typedef struct objc_cache *Cache

struct objc_cache {
    unsigned int mask /* total = mask + 1 */                 OBJC2_UNAVAILABLE;
    unsigned int occupied                                    OBJC2_UNAVAILABLE;
    Method buckets[1]                                        OBJC2_UNAVAILABLE;
};

Cache 爲方法調用的性能進行優化,每當實例對象接收到一個消息時,它不會直接在 isa 指針指向的類的方法列表中遍歷查找能夠響應的方法,因爲每次都要查找效率太低了,而是優先在 Cache 中查找。

Runtime 系統會把被調用的方法存到 Cache 中,如果一個方法被調用,那麼它有可能今後還會被調用,下次查找的時候就會效率更高。就像計算機組成原理中 CPU 繞過主存先訪問 Cache 一樣。

Property

typedef struct objc_property *Property;
typedef struct objc_property *objc_property_t;//這個更常用

可以通過class_copyPropertyList 和 protocol_copyPropertyList 方法獲取類和協議中的屬性:

objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

注意:
返回的是屬性列表,列表中每個元素都是一個 objc_property_t 指針

#import <Foundation/Foundation.h>

@interface Person : NSObject

/** 姓名 */
@property (strong, nonatomic) NSString *name;

/** age */
@property (assign, nonatomic) int age;

/** weight */
@property (assign, nonatomic) double weight;

@end
以上是一個 Person 類,有3個屬性。讓我們用上述方法獲取類的運行時屬性。

    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, attributes);
    }
打印結果如下:

2014-11-10 11:27:28.473 test[2321:451525] 3
2014-11-10 11:27:28.473 test[2321:451525] name--------T@"NSString",&,N,V_name
2014-11-10 11:27:28.473 test[2321:451525] age--------Ti,N,V_age
2014-11-10 11:27:28.474 test[2321:451525] weight--------Td,N,V_weight
property_getName 用來查找屬性的名稱,返回 c 字符串。property_getAttributes 函數挖掘屬性的真實名稱和 @encode 類型,返回 c 字符串。

objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
class_getProperty 和 protocol_getProperty 通過給出屬性名在類和協議中獲得屬性的引用。

消息

消息直到運行時纔會與方法實現進行綁定。

這裏要清楚一點,objc_msgSend 方法看清來好像返回了數據,其實objc_msgSend 從不返回數據,而是你的方法在運行時實現被調用後纔會返回數據。

1.首先檢測這個 selector 是不是要忽略。比如 Mac OS X 開發,有了垃圾回收就不理會 retain,release 這些函數。
2.檢測這個 selector 的 target 是不是 nil,Objc 允許我們對一個 nil 對象執行任何方法不會 Crash,因爲運行時會被忽略掉。
3.如果上面兩步都通過了,那麼就開始查找這個類的實現 IMP,先從 cache 裏查找,如果找到了就運行對應的函數去執行相應的代碼。
4.如果 cache 找不到就找類的方法列表中是否有對應的方法。
5.如果類的方法列表中找不到就到父類的方法列表中查找,一直找到 NSObject 類爲止。
6.如果還找不到,就要開始進入動態方法解析了,後面會提到。
在消息的傳遞中,編譯器會根據情況在 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 結構體:

struct objc_super { id receiver; Class class; };
這個結構體指明瞭消息應該被傳遞給特定的父類。 receiver 仍然是 self 本身,當我們想通過 [super class] 獲取父類時,編譯器其實是將指向 self 的 id 指針和 class 的 SEL 傳遞給了 objc_msgSendSuper 函數。只有在 NSObject 類中才能找到 class 方法,然後 class 方法底層被轉換爲 object_getClass(), 接着底層編譯器將代碼轉換爲 objc_msgSend(objc_super->receiver, @selector(class)),傳入的第一個參數是指向 self 的 id 指針,與調用 [self class] 相同,所以我們得到的永遠都是 self 的類型。因此你會發現:

// 這句話並不能獲取父類的類型,只能獲取當前類的類型名
NSLog(@”%@”, NSStringFromClass([super class]));
這也是 Runtime 系統的作用,self 實在方法運行時被動態傳入的。

動態方法解析

你可以動態提供一個方法實現。如果我們使用關鍵字 @dynamic 在類的實現文件中修飾一個屬性,表明我們會爲這個屬性動態提供存取方法,編譯器不會再默認爲我們生成這個屬性的 setter 和 getter 方法了,需要我們自己提供。

@dynamic propertyName;
這時,我們可以通過分別重載 resolveInstanceMethod: 和 resolveClassMethod: 方法添加實例方法實現和類方法實現。

當 Runtime 系統在 Cache 和類的方法列表(包括父類)中找不到要執行的方法時,Runtime 會調用 resolveInstanceMethod: 或 resolveClassMethod: 來給我們一次動態添加方法實現的機會。我們需要用 class_addMethod 函數完成向特定類添加特定方法實現的操作:

void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子爲 resolveThisMethodDynamically 方法添加了實現內容,就是 dynamicMethodIMP 方法中的代碼。其中 “v@:” 表示返回值和參數
這個符號表示的含義見:Type Encoding

注意:
動態方法解析會在消息轉發機制侵入前執行,動態方法解析器將會首先給予提供該方法選擇器對應的 IMP 的機會。如果你想讓該方法選擇器被傳送到轉發機制,就讓 resolveInstanceMethod: 方法返回 NO。

消息轉發

image

重定向

消息轉發機制執行前,Runtime 系統允許我們替換消息的接收者爲其他對象。通過 - (id)forwardingTargetForSelector:(SEL)aSelector 方法。

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:)){
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

如果此方法返回 nil 或者 self,則會計入消息轉發機制(forwardInvocation:),否則將向返回的對象重新發送消息。

轉發

當動態方法解析不做處理返回 NO 時,則會觸發消息轉發機制。這時 forwardInvocation: 方法會被執行,我們可以重寫這個方法來自定義我們的轉發邏輯:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}

唯一參數是個 NSInvocation 類型的對象,該對象封裝了原始的消息和消息的參數。我們可以實現 forwardInvocation: 方法來對不能處理的消息做一些處理。也可以將消息轉發給其他對象處理,而不拋出錯誤。

注意:參數 anInvocation 是從哪來的?
在 forwardInvocation: 消息發送前,Runtime 系統會向對象發送methodSignatureForSelector: 消息,並取到返回的方法簽名用於生成 NSInvocation 對象。所以重寫 forwardInvocation: 的同時也要重寫 methodSignatureForSelector: 方法,否則會拋異常。

當一個對象由於沒有相應的方法實現而無法相應某消息時,運行時系統將通過 forwardInvocation: 消息通知該對象。每個對象都繼承了 forwardInvocation: 方法。但是, NSObject 中的方法實現只是簡單的調用了 doesNotRecognizeSelector:。通過實現自己的 forwardInvocation: 方法,我們可以將消息轉發給其他對象。

forwardInvocation: 方法就是一個不能識別消息的分發中心,將這些不能識別的消息轉發給不同的接收對象,或者轉發給同一個對象,再或者將消息翻譯成另外的消息,亦或者簡單的“吃掉”某些消息,因此沒有響應也不會報錯。這一切都取決於方法的具體實現。

注意:
forwardInvocation:方法只有在消息接收對象中無法正常響應消息時纔會被調用。所以,如果我們嚮往一個對象將一個消息轉發給其他對象時,要確保這個對象不能有該消息的所對應的方法。否則,forwardInvocation:將不可能被調用。

轉發和多繼承

轉發和繼承相似,可用於爲 Objc 編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發出去,就好像它把另一個對象中的方法接過來或者“繼承”過來一樣。
image
這使得在不同繼承體系分支下的兩個類可以實現“繼承”對方的方法,在上圖中 Warrior 和 Diplomat 沒有繼承關係,但是 Warrior 將 negotiate 消息轉發給了 Diplomat 後,就好似 Diplomat 是 Warrior 的超類一樣。

消息轉發彌補了 Objc 不支持多繼承的性質,也避免了因爲多繼承導致單個類變得臃腫複雜。

轉發與繼承

雖然轉發可以實現繼承的功能,但是 NSObject 還是必須表面上很嚴謹,像 respondsToSelector: 和 isKindOfClass: 這類方法只會考慮繼承體系,不會考慮轉發鏈。

如果上圖中的 Warrior 對象被問到是否能響應 negotiate消息:

if ( [aWarrior respondsToSelector:@selector(negotiate)] )

回答當然是 NO, 儘管它能接受 negotiate 消息而不報錯,因爲它靠轉發消息給 Diplomat 類響應消息。

如果你就是想要讓別人以爲 Warrior 繼承到了 Diplomat 的 negotiate 方法,你得重新實現 respondsToSelector: 和 isKindOfClass: 來加入你的轉發算法:

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ( [super respondsToSelector:aSelector] )
        return YES;
    else {
        /* Here, test whether the aSelector message can     *
         * be forwarded to another object and whether that  *
         * object can respond to it. Return YES if it can.  */
    }
    return NO;
}

除了 respondsToSelector: 和 isKindOfClass: 之外,instancesRespondToSelector: 中也應該寫一份轉發算法。如果使用了協議,conformsToProtocol: 同樣也要加入到這一行列中。

如果一個對象想要轉發它接受的任何遠程消息,它得給出一個方法標籤來返回準確的方法描述 methodSignatureForSelector:,這個方法會最終響應被轉發的消息。從而生成一個確定的 NSInvocation 對象描述消息和消息參數。這個方法最終響應被轉發的消息。它需要像下面這樣實現:

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}

健壯的實例變量(Non Fragile ivars)

在 Runtime 的現行版本中,最大的特點就是健壯的實例變量了。當一個類被編譯時,實例變量的內存佈局就形成了,它表明訪問類的實例變量的位置。實例變量一次根據自己所佔空間而產生位移:
image
上圖左是 NSObject 類的實例變量佈局。右邊是我們寫的類的佈局。這樣子有一個很大的缺陷,就是缺乏拓展性。哪天蘋果更新了 NSObject 類的話,就會出現問題:
image
我們自定義的類的區域和父類的區域重疊了。只有蘋果將父類改爲以前的佈局才能拯救我們,但這樣導致它們不能再拓展它們的框架了,因爲成員變量佈局被固定住了。在脆弱的實例變量(Fragile ivar)環境下,需要我們重新編譯繼承自 Apple 的類來恢復兼容。如果是健壯的實例變量的話,編譯器生成的實例變量佈局跟以前一樣,但是當 Runtime 系統檢測到與父類有部分重疊時它會調整你新添加的實例變量的位移,那樣你再子類中新添加的成員變量就被保護起來了。

注意:
在健壯的實例變量下,不要使用 siof(SomeClass),而是用 class_getInstanceSize([SomeClass class]) 代替;也不要使用 offsetof(SomeClass, SomeIvar),而要使用 ivar_getOffset(class_getInstanceVariable([SomeClass class], “SomeIvar”)) 來代替。

總結

我們讓自己的類繼承自 NSObject 不僅僅是因爲基類有很多複雜的內存分配問題,更是因爲這使得我們可以享受到 Runtime 系統帶來的便利。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章