-
一個 Objective-C 對象的內存結構是怎樣的?
如果把類的實例看成一個C語言的結構體(struct),它首先包含的是一個 isa 指針,而類的其它成員變量依次排列在結構體中。排列順序如下圖所示:
為了驗證該說法,我們在Xcode中新建一個工程,在main.m中運行如下代碼:
#import <UIKit/UIKit.h> @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]; @autoreleasepool { // ... } }
我們將斷點下在 @autoreleasepool 處,然后在Console中輸入p *child,則可以看到Xcode輸出如下內容,這與我們上面的說法一致。
(lldb) p *child (Child) $0 = { (Father) Father = { (NSObject) NSObject = { (Class) isa = Child } (int) _father = 0 } (int) _child = 0 }
因為對象在內存中的排布可以看成一個結構體,該結構體的大小並不能動態變化。所以無法在運行時動態給對象增加成員變量。
注:需要特別說明一下,通過 objc_setAssociatedObject 和 objc_getAssociatedObject方法可以變相地給對象增加成員變量,但由於實現機制不一樣,所以並不是真正改變了對象的內存結構。
-
Objective-C 對象內存結構中的 isa 指針是用來做什么的,有什么用?
Objective-C 是一門面向對象的編程語言。每一個對象都是一個類的實例。在 Objective-C 語言的內部,每一個對象都有一個名為 isa 的指針,指向該對象的類。每一個類描述了一系列它的實例的特點,包括成員變量的列表,成員函數的列表等。每一個對象都可以接受消息,而對象能夠接收的消息列表是保存在它所對應的類中。
在 Xcode 中按Shift + Command + O, 然后輸入 objc/NSObject.h,可以打開 NSObject 的定義頭文件,通過頭文件我們可以看到,NSObject 就是一個包含 isa 指針的結構體,如下圖所示:
按照面向對象語言的設計原則,所有事物都應該是對象(嚴格來說 Objective-C 並沒有完全做到這一點,因為它有象 int, double 這樣的簡單變量類型,而 Swift 語言,連 int 變量也是對象)。在 Objective-C 語言中,每一個類實際上也是一個對象。每一個類也有一個名為 isa 的指針。每一個類也可以接受消息,例如代碼[NSObject alloc],就是向 NSObject 這個類發送名為alloc消息。
在 Xcode 中按Shift + Command + O, 然后輸入 runtime.h,可以打開 Class 的定義頭文件,通過頭文件我們可以看到,Class 也是一個包含 isa 指針的結構體,如下圖所示。(圖中除了 isa 外還有其它成員變量,但那是為了兼容非 2.0 版的 Objective-C 的遺留邏輯,大家可以忽略它。)
因為類也是一個對象,那它也必須是另一個類的實列,這個類就是元類 (metaclass)。元類保存了類方法的列表。當一個類方法被調用時,元類會首先查找它本身是否有該類方法的實現,如果沒有,則該元類會向它的父類查找該方法,直到一直找到繼承鏈的頭。
元類 (metaclass) 也是一個對象,那么元類的 isa 指針又指向哪里呢?為了設計上的完整,所有的元類的 isa 指針都會指向一個根元類 (root metaclass)。根元類 (root metaclass) 本身的 isa 指針指向自己,這樣就行成了一個閉環。上面提到,一個對象能夠接收的消息列表是保存在它所對應的類中的。在實際編程中,我們幾乎不會遇到向元類發消息的情況,那它的 isa 指針在實際上很少用到。不過這么設計保證了面向對象概念在 Objective-C 語言中的完整,即語言中的所有事物都是對象,都有 isa 指針。
我們再來看看繼承關系,由於類方法的定義是保存在元類 (metaclass) 中,而方法調用的規則是,如果該類沒有一個方法的實現,則向它的父類繼續查找。所以,為了保證父類的類方法可以在子類中可以被調用,所以子類的元類會繼承父類的元類,換而言之,類對象和元類對象有着同樣的繼承關系。
我很想把關系說清楚一些,但是這塊兒確實有點繞,我們還是來看圖吧,很多時候圖象比文字表達起來更為直觀。下面這張圖或許能夠讓大家對 isa 和繼承的關系清楚一些:
我們可以從圖中看出:
NSObject 的類中定義了實例方法,例如 - (id)init 方法 和 - (void)dealloc 方法。
NSObject 的元類中定義了類方法,例如 + (id)alloc 方法 和 + (void)load 、+ (void)initialize 方法。
NSObject 的元類繼承自 NSObject 類,所以 NSObject 類是所有類的根,因此 NSObject 中定義的實例方法可以被所有對象調用,例如 - (id)init 方法 和 - (void)dealloc 方法。
NSObject 的元類的 isa 指向自己。
isa swizzling 的應用:
系統提供的 KVO 的實現,就利用了動態地修改 isa 指針的值的技術。在 蘋果的文檔中可以看到如下描述:
Key-Value Observing Implementation Details Automatic key-value observing is implemented using a technique called isa-swizzling. The isa pointer, as the name suggests, points to the object’s class which maintains a dispatch table. This dispatch table essentially contains pointers to the methods the class implements, among other data. When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class. As a result the value of the isa pointer does not necessarily reflect the actual class of the instance. You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.