1. 程式人生 > >Runtime最全詳解

Runtime最全詳解

app 面向 default 才會 long con 方便 ast 一起

簡介

OC這門語言把很多事情從編譯和鏈接階段推遲到運行時處理。只要有可能,它就會采取動態運行時機制。這意味著這門語言不僅需要一個編譯器還需要一個運行時系統來執行這些編譯後的代碼。這個運行時系統相當於OC語言的操作系統,它使得這門語言運轉良好。

Runtime版本和平臺

Objective-C runtime 在不同的平臺上使用不同的版本。 Objective-C runtime,有2個版本:“modern” and “legacy”. modern 版本是在 Objective-C 2.0 介紹的,並且包含了很多新特性。 legacy 版本的使用接口是在 Objective-C 1 Runtime Reference介紹的。modern版本的使用接口 是在 Objective-C Runtime Reference介紹的。 Modern版本的Runtime System有一個顯著的特征:實例變量是“non-fragile”, legacy runtime中,父類的成員變量的布局發生改變時,子類需要重新編譯 modern runtime中, 父類的成員變量的布局發生改變時,子類不需要重新編譯 此外,還支持為聲明的屬性進行合成操作(即@property和@synthesis)。 iPhone 應用和 OS X v10.5 之後的 64-bit 使用 modern runtime.其他使用 legacy runtime。

與Runtime的交互

OC程序可以通過三種不同的方式和runtime系統交互: 1.通過OC源代碼 2.通過Foundation框架中NSObject的定義方法 3.通過直接調用runtime的方法

OC源代碼

大部分情況下,runtime系統在後臺自動工作,你編寫並且編譯後的代碼就會轉換成runtime語言。當你編譯的代碼包含OC類和對象的時候,這個編譯器會創建數據結構和方法來實現這門語言的動態特性。這個數據結構捕捉類的信息、申明在協議中的分類、方法選擇器和一些源代碼中的其他信息.runtime系統的關鍵就是消息傳遞,一般調用類或對象的方法就會促發它。

NSObject方法

大部分Cocoa對象是NSObject子類(NSProxy例外,它是個虛擬的超類),因此繼承了NSObject的方法。然而在一些情況下,NSObject只提供了一些方法的定義,並沒有實現它,比如description實例方法,需要返回一個描述對內容的字符串描述,主要用於debug調試。它的實現方法不知道這個類包含的信息內容,所以它返回的是類的名稱和地址,子類可以重寫這個方法獲得更多信息,比如NSArray類返回一系列它包含的對象的描述。一些NSObject方法向runtime系統查詢信息,例如: isKindOfClass: 和 isMemberOfClass:方法是判斷一個對象代表的類是否位於某個類的繼承體系中。 respondsToSelector:判斷一個對象是否響應了某個方法 conformsToProtocol:判斷一個對象是否遵循了特定協議的方法 methodForSelector:提供了一個方法實現的地址 像這些方法有助於一個對象更好的了解自身的信息。

Runtime方法

runtime是一個動態的開源庫,在它的/usr/include/objc頭文件裏定義了很多公開的方法和數據結構,許多這些方法可以用C語言來編寫這些OC的運行時代碼,其他一些方法就是使用NSObject定義的方法了。你不一定要在編寫OC時候使用runtime方法,但是使用少量的runtime方法可以使得代碼更有用。

消息傳遞

在OC中消息直到運行時才會實現,編譯器會把消息語句[receiver message]轉換成objc_msgSend(receiver, selector)這個方法將一個接收者和方法選擇器作為參數,如果方法帶參數就跟在後面傳遞objc_msgSend(receiver, selector, arg1, arg2, …)。這個方法將完成動態綁定的所有工作。 1.首先它會找到方法選擇器對應的方法實現,介於相同的方法名可以對應不同類的不同實現。找到這個正確的方法實現就取決於這個接收者的類了。 2.找到方法的實現後,就需要將這個方法需要的參數傳遞給這個接受對象了, 3.最後將這個函數實現的返參作為自己的返參。 這個消息發送的鏈條關鍵信息有兩點 (1)指向父類的指針 (2)一個類的方法列表,這個列表建立了方法定義和實現的映射關系。 當一個對象創建以後,系統就會給它分配內存和初始化。這個對象生成的第一個變量就是一個指向該類結構體的指針,稱為isa指針。可以通過它訪問這個對象的類以及它的所有繼承類。 當給一個對象發送一條消息時,這個objc_msgSend就會沿著這個isa指針找到它的類結構體,這樣就能找到這個類的方法列表。如果方法列表裏面找不到該方法,就會沿著它的父類指針指向的類來尋找它的父類方法列表,一直到NSObject基類。一旦找到這個方法,就會傳遞參數調用方法實現。這是在運行時選擇方法實現的方法,或者在面向對象編程的術語中,方法動態地綁定到消息。為了加快消息傳遞過程,當一個方法被調用時運行時系統會緩存方法的選擇器和地址。每個類都有一個單獨的緩存,它可以包含繼承方法的選擇器以及類中定義的方法。在搜索調度表之前,消息傳遞例程首先檢查接收對象類的緩存(關於一次使用的方法可能再次被使用的理論)。如果方法選擇器在緩存中,消息傳遞只比函數調用慢一點。一旦程序運行足夠長,以“預熱”它的緩存,它發送的幾乎所有消息都會找到一個緩存方法。當程序運行時,緩存動態增長以容納新消息。 當找到方法的實現時,它會調用這個方法的實現以及進行傳參,這裏有兩個隱藏的傳參,self代表消息接收者,_cmd代表方法選擇器。 然而,在有些情況下需要規避這種耗時的消息傳遞,可以通過調用NSObject的methodForSelector:方法來返回方法實現的地址,然後直接調用它的實現。這種情況一般用於在一段時間連續調用同一個方法。註意這個方法是cocoa的方法不是runtime的方法。

