IOS開發系列——Objective-c Runtime專題總結【整理】

Objective-c Runtime專題總結

 

原文  http://yulingtianxia.com/blog/2014/11/05/objective-c-runtime/

 

1    OC與Runtime的交互方式

   OC 從三種不同的層級上與 Runtime 系統進行交互,分別是通過 Objective-C 源代碼,通過Foundation 框架的 NSObject 類定義的方法,通過對 runtime 函數的直接調用。

1.1     Objective-C源代碼

大部分情況下你就只管寫你的OC代碼就行,runtime 系統自動在幕後辛勤勞作着。

1.2     NSObject的方法

Cocoa 中大多數類都繼承於NSObject 類,也就自然繼承了它的方法。最特殊的例外是 NSProxy ,它是個抽象超類,它實現了一些消息轉發有關的方法,可以通過繼承它來實現一個其他類的替身類或是虛擬出一個不存在的類。

    有的NSObject 中的方法起到了抽象接口的作用,比如 description 方法需要你重載它併爲你定義的類提供描述內容。 NSObject 還有些方法能在運行時獲得類的信息,並檢查一些特性,比如class 返回對象的類; isKindOfClass: 和isMemberOfClass: 則檢查對象是否在指定的類繼承體系中;respondsToSelector: 檢查對象能否響應指定的消息;conformsToProtocol: 檢查對象是否實現了指定協議類的方法;methodForSelector: 則返回指定方法實現的地址。

1.3    Runtime的函數

    Runtime 系統是一個由一系列函數和數據結構組成,具有公共接口的動態共享庫。頭文件存放於 /usr/include/objc 目錄下。許多函數允許你用純C代碼來重複實現 OC 中同樣的功能。雖然有一些方法構成了 NSObject 類的基礎,但是你在寫OC 代碼時一般不會直接用到這些函數的,除非是寫一些 OC 與其他語言的橋接或是底層的debug工作。在Objective-C Runtime Reference 中有對 Runtime函數的詳細文檔。

2     Runtime術語

id objc_msgSend ( id self, SELop, ... );

 

2.1    SEL

    objc_msgSend 函數第二個參數類型爲 SEL ,它是selector 在OC中的表示類型(Swift中是Selector類)。 selector 是方法選擇器,可以理解爲區分方法的 ID,而這個 ID 的數據結構是 SEL :

typedef struct objc_selector*SEL;

 

其實它就是個映射到方法的C字符串,你可以用 OC 編譯器命令 @selector() 或者 Runtime 系統的 sel_registerName 函數來獲得一個 SEL 類型的方法選擇器。

不同類中相同名字的方法所對應的方法選擇器是相同的,即使方法名字相同而變量類型不同也會導致它們具有相同的方法選擇器,於是OC 中方法命名有時會帶上參數類型( NSNumber 一堆抽象工廠方法拿走不謝),Cocoa 中有好多長長的方法哦。

2.2    Id 與objc_object結構體

objc_msgSend 第一個參數類型爲 id ,大家對它都不陌生,它是一個指向類實例的指針:

typedefstructobjc_object*id;

 objc_object 又是啥呢:

structobjc_object{Classisa;};

objc_object 結構體包含一個 isa 指針,根據 isa 指針就可以順藤摸瓜找到對象所屬的類。

2.3    Class

之所以說 isa 是指針是因爲 Class 其實是一個指向 objc_class 結構體的指針:

typedefstructobjc_class*Class;

 objc_class 就是我們摸到的那個瓜,裏面的東西多着呢:

structobjc_class{     
Classisa  OBJC_ISA_AVAILABILITY; 
#if !__OBJC2__     
Classsuper_class             OBJC2_UNAVAILABLE;     
constchar*name               OBJC2_UNAVAILABLE;     
longversion                    OBJC2_UNAVAILABLE;     
longinfo                        OBJC2_UNAVAILABLE;     
longinstance_size             OBJC2_UNAVAILABLE;     
structobjc_ivar_list*ivarsOBJC2_UNAVAILABLE;     
structobjc_method_list**methodLists    OBJC2_UNAVAILABLE;     
structobjc_cache*cache     OBJC2_UNAVAILABLE;     
structobjc_protocol_list*protocols    OBJC2_UNAVAILABLE;
#endif  
}OBJC2_UNAVAILABLE;

 

 

    可以看到運行時一個類還關聯了它的超類指針,類名,成員變量,方法,緩存,還有附屬的協議。

