本文的題目源自2014年11月1日合溺,sunny分享的objc runtime浆洗。
在一次看到這四個題目的時候流炕,我居然很巧妙的避開了所有的正確答案,這讓我對自己的技術水平產(chǎn)生了深深的懷疑铅匹。更讓我感到絕望的是有些題目我居然看參考答案都無法理解Q荷蕖!包斑!時隔多年流礁,當我回過頭來繼續(xù)看這些題目的時候,我發(fā)現(xiàn)我居然能夠理解了罗丰,這種能夠看到自己進步的感覺神帅,真好。由于前三道題目比較簡單萌抵,而且太多的博客講解了找御,在此不做細談,重點來看第四題绍填。
(4) 下面的代碼會霎桅?Compile Error / Runtime Crash / NSLog…?
@interface Sark : NSObject @property (nonatomic, copy) NSString *name; @end @implementation Sark - (void)speak { NSLog(@"my name's %@", self.name); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak]; } @end
答案
(4)編譯運行正常,輸出ViewController中的self對象讨永。 編譯運行正常滔驶,調(diào)用了-speak方法,由于
id cls = [Sark class]; void *obj = &cls;
obj已經(jīng)滿足了構成一個objc對象的全部要求(首地址指向ClassObject)卿闹,遂能夠正常走消息機制揭糕;
由于這個人造的對象在棧上,而取self.name的操作本質(zhì)上是self指針在內(nèi)存向高位地址偏移(32位下一個指針是4字節(jié))锻霎,按viewDidLoad執(zhí)行時各個變量入棧順序從高到底為(self, _cmd, self.class, self, obj)(前兩個是方法隱含入?yún)⒅牵S后兩個為super調(diào)用的兩個壓棧參數(shù)),遂棧低地址的obj+4取到了self旋恼。
這道題考察的重點是:
1.什么是一個OC的對象吏口?
2.對象怎么去調(diào)用一個方法?
3.對象怎么去獲取一個屬性的值?
客官锨侯,坐下來喝杯茶,且聽我徐徐道來冬殃。
什么是一個OC的對象?
在OC2.0中囚痴,對象的定義是:
typedef struct objc_class *Class;
typedef struct objc_object *id;
@interface Object {
Class isa;
}
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
isa_t isa;
}
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}
可以看到我們所使用的OC對象其實是一個結構體,它里面的第一個變量就是一個指向類地址的isa指針审葬。而且我們的類也是一個對象深滚,他繼承自objc_object。同時它的isa指針指向meta class涣觉。下面看這張經(jīng)典的圖痴荐。
圖中虛線表示isa指針,實線表示superclass指針官册。
1.Root class (class)其實就是NSObject生兆,NSObject是沒有超類的,所以Root class(class)的superclass指向nil膝宁。
2.每個Class都有一個isa指針指向自身的Meta class鸦难。
3.Root class(meta)的superclass指向Root class(class),也就是NSObject员淫,形成一個回路合蔽。
4.每個Meta class的isa指針都指向Root class (meta)。
這個時候再來回歸我們的問題介返,什么是一個OC的對象?
答:一個首地址指向類地址的結構體拴事。
注:如果還想繼續(xù)深入可以參考霜神這片文章
對象怎么去調(diào)用一個方法?
上面我們說到了圣蝎,每個實例對象都有一個isa指針指向類對象刃宵。
為什么要指向類對象呢?因為所有的實例方法列表都存儲在類對象中徘公,類方法存儲在元類中组去。
當我們?nèi)フ{(diào)用一個實例方法時,是通過實例對象的isa找到類步淹,然后再去尋找方法的實現(xiàn)从隆。
網(wǎng)上有很多的文章可供參考,在此不做細談缭裆。
對象怎么去獲取一個屬性的值键闺?
我們已經(jīng)知道了OC的對象其實是一個結構體,且實例對象的方法存儲在類對象中澈驼,類對象和元類對象在全局中只有一份辛燥,所以實例對象的屬性肯定是存儲在實例對象中。
下面我們來證明:
首先:屬性 = Ivar + get +set;
@interface IntClass : NSObject{
@public
int value1;
}
@end
@implementation IntClass
@end
@interface CharClass : NSObject {
@public
char value1;
char value2;
}
@end
我們定義了兩個類IntClass和CharClass挎塌,他們分別有一個int型實例變量value1和兩個char型實例變量value1徘六,value2。
接下來看我的測試代碼:
CharClass *charObject = [CharClass new];
charObject->value1 = 1;
charObject->value2 = 2;
int value = ((IntClass *)charObject)->value1;
在這里你可以看到榴都,我把一個CharClass的對象強轉為IntClass的對象并且強制獲取的他value1待锈,沒有奔潰,而且有值為513嘴高。
這個513是不是有點眼熟竿音,剛好等于256*2+1,也就是說他去取值的時候拴驮,剛好把char類型的value2和value1的值當做了一個int類型來讀取春瞬。
這個時候我們再把實例變量換成屬性。
CharClass *charObject = [CharClass new];
charObject.value1 = 1;
charObject.value2 = 2;
struct object *obj = (__bridge struct object *)charObject;
obj->isa = [IntClass class];
int value = ((__bridge IntClass *)obj).value1;
struct object
的定義如下:
struct object {
Class isa;
};
你會發(fā)現(xiàn)結果和上面一直套啤,但是如果你注釋掉第52行代碼宽气,你會發(fā)現(xiàn)value的值為1。
由此我們可以得出以下結論:
1.實例變量存儲在實例對象(結構體)中潜沦。
2.實例變量的獲取方式為實例對象的地址+offset抹竹。
如圖所示:
因為char類型占一個字節(jié),而一個int占4個字節(jié)止潮。所以窃判,當我們把isa指向IntClass或者用實例變量來強制訪問IntClass的value1值時,其實是把value1和value2當做了一個整體來讀取喇闸,即*(int *)(charObject+8)袄琳。注:一個指針占8個字節(jié),Class其實是一個struct objc_class的指針燃乍。
這個時候再回到最開始的問題唆樊,也就是sunny的考試題。
@interface Sark : NSObject @property (nonatomic, copy) NSString *name; @end @implementation Sark - (void)speak { NSLog(@"my name's %@", self.name); } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak]; } @end
很顯然obj滿足了一個OC的基本條件刻蟹,擁有指向類對象的指針逗旁。
但是此時這個類對象有點特殊,因為他沒有指向堆區(qū)舆瘪,而是指向了棧區(qū)片效。堆是從低地址向高地址生長,而棧是從高地址向低地址生長英古。因此obj+8等于向棧底偏移8個單位淀衣。那么這個時候他指向了哪里呢?我們用clang命令重寫看看召调。
我們可以看到viewDidLoad方法中有兩個隱藏參數(shù):self膨桥,_cmd在棧底蛮浑,然后是super標識符的構成,self只嚣,ViewController類沮稚。
所以此時的棧分布圖如下:
注:引用自霜神博客。
證明圖如下所示:
所以册舞,此時的obj對象的name屬性就是cls的地址再偏移8個字節(jié)蕴掏,也就是剛好是self的地址。所以此時輸出的是self的信息环础。