本文來自網易云社區
作者:宋申易
所以到底 objc_msgSend 發生了什么?
很多事情。看一下這段代碼:
[self printMessageWithString:@"Hello World!"];
這實際上被編譯器翻譯成:
objc_msgSend(self, @selector(printMessageWithString:), @"Hello World!");
我們順著目標對象的 isa 指針查找,看該對象(或者它其中一個父類)是否能響應 @selector(printMessageWithString:) 選擇器。假設我們在分派表(dispatch table)或者緩存中找到了該選擇器,我們會跟蹤函數指針并執行它。所以 objc_msgSend() 永遠不會返回,它開始執行,然后跟蹤一個指向你的方法的指針,然后你的方法返回,這看起來就像 objc_msgSend() 返回了一樣。
Bill Bumgarner 在(Part 1, Part 2 & Part 3)里描述了更多 objc_msgSend() 的細節。總結一下他的文章結合你看到的 Objective-C 運行時代碼:
檢查被忽略的選擇器和短路。顯然,如果我們在垃圾收集下運行,我們可以忽略 -retain,-release 等調用。
檢查 nil 目標。和其他語言不同,在 ObjC 里向 nil 發送消息十分合理并且有些情況下確實想要這么做。假如不是 nil 則繼續……
接下來在類中找到 IMP,首先通過類緩存來查找,如果找到就跟隨指針跳轉到對應的函數
如果在緩存中找不到 IMP,則通過分派表來查找,如果找到就跟隨指針跳轉到對應的函數
如果這兩個地方都找不到 IMP,則跳轉到轉發(forwarding)機制。
這意味著最終你的代碼會被編譯器轉譯成 C 函數。你寫的某個方法可能是這樣:
-(int)doComputeWithNum:(int)aNum
它會被轉換成……
int aClass_doComputeWithNum(aClass *self, SEL _cmd, int aNum)
ObjC 運行時會通過調用這些方法的函數指針來真正執行方法。我曾說過你不能直接調用這些轉譯后的方法,但其實 Cocoa 框架提供了一個獲取函數指針的方法……
// C function pointer
int (computeNum *)(id, SEL, int);
// methodForSelector is COCOA & not ObjC Runtime
// gets the same function pointer objc_msgSend gets
computeNum = (int (*)(id, SEL, int))[target methodForSelector:@selector(doComputeWithNum:)];
// execute the C function pointer returned by the runtime
computeNum(obj, @selector(doComputeWithNum:), aNum);
用這種方式你可以直接訪問函數并且直接在運行時中執行它,甚至繞過運行時的動態特性(為了確保指定的方法被執行)。ObjC 運行時也用這種方法來調用你的函數,只是用了 objc_msgSend()。
Objecetive-C 消息轉發
在 Objective-C 中,發送消息給可能不能響應該消息的對象是合法的(可能是有意設計的)。蘋果文檔里提到可能的原因一個是模擬 Objective-C 并不原生支持的多重繼承,或者是你想把真正接受消息的類或者對象隱藏起來。這也是運行時很有必要的一件事。具體是這樣的:
運行時搜索類緩存、類分派表以及父類的所有方法,沒有找到指定的方法。
運行時對你的類調用 + (BOOL)resolveInstanceMethod:(SEL)aSEL。這給你提供了一個方法實現的機會,告訴運行時你已經解決了這個方法,如果它應該開始進行搜索,它將會找到方法。具體你可以這樣做,定義一個函數:
void fooMethod(id obj, SEL _cmd) {
NSLog(@"Doing Foo");
}
然后可以使用 class_addMethod() 來解析它…
+ (BOOL)resolveInstanceMethod:(SEL)aSEL {
if(aSEL == @selector(doFoo:)) {
class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
return YES;
}
return [super resolveInstanceMethod];
}
class_addMethod() 的最后一部分中的 v@: 是該方法返回的內容,也是它的參數。你可以在運行時指南的 Type Encodings 章節中了解可以放入哪些內容。
如果我們不能解析該方法,運行時會繼續調用 - (id)forwardingTargetForSelector:(SEL)aSelector。它所做的是給你一個機會,讓運行時指向在另一個可以響應消息的對象。最好在開銷更大的 - (void)forwardInvocation:(NSInvocation *)anInvocatio 方法接管之前調用,例如:
{
if(aSelector == @selector(mysteriousMethod:)) {
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
顯然,你不想從這個方法中返回 self,因為這樣會導致無限循環。
運行時最后一次嘗試發送一個消息發送到它的預定目標,調用 - (void)forwardInvocation:(NSInvocation *)anInvocation。NSInvocation 本質上是一個 Objective-C 消息的的對象形式。一旦你有了一個 NSInvocation,你基本上可以改變任何信息,包括它的目標,選擇器和參數。例如你可以做:
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL invSEL = invocation.selector;
if([altObject respondsToSelector:invSEL]) {
[invocation invokeWithTarget:altObject];
} else {
[self doesNotRecognizeSelector:invSEL];
}
}
如果你的對象繼承了 NSObject, 默認情況下 - (void)forwardInvocation:(NSInvocation *)anInvocation 實現會調用 -doesNotRecognizeSelector:方法。你可以重寫這個方法如果你想最后再做點什么。
不脆弱的(Non Fragile)實例變量列表(ivars) (現代運行時)
現代運行時新增加了不脆弱的(Non Fragile) ivars 的概念。當編譯你的類的時候,編譯器生成了一個實例變量內存布局(ivar layout),來告訴運行時去那里訪問你的類的實例變量們。這是一個底層實現細節:ivars 是實例變量分別相對于你的對象地址的偏移量,讀取 ivars 的字節數就是讀取的變量的大小。你的 ivar 布局可能看起來像這樣(第一列是字節偏移量):
這里我們畫出了一個 NSObject 的實例變量內存布局。我們有一個繼承了 NSObject 的類,增加了一些新的實例變量。這沒什么問題,直到蘋果發布了新的 Mac OS X 10.x 系統,NSObject 突然增加兩個新的實例變量,于是:
你的自定義對象和 NSObject 對象重疊的部分被清除。如果 Apple 永遠不改變之前的布局可以避免這種情況,但如果他們那樣做,那么他們的框架就永遠不會進步。在“脆弱的 ivars” 下,你必須重新編譯你從 Apple 繼承的類,來恢復兼容性。那么在不脆弱的情況下會發生什么呢?
在不脆弱的 ivars 下,編譯器生成與脆弱 ivars 相同的 ivars 布局。然而,當運行時檢測到和父類有重疊時,它會調整偏移量,以增加對類的補充,保留了在子類中添加的內容。
Objective-C 關聯對象(Associated Objects)
Mac OS X 10.6 Snow Leopard 中引入了關聯引用。Objective-C 沒有原生支持動態地將變量添加到對象上。因此,你需要竭盡全力構建基礎架構,以假裝正在向類中添加一個變量。在 Mac OS X 10.6 中,Objective-C 運行時提供了原生支持。如果我們想給每個已經存在的類添加一個變量,比如 NSView,我們可以這樣做:
#import //Cocoa
#include //objc runtime api’s
@interface NSView (CustomAdditions)
@property(retain) NSImage *customImage;
@end
@implementation NSView (CustomAdditions)
static char img_key; //has a unique address (identifier)
- (NSImage *)customImage {
return objc_getAssociatedObject(self,&img_key);
}
- (void)setCustomImage:(NSImage *)image {
objc_setAssociatedObject(self,&img_key,image,
OBJC_ASSOCIATION_RETAIN);
}
@end
你可以在 runtime.h 看到。如何存儲傳遞給 objc_setAssociatedObject() 的值的選項:
/* Associated Object support. */
/* objc_setAssociatedObject() options */
enum {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
這些與你可以在@property語法中傳遞的選項相匹配。
混合 vTable 分發
如果你看一下現代運行時代碼,你會看到這個(在 objc-runtime-new.m)。
/***********************************************************************
*vtable dispatch
**每個類都有一個 vtable 指針。vtable 是一個 IMP 數組,
*所有的類的 vtable 中表示的選擇器數量都是相同的。(i.e.
*沒有一個類有更大或更小的 vtable).
*每個 vtable 索引都有一個關聯的蹦床,該蹦床在接收者類的
*vtable 的該索引處分派給 IMP(檢查 NULL 后)。分派
*fixup 使用了蹦床而不是 objc_msgSend.
*脆弱性:vtable 的大小和選擇器列表在啟動時已經設定好了。
*編譯器生成的代碼無法依賴于任何特定的vtable配置,甚至
*根本不使用 vtable 調度。
*內存大小:如果一個類的 vtable 和它的父類相同(i.e. 該類
*沒有重寫任何 vtable 選擇器), 那么這個類直接指向它的父
*類的 vtable。這意味著被選中包含在 vtable 中的選擇器應
*該有以下特點:
*(1) 經常被調用,但是 (2) 不經常被重寫。
*特別的是,-dealloc 是一個壞的選擇。
*轉發: 如果一個類沒有實現 vtable 中的部分選擇器, 這個類的
*vtable 中的這些選擇器的 IMP 會被設置成 objc_msgSend。
*+initialize: 每個類保持默認的 vtable(總是重定向到
*objc_msgSend)直到其 +initialize 初始化方法完成。否則,
*一個類的第一個消息可能是一個 vtable 調度,而 vtable
*蹦床不包括 +initialize 初始化檢查。
*改變: Categories, addMethod, 和 setImplementation 如果影響
*到了 vtable 的選擇器,類和所有的子類的 vtable 都將強制重建。
**********************************************************************/
這背后的思想是,運行時試圖在這個 vtable 里面存儲最常被調用的選擇器,這可以給 app 加速,因為這比 objc_msgSend 使用了更少的指令。這個 vtable 包含 16 個最常被調用的選擇器,占據了絕大部分全局調用的選擇器。你可以看到垃圾回收 app 和非垃圾回收 app 的默認選擇器都是什么。
static const char * const defaultVtable[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"retain",
"release",
"autorelease",
};
static const char * const defaultVtableGC[] = {
"allocWithZone:",
"alloc",
"class",
"self",
"isKindOfClass:",
"respondsToSelector:",
"isFlipped",
"length",
"objectForKey:",
"count",
"objectAtIndex:",
"isEqualToString:",
"isEqual:",
"hash",
"addObject:",
"countByEnumeratingWithState:objects:count:",
};
那么你怎么知道是否使用了 vtable 中的方法了呢?你會在調試的堆棧跟蹤中看到以下幾個方法。這些方法你可以看成調試版的 objc_msgSend()。
objc_msgSend_fixup 代表 runtime 調用一個方法并正要把它加入到 vtable 中。
objc_msgSend_fixedup 代表你調用方法曾經在 vtable 中,現在已經不在里面了。
objc_msgSend_vtable[0-15] 代表上述 vtable 中的一個常用方法。runtime 可以隨意分配或取消它想要的值。所以這一次 objc_msgSend_vtable10 對應于 -length 方法,下一次運行可能對應方法就變了。
總結
我希望你喜歡這些,這篇文章大體上組成了我在我給 Des Moines Cocoaheads 的 ObjC 演講中提到的內容。ObjC 運行時寫的很棒,它提供了許多我們在 Cocoa / Objective-C 中習以為常的特性。如果你還沒看過 Apple 的 ObjC 運行時文檔,希望你去看一看。謝謝!
網易云免費體驗館,0成本體驗20+款云產品!
更多網易研發、產品、運營經驗分享請訪問網易云社區。