其中 objc_ivar_list  objc_method_list 分別是成員變量列表和方法列表:

structobjc_ivar_list{     
intivar_count               OBJC2_UNAVAILABLE;#ifdef __LP64__     intspace                   OBJC2_UNAVAILABLE;
#endif     /* variable length structure */     
structobjc_ivarivar_list[1]      OBJC2_UNAVAILABLE;
}  OBJC2_UNAVAILABLE;  
structobjc_method_list{     
structobjc_method_list*obsolete      OBJC2_UNAVAILABLE;  
intmethod_count              OBJC2_UNAVAILABLE;
#ifdef __LP64__     
intspace                       OBJC2_UNAVAILABLE;
#endif     
/* variable length structure */     
structobjc_methodmethod_list[1]     OBJC2_UNAVAILABLE;
}

2.4    元類 (MetaClass)

    一個ObjC 類同時也是一個對象,爲了處理類和對象的關係,runtime 庫創建了一種叫做元類 (Meta Class) 的東西。當你發出一個類似 [NSObjectalloc] 的消息時,你事實上是把這個消息發給了一個類對象 (Class Object) ,這個類對象必須是一個元類的實例,而這個元類同時也是一個根元類 (root meta class) 的實例。你會說NSObject 的子類時,你的類就會指向 NSObject 做爲其超類。但是所有的元類都指向根元類爲其超類。所有的元類的方法列表都有能夠響應消息的類方法。所以當[NSObjectalloc] 這條消息發給類對象的時候, objc_msgSend() 會去它的元類裏面去查找能夠響應消息的方法,如果找到了,然後對這個類對象執行方法調用。

    上圖實線是super_class 指針,虛線是isa指針。 有趣的是根元類的超類是NSObject,而isa指向了自己,而NSObject的超類爲nil,也就是它沒有超類。

2.5    Method

Method 是一種代表類中的某個方法的類型。

typedefstructobjc_method*Method;

 objc_method 在上面的方法列表中提到過,它存儲了方法名,方法類型和方法實現:

structobjc_method{     
SELmethod_name                       OBJC2_UNAVAILABLE;     
char*method_types                  OBJC2_UNAVAILABLE;     
IMPmethod_imp                       OBJC2_UNAVAILABLE;
}OBJC2_UNAVAILABLE;

·      方法名類型爲 SEL 前面提到過相同名字的方法即使在不同類中定義它們的方法選擇器也相同

·      方法類型 method_types 是個 char 指針其實存儲着方法的參數類型和返回值類型

·      method_imp 指向了方法的實現本質上是一個函數指針後面會詳細講到

 

2.6    Ivar

Ivar 是一種代表類中實例變量的類型。

typedefstructobjc_ivar*Ivar;

 objc_ivar 在上面的成員變量列表中也提到過:

tructobjc_ivar{     
char*ivar_name                 OBJC2_UNAVAILABLE;     
char*ivar_type                 OBJC2_UNAVAILABLE;     
intivar_offset                 OBJC2_UNAVAILABLE;
#ifdef __LP64__     
intspace                        OBJC2_UNAVAILABLE;
#endif
}OBJC2_UNAVAILABLE;

PS: OBJC2_UNAVAILABLE 之類的宏定義是蘋果在 OC 中對系統運行版本進行約束的黑魔法,有興趣的可以查看源代碼。

 

2.7    IMP 函數指針

IMP  objc.h 中的定義是:

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

  它就是一個函數指針,這是由編譯器生成的。當你發起一個 ObjC 消息之後,最終它會執行的那段代碼,就是由這個函數指針指定的。而IMP這個函數指針就指向了這個方法的實現。既然得到了執行某個實例某個方法的入口,我們就可以繞開消息傳遞階段,直接執行方法,這在後面會提到。

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

 

2.8    Cache

 runtime.h Cache的定義如下:

typedefstructobjc_cache*Cache

還記得之前 objc_class 結構體中有一個 struct objc_cache *cache 吧,它到底是緩存啥的呢,先看看 objc_cache 的實現:

structobjc_cache{     
unsignedintmask/* total = mask + 1 */   OBJC2_UNAVAILABLE;
unsignedintoccupied                OBJC2_UNAVAILABLE;
Methodbuckets[1]                      OBJC2_UNAVAILABLE;
};