動態方法解析

在一些情況下需要動態的提供一個方法的實現,比如你使用了dynamic來申明一個屬性@dynamic propertyName;,告訴編譯器這個屬性相關的方法是動態生成的。你可以實現 resolveInstanceMethod: 和 resolveClassMethod:方法用於動態的提供類方法和實例方法的實現。 一個OC方法相當於一個C方法帶self和_cmd兩個參數。你可以通過class_addMethod給一個類添加方法。
void dynamicMethodIMP(id self, SEL _cmd) {
    // implementation ....
}
你可以動態把它添加到一個類作為一個方法(稱為resolvethismethoddynamically)使用resolveinstancemethod:像這樣:
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "[email protected]:");
          return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}
@end
轉發方法和動態方法解決方案在很大程度上是正交的。在轉發機制啟動之前,類有機會動態地解析方法。如果respondstoselector:或instancesrespondtoselector:被調用時,動態方法解析有機會為第一重要的選擇。如果你實現resolveinstancemethod:但要特別的選擇消息轉發機制,你可以在這些方法裏返回NO。 動態加載 一個Objective-C程序可以在運行時加載和鏈接新的類和分類。新的代碼被合並到程序中,並與開始加載在開始時的類和分類是一樣的。動態加載可以用來做很多不同的事情。例如,各個模塊的系統偏好設置就是動態加載的。 在cocoa環境中,動態加載通常用於允許定制應用程序。別人可以寫模塊用於你的程序運行時加載,比如在IB中加載自定義模版和在OS X系統偏好設置自定義模塊。這個加載模塊擴展您的應用程序。它們以你所允許的方式做出貢獻,但是不允許定義你的程序。您提供框架,但其他提供代碼。 雖然是一個運行時函數的執行,在Objective-C Mach-O文件動態加載模塊(在objc / objc-load.h定義objc_loadmodules,),cocoa的NSBundle類提供了一個面向對象的動態加載和相關服務集成更方便的接口。想了解在NSBundle類及其使用信息基礎框架參可考NSBundle類規範。想了解OS X Mach-O文件格式可參考ABI在Mach-O文件信息。

消息轉發

