Objective-C Runtime(一)機制,類、對象結構,以及消息傳遞

Runtime 1 簡介,對象、類的結構,消息傳遞

前言

對于 runtime,看了很多的文章, 開發過程中也零零碎碎的用到過,感覺都不甚全面。反反復復研究了幾次,決定寫下來。對于初學者,感覺 runtime 很高深,不敢涉足;對于經常使用 runtime 的人來說,覺得 runtime 就那么點東西,不足為奇。其實在我反復研究它的過程中,每次都有收獲,因為它的原理不難,但是涉及很多,它貫穿這門語言,卻刀鋒尖利。

Objective-C Runtime(一)

  • 簡介
  • 對象、類的結構
    • objc_object
    • objc_class
  • 消息傳遞(Messaging)
    • objc_method
    • objc_msgSend

Objective-C Runtime(二)

  • 動態方法解析和轉發
    • 動態方法解析
    • 快速消息轉發
    • 標準消息轉發
    • 消息轉發與多繼承
    • 消息轉發與代理對象

Objective-C Runtime(三)

  • Method Swizzling
    • class_replaceMethod
    • method_setImplementation
    • method_exchangeImplementations
    • Method Swizzling 的應用
    • Method Swizzling 注意事項

Objective-C Runtime(四)

  • isa swizzling
    • 介紹
    • 應用之KVO
    • 注意

持續更新中...

一. runtime 簡介

Objective-C 擴展了 C 語言,并加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態機制的基石。

Objective-C 是一個動態語言,這意味著它不僅需要一個編譯器,也需要一個運行時系統來動態得創建類和對象、進行消息傳遞和轉發。

OC與C語言的區別:

  • 對于C語言,函數的調用在編譯的時候會決定調用哪個函數。
  • 對于OC的函數,屬于動態調用過程,在編譯的時候并不能決定真正調用哪個函數,只有在真正運行的時候才會根據函數的名稱找到對應的函數來調用。
  • 在編譯階段,OC可以調用任何函數,即使這個函數并未實現,只要聲明過就不會報錯。
  • 在編譯階段,C語言調用未實現的函數就會報錯。

二:對象、類的結構

在了解OC的消息傳遞之前,我們先明確對象、類的結構。

在OC中,類、對象都是一個C的結構體,從objc/objc.hobjc/runtime.h頭文件中,我們可以找到它們的定義:

typedef struct objc_class *Class;
typedef struct objc_object *id;

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; //指向它的類對象
};

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; //isa指針指向Meta Class,因為Objc的類的本身也是一個Object,為了處理這個關系,runtime就創造了Meta Class,當給類發送[NSObject alloc]這樣消息時,實際上是把這個消息發給了Class Object

#if !__OBJC2__
    Class _Nullable super_class     OBJC2_UNAVAILABLE; //父類
    const char * _Nonnull name      OBJC2_UNAVAILABLE; //類名
    long version        OBJC2_UNAVAILABLE; //類的版本信息,默認為0
    long info           OBJC2_UNAVAILABLE; //類信息,供運行期使用的一些位標識
    long instance_size      OBJC2_UNAVAILABLE; //該類的實例變量大小
    struct objc_ivar_list * _Nullable ivars     OBJC2_UNAVAILABLE; //該類的成員變量鏈表
    struct objc_method_list * _Nullable * _Nullable methodLists     OBJC2_UNAVAILABLE; //方法定義的鏈表
    struct objc_cache * _Nonnull cache      OBJC2_UNAVAILABLE; //方法緩存,對象接到一個消息會根據isa指針查找消息對象,這時會在method Lists中遍歷,如果cache了,常用的方法調用時就能夠提高調用的效率。
    struct objc_protocol_list * _Nullable protocols     OBJC2_UNAVAILABLE; //協議鏈表
#endif

} OBJC2_UNAVAILABLE;

可以看到有__OBJC2__OBJC2_UNAVAILABLE標識,在objc2中已經不可用了,但在runtime的源碼中,包含的內容大致是一樣的,只是結構略有不同。為了簡單,我們就以這個定義來理解類和對象的結構是沒問題的。

對象(objc_object)由isa指針和成員變量組成,其中isa指針指向它的類,其中成員變量包括所有父類和自己的成員變量:

Objective-C 對象的結構圖
isa指針
根類的實例變量
倒數第二層父類的實例變量
...
父類的實例變量
類的實例變量

例如:

@interface Father: NSObject{
    int _father;
}
@end
@implementation Father
@end

@interface Child: Father {
    int _child;
}
@end
@implementation Child
@end

int main(int argc, char * argv[]) {
     Child *child = [[Child alloc] init];
    return 0;
}

然后在return 0的一行打斷點,運行程序在斷點停止時,在控制臺輸入p *child,可以看到如下輸出:

(lldb) p *child
(Child) $0 = {
  Father = (_father = 0)
  _child = 0
}
objc_system.png