Cache 爲方法調用的性能進行優化通俗地講每當實例對象接收到一個消息時它不會直接在 isa 指向的類的方法列表中遍歷查找能夠響應消息的方法因爲這樣效率太低了而是優先在 Cache 中查找Runtime 系統會把被調用的方法存到Cache 理論上講一個方法如果被調用那麼它有可能今後還會被調用下次查找的時候效率更高

 

3    消息

OC中發送消息是用中括號 []把接收者和消息括起來而直到運行時纔會把消息與方法實現綁定

3.1    objc_msgSend函數

  看起來像是 objc_msgSend返回了數據,其實 objc_msgSend 從不返回數據而是你的方法被調用後返回了數據。下面詳細敘述下消息發送步驟:

1.   檢測這個 selector 是不是要忽略的比如 Mac OS X 開發有了垃圾回收就不理會 retain , release 這些函數了

2.   檢測這個target 是不是 nil 對象ObjC的特性是允許對一個 nil 對象執行任何一個方法不會 Crash因爲會被忽略掉

3.   如果上面兩個都過了那就開始查找這個類的 IMP 先從 cache 裏面找完了找得到就跳到對應的函數去執行

4.   如果 cache 找不到就找一下方法分發表。(Class 中的方法列表)

5.   如果分發表找不到就到超類的分發表去找一直找直到找到 NSObject 類爲止

6.   如果還找不到就要開始進入 動態方法 解析了後面會提到

 

其實編譯器會根據情況在 objc_msgSend , objc_msgSend_stret ,objc_msgSendSuper ,  objc_msgSendSuper_stret 四個方法中選擇一個來調用如果消息是傳遞給超類那麼會調用名字帶有”Super”的函數如果消息返回值是數據結構而不是簡單值時那麼會調用名字帶有”stret”的函數排列組合正好四個方法

3.2    方法中的隱藏參數

  我們經常在方法中使用 self 關鍵字來引用實例本身,但從沒有想過爲什麼 self就能取到調用當前方法的對象吧。其實 self 的內容是在方法運行時被偷偷地動態傳入的

   objc_msgSend 找到方法對應的實現時,它將直接調用該方法實現,並將消息中所有的參數都傳遞給方法實現,同時,它還將傳遞兩個隱藏的參數:

接收消息的對象(也就是 self 指向的內容)

方法選擇器 _cmd 指向的內容)

  之所以說它們是隱藏的是因爲在源代碼方法的定義中並沒有聲明這兩個參數。它們是在代碼被編譯時被插入實現中的。儘管這些參數沒有被明確聲明,在源代碼中我們仍然可以引用它們。在下面的例子中, self 引用了接收者對象,而 _cmd 引用了方法本身的選擇器:

- strange {    

id  target = getTheReceiver();    

SEL method = getTheMethod();     

if ( target == self || method == _cmd )   return nil

return [target performSelector:method];

}

在這兩個參數中, self 更有用。實際上,它是在方法實現中訪問消息接收者對象的實例變量的途徑。而當方法中的 super 關鍵字接收到消息時,編譯器會創建一個 objc_super 結構體:

struct objc_super { id receiver; Class class; };

這個結構體指明瞭消息應該被傳遞給特定超類的定義。

3.3    獲取方法地址

  IMP那節提到過可以避開消息綁定而直接獲取方法的地址並調用方法。這種做法很少用,除非是需要持續大量重複調用某方法的極端情況,避開消息發送氾濫而直接調用該方法會更高效。

NSObject 類中有個 methodForSelector: 實例方法,你可以用它來獲取某個方法選擇器對應的 IMP ,舉個栗子:

void (*setter)(id, SEL, BOOL);

int i; 

setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];

for ( i = 0 ; i < 1000 ; i++ )    

    setter(targetList[i], @selector(setFilled:), YES);

PS methodForSelector: 方法是由 Cocoa Runtime 系統提供的而不是 OC 自身的特性

 

4    動態方法解析

  你可以動態地提供一個方法的實現。例如我們可以用 @dynamic 關鍵字在類的實現文件中修飾一個屬性:

@dynamic propertyName;

   這表明我們會爲這個屬性動態提供存取方法,也就是說編譯器不會再默認爲我們生成 setPropertyName:  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

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

 

5    消息轉發