給一個對象發送一條無法處理的消息就會產生一個錯誤,然而,在觸發這個錯誤前,這個運行時系統會給這個接收者一個機會處理這條消息。 如果你給一個對象發送無法處理的消息,在觸發一個錯誤之前運行時系統會給這個對象發送一條forwardinvocation消息並攜帶一個NSInvocation對象作為其唯一的參數,NSInvocation對象封裝原始消息以及傳遞的參數。 你可以實現一個forwardinvocation:給消息的一個默認響應的方法,或用其他方法來避免錯誤。正如它的名字所暗示的,forwardinvocation:通常用於給另一個對象轉發消息。 要查看轉發的範圍和意圖,設想以下場景:假設,首先,您設計了一個對象可以響應稱為negotiate的消息,並且希望它的響應包含另一種對象的響應。您可以輕松地完成這一任務,那就是你可以在你的negotiate方法實現體裏給另一個對象傳遞一個negotiate消息。 更進一步,假設您希望你的對象對negotiate消息的響應可以在另一個類中的響應實現。實現這一目標的一種方法是讓您的類繼承另一個類的方法。然而,可能不可能這樣安排事情。因為有可能你的類和實現negotiate的類在繼承層次結構的不同分支中。 即使您的類不能繼承negotiate方法,仍然可以借用這個類將消息傳遞到另一個類的實例的方法來實現這個目的。
- (id)negotiate
{
    if ( [someOtherObject respondsTo:@selector(negotiate)] )
        return [someOtherObject negotiate];
    return self;
}
這樣做的方式可能會有點麻煩,特別是如果有許多消息要將對象傳遞給另一個對象。您必須實現一種方法來覆蓋從其他類中借用的每個方法。此外,在你編寫代碼時,你不可能處理你不知道的情況,你可能想要轉發的全部信息。該集合可能取決於運行時的事件,並且隨著新方法和類在將來實現,它可能會發生變化。 forwardinvocation提供的第二次機會是:消息提供了一些臨時解決方法來處理這種問題,其中一個方法就是動態處理而不是靜態的,它的工作流程是這樣的:當一個對象不能響應消息因為它沒有一個方法在消息選擇器匹配,運行時系統通知對象發送一個消息forwardinvocation。每一個對象都從從NSObject類的方法繼承了一個forwardinvocation:方法。然而,NSObject的版本的方法只是調用doesnotrecognizeselector:。通過重寫NSObject的版本和實現你自己的,你可以利用這個forwardinvocation:方法提供轉發消息到其他對象。 為了轉發一個消息,forwardinvocation需要做的事情是:確定這個消息應該轉發到哪裏並且把它的原始信息傳遞到那裏。 這個消息可以通過invokeWithTarget:方法進行傳遞
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([someOtherObject respondsToSelector:
            [anInvocation selector]])
        [anInvocation invokeWithTarget:someOtherObject];
    else
        [super forwardInvocation:anInvocation];
}
轉發的消息的返回值被返回給原始發送方。所有類型的返回值都可以傳遞給發送方,包括id、結構和雙精度浮點數。 一個forwardinvocation:方法可以作為一種識別的信息集散中心,組合出來不同的接收器。或者它可以是一個中轉站,把所有的信息發送到同一個目的地。它可以把一個消息翻譯成另一個,或者簡單地“吞咽”一些消息,這樣就沒有響應,也沒有錯誤。一個forwardinvocation:方法也可以合並多個消息到一個單一的響應。forwardinvocation:所做的就是提升類的方法實現能力,然而,它提供了在轉發鏈中連接對象的機會,為程序設計提供了可能性。

轉發和多重繼承

消息轉發可以用於模擬多重繼承,如圖所示,一個Warrior實例轉發negotiate消息到一個Diplomat實例。這就好像Warrior繼承了Diplomat來響應negotiate消息 技術分享 然而轉發和多重繼承有一個最主要的區別就是:多重繼承合並不同的功能到單個對象中。它趨向於大的、多方面的對象。另一方面,轉發就是將不同的職責分配給不同的對象,它將問題分解為較小的對象,但將這些對象透明的關聯到一個消息發送者。

代理對象

轉發不僅模擬了多重繼承,還可以開發表示或覆蓋更多實體對象的輕量級對象。這個代理可以代表其他對象並且作為一個漏鬥轉發消息。 在Objective-C編程語言關於遠程通信提到的proxy就是這樣一個代理,一個proxy將消息轉發給遠程接收器的管理細節,確保參數值在連接上復制和檢索,等等. 但它不會做其他事情;它不復制遠程對象的功能,而是簡單地給遠程對象一個本地地址,一個它可以在另一個應用程序中接收消息的地方。 還有其他類型的一些代理對象。例如,假設您有一個操作大量數據的對象,也許它會創建一個復雜的圖片,或者讀取磁盤上文件的內容。設置此對象可能耗時,因此您希望在需要時或系統資源暫時閑置時進行此操作。同時,為了應用程序中的其他對象正常運行,您至少需要一個占位符 在這種情況下,您可以首先為它創建一個輕量級代理而非真實對象。這個代理可以自己做一些事情,比如回答有關數據的問題,但大多數情況下,它只為較大的對象保留一個位置,當時間到來時,向它轉發消息。當代理的forwardinvocation:方法首先接收一個信息用於其他對象,它將確保對象的存在,沒有就創建它,這個較大對象的所有的消息都通過這個代理,就程序的其余部分而言,這個代理和較大的對象是等價的。 轉發和繼承 雖然轉發模仿了繼承,這個NSObject類從來不會混淆了這兩者.像respondstoselector 和isKindOfClass:方法只看繼承層次結構,而不看轉發鏈。例如,如果詢問戰士對象是否對協商消息作出響應,
if ( [aWarrior respondsToSelector:@selector(negotiate)] )
    ...