這就是child對象的結構,從下到上分別是自己到根類的實例變量。

類(objc_class)主要組成:isa指向元類(Meta Class),super_class指向父類、objc_method_list存儲實例方法。類里面和對象一樣也有isa指針,說明類也是個對象,類是元類的實例。

元類(objc_class),在類對象里的isa指針也指向一個objc_class類型的結構體,就是元類對象,結構和類對象一樣,但是objc_method_list存儲的是類方法。

三、消息傳遞(Messaging)

I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging” – that is what the kernal[sic] of Smalltalk is all about... The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.

Alan Kay 曾多次強調 Smalltalk 的核心不是面向對象,面向對象只是 the lesser ideas,消息傳遞才是 the big idea。

在很多語言,比如 C ,調用一個方法其實就是跳到內存中的某一點并開始執行一段代碼。沒有任何動態的特性,因為這在編譯時就決定好了。而在 Objective-C 中,[object foo] 語法并不會立即執行 foo 這個方法的代碼。它是在運行時給 object 發送一條叫 foo 的消息。這個消息,也許會由 object 來處理,也許會被轉發給另一個對象,或者不予理睬假裝沒收到這個消息。多條不同的消息也可以對應同一個方法實現。這些都是在程序運行的時候決定的。

事實上,在編譯時你寫的 Objective-C 函數調用的語法都會被翻譯成一個 C 的函數調用-objc_msgSend。比如,下面兩行代碼就是等價的:

[array insertObject:foo atIndex:5];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 5);

方法的結構(objc_method)

objc_class里的objc_method_list本質是一個有objc_method元素的可變長度的數組。objc_method`的定義如下:

struct objc_method {
    SEL _Nonnull method_name            OBJC2_UNAVAILABLE;
    char * _Nullable method_types       OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp             OBJC2_UNAVAILABLE;                            
}                                       OBJC2_UNAVAILABLE;
  • SEL: 又叫選擇器,表示一個方法的名字。Objective-C在編譯時,會依據每一個方法的名字、參數序列,生成一個唯一的整型標識(Int類型的地址),這個標識就是SEL。
  • IMP: 實際上是一個函數指針,指向方法實現的首地址。定義:id (*IMP)(id, SEL, ...)
  • method_types:表示函數參數及返回值類型的字符串 (見Type Encoding)

objc_msgSend

消息傳遞的關鍵在于 objc_object 中的 isa 指針和 objc_class 中的 class dispatch table。舉objc_msgSend(obj, foo)這個例子來說:

  • 首先,通過 obj 的 isa 指針找到它的 class ;
  • 在 class 的 method list 找 foo ;
  • 如果 class 中沒到 foo,繼續往它的 superclass 中找 ;
  • 旦找到 foo 這個函數,就去執行它的實現IMP .
messaging.gif

但這種實現有個問題,效率低。但一個 class 往往只有 20% 的函數會被經常調用,可能占總調用次數的 80% 。每個消息都需要遍歷一次objc_method_list并不合理。如果把經常被調用的函數緩存下來,那可以大大提高函數查詢的效率。這也就是objc_class中另一個重要成員objc_cache做的事情 —— 再找到 foo 之后,把 foo 的method_name作為 key ,method_imp作為 value 給存起來。當再次收到 foo 消息的時候,可以直接在 cache 里找到,避免去遍歷objc_method_list.

隱藏參數

objc_msgSend找到函數的實現,就會調用函數,并傳遞消息中所有的參數。也傳遞兩個隱藏參數到函數中:

  • 接收對象
  • 方法選擇器

這兩個參數為方法的實現提供了調用者的信息。之所以說是隱藏的,是因為它們在定義方法的源代碼中沒有聲明。它們是在編譯期被插入實現代碼的。

雖然這些參數沒有顯示聲明,但在代碼中仍然可以引用它們。我們可以使用self來引用接收者對象,使用_cmd來引用選擇器。

避免動態綁定

runtime的動態綁定讓我們寫代碼時更具有靈活性,可以在消息的傳遞過程中做一些處理,比如轉發或者交換方法的實現。不過靈活性也帶來了性能上的損耗,畢竟我們需要去查找方法的實現,而不像函數調用來得那么直接。當然,方法的緩存一定程度上解決了這一問題。

如果想要避開這種動態綁定方式,我們可以獲取方法實現的地址,然后像調用函數一樣來直接調用它。特別是當我們需要在一個循環內頻繁地調用一個特定的方法時,通過這種方式可以提高程序的性能。

NSObject類提供了methodForSelector:方法,讓我們可以獲取到方法的指針,然后通過這個指針來調用實現代碼:

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);

前兩個參數傳遞給接收對象(self)的程序和方法選擇器(_cmd)。這些參數在方法語法中是隱藏的,但當該方法當成函數調用時,必須是顯式的。

參考

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容