5.1    重定向

  在消息轉發機制執行前,Runtime系統會再給我們一次偷樑換柱的機會,即通過重載 -(id)forwardingTargetForSelector:(SEL)aSelector 方法替換消息的接受者爲其他對象:

- (id)forwardingTargetForSelector:(SEL)aSelector {    

if(aSelector == @selector(mysteriousMethod:)){        

    return alternateObject;    

}    

    return [super forwardingTargetForSelector:aSelector];

}

  畢竟消息轉發要耗費更多時間,抓住這次機會將消息重定向給別人是個不錯的選擇,不過千萬別返回 self ,因爲那樣會死循環。

 

5.2    轉發

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

- (void)forwardInvocation:(NSInvocation *)anInvocation {    

if ([someOtherObject respondsToSelector: [anInvocation selector]])

    [anInvocation invokeWithTarget:someOtherObject];    

else [super forwardInvocation:anInvocation];

}

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

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

   forwardInvocation: 方法就像一個不能識別的消息的分發中心,將這些消息轉發給不同接收對象。或者它也可以象一個運輸站將所有的消息都發送給同一個接收對象。它可以將一個消息翻譯成另外一個消息,或者簡單的喫掉某些消息,因此沒有響應也沒有錯誤。 forwardInvocation: 方法也可以對不同的消息提供同樣的響應,這一切都取決於方法的具體實現。該方法所提供是將不同的對象鏈接到消息鏈的能力。

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

5.3    轉發和多繼承

  轉發和繼承相似,可以用於爲OC編程添加一些多繼承的效果。就像下圖那樣,一個對象把消息轉發出去,就好似它把另一個對象中的方法借過來或是繼承過來一樣。

 這使得不同繼承體系分支下的兩個類可以繼承對方的方法,在上圖中 Warrior Diplomat 沒有繼承關係,但是 Warrior  negotiate 消息轉發給了Diplomat 後,就好似 Diplomat  Warrior 的超類一樣。

  消息轉發彌補了 OC 不支持多繼承的性質,也避免了因爲多繼承導致單個類變得臃腫複雜。它將問題分解得很細,只針對想要借鑑的方法才轉發,而且轉發機制是透明的。

5.4    替代者對象(Surrogate Objects)

轉發不僅能模擬多繼承,也能使輕量級對象代表重量級對象。弱小的女人背後是強大的男人,畢竟女人遇到難題都把它們轉發給男人來做了。這裏有一些適用案例,可以參看 官方文檔 

 

5.5   轉發與繼承

儘管轉發很像繼承,但是 NSObject 類不會將兩者混淆。像respondsToSelector:  isKindOfClass: 這類方法只會考慮繼承體系,不會考慮轉發鏈。比如上圖中一個 Warrior 對象如果被問到是否能響應 negotiate消息:

if([aWarriorrespondsToSelector:@selector(negotiate)])     ...

結果是 NO ,儘管它能夠接受 negotiate 消息而不報錯,因爲它靠轉發消息給Diplomat 類來響應消息。

如果你爲了某些意圖偏要弄虛作假讓別人以爲 Warrior 繼承到了 Diplomat negotiate 方法,你得重新實現 respondsToSelector: isKindOfClass: 來加入你的轉發算法:

-(BOOL)respondsToSelector:(SEL)aSelector{     if([superrespondsToSelector:aSelector])         returnYES;     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.  */     }     returnNO;}

除了 respondsToSelector:  isKindOfClass: 之外,instancesRespondToSelector: 中也應該寫一份轉發算法。如果使用了協議,conformsToProtocol: 同樣也要加入到這一行列中。類似地,如果一個對象轉發它接受的任何遠程消息,它得給出一個 methodSignatureForSelector: 來返回準確的方法描述,這個方法會最終響應被轉發的消息。比如一個對象能給它的替代者對象轉發消息,它需要像下面這樣實現 methodSignatureForSelector: 

- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector{
NSMethodSignature*signature=[supermethodSignatureForSelector:selector];
if(!signature){
    signature=[surrogatemethodSignatureForSelector:selector];
}
returnsignature;
}

 