答案是NO,盡管它可以在不出錯的情況下收到談判信息,並在一定程度上對他們作出回應,將他們轉交給外交官。 在許多情況下,會正確的返回NO,但也有可能不會,如果使用轉發來設置代理對象或擴展類的功能,那麽轉發機制應該與繼承一樣透明。如果你希望你的對象看起來就像是繼承了這個對象(轉發消息到這個對象)的行為,你就需要重新實現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: 方法來準確的描述這個轉發消息。如果一個對象有能力轉發一條消息到它的代理的話,你應該像下面這樣實現methodSignatureForSelector:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:selector];
    if (!signature) {
       signature = [surrogate methodSignatureForSelector:selector];
    }
    return signature;
}
你應該考慮在私有代碼裏寫這個轉發算法,然後通過 forwardInvocation:調用它。這是個前沿技術,用於你沒有其他方式處理未知消息的時候,它不是替代繼承,如果你必須使用該技術,確保你充分理解了消息轉發機制。

類型編碼

為了支持運行時系統,編譯器將傳參類型和返參類型進行了編碼,相應的編碼器指示符是@encode,比如void 編碼為v,char編碼為c,對象編碼為@,類編碼為#,SEL編碼為:,而結構體類型則由基本類型組成。事實上,可以用來作為一個參數C sizeof()算法。
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
結構體類型則由基本類型組成,如下:
typedef struct example {
    id   anObject;
    char *aString;
    int  anInt;
} Example;

屬性申明

當編譯器遇到屬性申明時,它會生成一些可描述的元數據將其與相應的類、分類和協議關聯起來。存在一些函數可以在類活著協議中通過名稱來查找這些元數據,通過這些函數我們可以獲得編碼後的屬性類型,復制屬性的attribute列表,因此類和協議的屬性列表我們都可以獲得。 Property結構體定義了一個指向屬性描述符的不透明句柄
typedef struct objc_property *Property;
你可以使用的class_copypropertylist和protocol_copypropertylist方法檢索一個類和協議關聯的屬性數組(包括加載的分類)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)
objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
比如
@interface Lender : NSObject {
    float alone;
}
@property float alone;
@end
你可以通過下面方法獲取到屬性列表
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
你可以通過property_getName方法獲取到一個屬性名稱
const char *property_getName(objc_property_t property)
你可以使用protocol_getproperty和class_getproperty方法在類和協議中通過給定名稱獲得參考的屬性
objc_property_t class_getProperty(Class cls, const char *name)
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty)
你可以通過property_getAttributes方法獲得名稱和編碼後的屬性類型
const char *property_getAttributes(objc_property_t property)
將這些集合放在一起,可以使用以下代碼打印與類相關聯的所有屬性的列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
    objc_property_t property = properties[i];
    fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}

屬性類型編碼

與類型編碼類似,屬性類型也有相應的編碼方案,比如readonly編碼為R,copy編碼為C,retain編碼為&等。通過函數可以獲取編碼後的字符串,該字符串以T開頭,緊接@encode type和逗號,接著以V和變量名結尾。比如: @property char charDefault 編碼為 Tc,VcharDefault。

Runtime常用的數據結構

id

指向某個類的實例的指針,指向objc_object結構體的指針指針,objc_object包含isa指針,根據 isa 指針就可以找到對象所屬的類。 typedef struct objc_object *id; struct objc_object { Class isa; };

SEL

OC中@selector表示類型,SEL是指向objc_selector的結構體指針。 typedef struct objc_selector *SEL;

Class

Class是指向objc_class的結構體指針 typedef struct objc_class *Class; struct objc_class { Class isa; Class super_class; const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols; } 從objc_class 可以看到,一個運行時類中包含了了它的父類指針、類名、成員變量、方法列表、緩存以及附屬的協議。

Ivar

一個不透明類型表示一個實例變量 typedef struct objc_ivar *Ivar; char *ivar_name; char *ivar_type; int ivar_offset; int space; }

Method

一個不透明類型表示類定義中的一個方法 typedef struct objc_method *Method; struct objc_method { SEL method_name; //方法名 char *method_types; //方法類型 IMP method_imp; //方法實現 } 其中IMP是一個block類型typedef id (*IMP)(id, SEL, …);

Cache

typedef struct objc_cache *Cache; struct objc_cache { unsigned int mask /* total = mask + 1 */; unsigned int occupied; Method buckets[1]; }; Cache 會緩存最近調用的方法,如果一個方法被調用,那麽它有可能今後還會被調用,每當實例對象接收到一個消息時,優先在 Cache 中查找。

完整的消息轉發機制

  • 首先檢測這條消息是否需要處理,比如selector 的 target 為 nil的話就不處理。
  • 如果消息有效,就進行消息傳遞機制,先從 cache 裏查找selector對應的實現IMP,如果找到了就運行對應的函數去執行相應的代碼。
  • 如果 cache 找不到就找類的方法列表中是否有對應的方法。
  • 如果類的方法列表中找不到就到父類的方法列表中查找,一直找到 NSObject 類為止。
  • 如果還找不到,就要開始進入動態方法解析了。
  • 如果還找不到,就要進行消息轉發機制了。

Runtime最全詳解