6    壯的實例變量(NonFragile ivars)

  Runtime 的現行版本中,最大的特點就是健壯的實例變量。當一個類被編譯時,實例變量的佈局也就形成了,它表明訪問類的實例變量的位置。從對象頭部開始,實例變量依次根據自己所佔空間而產生位移:

  上圖左邊是 NSObject 類的實例變量佈局,右邊是我們寫的類的佈局,也就是在超類後面加上我們自己類的實例變量,看起來不錯。但試想如果那天蘋果更新了NSObject 類,發佈新版本的系統的話,那就悲劇了:

我們自定義的類被劃了兩道線,那是因爲那塊區域跟超類重疊了。唯有蘋果將超類改爲以前的佈局才能拯救我們,但這樣也導致它們不能再拓展它們的框架了,因爲成員變量佈局被死死地固定了。在脆弱的實例變量(Fragileivars) 環境下我們需要重新編譯繼承自 Apple 的類來恢復兼容性。那麼在健壯的實例變量下回發生什麼呢?

在健壯的實例變量下編譯器生成的實例變量佈局跟以前一樣,但是當 runtime系統檢測到與超類有部分重疊時它會調整你新添加的實例變量的位移,那樣你在子類中新添加的成員就被保護起來了。

需要注意的是在健壯的實例變量下,不要使用 sizeof(SomeClass) ,而是用class_getInstanceSize([SomeClassclass]) 代替;也不要使用offsetof(SomeClass,SomeIvar) ,而要用ivar_getOffset(class_getInstanceVariable([SomeClassclass], "SomeIvar"))來代替。

 

7    Objective-C Associated Objects

OS X 10.6 之後,Runtime系統讓OC支持向對象動態添加變量。涉及到的函數有以下三個:

void objc_setAssociatedObject(idobject,constvoid*key,idvalue,objc_AssociationPolicypolicy);
id objc_getAssociatedObject(idobject,constvoid*key);
void objc_removeAssociatedObjects(idobject);

這些方法以鍵值對的形式動態地向對象添加、獲取或刪除關聯值。其中關聯政策是一組枚舉常量:

enum{    
OBJC_ASSOCIATION_ASSIGN  =0,    
OBJC_ASSOCIATION_RETAIN_NONATOMIC  =1,   
OBJC_ASSOCIATION_COPY_NONATOMIC  =3,
OBJC_ASSOCIATION_RETAIN  =01401,    
OBJC_ASSOCIATION_COPY  =01403
};

這些常量對應着引用關聯值的政策,也就是 OC 內存管理的引用計數機制。

8     總結

  我們之所以讓自己的類繼承 NSObject 不僅僅因爲蘋果幫我們完成了複雜的內存分配問題,更是因爲這使得我們能夠用上 Runtime 系統帶來的便利。可能我們平時寫代碼時可能很少會考慮一句簡單的 [receiver message] 背後發生了什麼,而只是當做方法或函數調用。深入理解 Runtime 系統的細節更有利於我們利用消息機制寫出功能更強大的代碼,比如 MethodSwizzling 等。

9    參考鏈接

– Objective-C Runtime Programming Guide

– Objective-C runtime之運行時的基本特點

– Understanding the Objective-C Runtime

 

Objective-C Runtime Programming Guide

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html

 

深入理解Objective-C的Runtime機制

http://www.csdn.net/article/2015-07-06/2825133-objective-c-runtime/1

 

Objective-C Runtime 運行時之一:類與對象

http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/

 

Objective-C Runtime 運行時之二:成員變量與屬性

http://southpeak.github.io/blog/2014/10/30/objective-c-runtime-yun-xing-shi-zhi-er-:cheng-yuan-bian-liang-yu-shu-xing/

 

Objective-C Runtime 運行時之三:方法與消息

http://southpeak.github.io/blog/2014/11/03/objective-c-runtime-yun-xing-shi-zhi-san-:fang-fa-yu-xiao-xi-zhuan-fa/

 

Objective-C Runtime 運行時之四:MethodSwizzling

http://southpeak.github.io/blog/2014/11/06/objective-c-runtime-yun-xing-shi-zhi-si-:method-swizzling/

 

Objective-C Runtime 運行時之五:協議與分類

http://southpeak.github.io/blog/2014/11/08/objective-c-runtime-yun-xing-shi-zhi-wu-:xie-yi-yu-fen-lei/

 

Objective-C Runtime 運行時之六:拾遺

http://southpeak.github.io/blog/2014/11/09/objective-c-runtime-yun-xing-shi-zhi-liu-:shi-yi/

 

 

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