神經(jīng)病院objc runtime入院考試

本文的題目源自2014年11月1日,sunny分享的objc runtime。

在一次看到這四個題目的時候,我居然很巧妙的避開了所有的正確答案,這讓我對自己的技術(shù)水平產(chǎn)生了深深的懷疑。更讓我感到絕望的是有些題目我居然看參考答案都無法理解?。?!時隔多年,當(dāng)我回過頭來繼續(xù)看這些題目的時候,我發(fā)現(xiàn)我居然能夠理解了,這種能夠看到自己進(jìn)步的感覺,真好。由于前三道題目比較簡單,而且太多的博客講解了,在此不做細(xì)談,重點來看第四題。

(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)滿足了構(gòu)成一個objc對象的全部要求(首地址指向ClassObject),遂能夠正常走消息機(jī)制;
由于這個人造的對象在棧上,而取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對象其實是一個結(jié)構(gòu)體,它里面的第一個變量就是一個指向類地址的isa指針。而且我們的類也是一個對象,他繼承自objc_object。同時它的isa指針指向meta class。下面看這張經(jīng)典的圖。

isa指針

圖中虛線表示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的對象?

答:一個首地址指向類地址的結(jié)構(gòu)體。

注:如果還想繼續(xù)深入可以參考霜神這片文章

對象怎么去調(diào)用一個方法?

上面我們說到了,每個實例對象都有一個isa指針指向類對象。
為什么要指向類對象呢?因為所有的實例方法列表都存儲在類對象中,類方法存儲在元類中。
當(dāng)我們?nèi)フ{(diào)用一個實例方法時,是通過實例對象的isa找到類,然后再去尋找方法的實現(xiàn)。
網(wǎng)上有很多的文章可供參考,在此不做細(xì)談。

對象怎么去獲取一個屬性的值?

我們已經(jīng)知道了OC的對象其實是一個結(jié)構(gòu)體,且實例對象的方法存儲在類對象中,類對象和元類對象在全局中只有一份,所以實例對象的屬性肯定是存儲在實例對象中。

下面我們來證明:
首先:屬性 = 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;
測試代碼1

在這里你可以看到,我把一個CharClass的對象強(qiáng)轉(zhuǎn)為IntClass的對象并且強(qiáng)制獲取的他value1,沒有奔潰,而且有值為513。
這個513是不是有點眼熟,剛好等于256*2+1,也就是說他去取值的時候,剛好把char類型的value2和value1的值當(dāng)做了一個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;
};
測試代碼2

你會發(fā)現(xiàn)結(jié)果和上面一直,但是如果你注釋掉第52行代碼,你會發(fā)現(xiàn)value的值為1。

由此我們可以得出以下結(jié)論:
1.實例變量存儲在實例對象(結(jié)構(gòu)體)中。
2.實例變量的獲取方式為實例對象的地址+offset。
如圖所示:

示意圖1

因為char類型占一個字節(jié),而一個int占4個字節(jié)。所以,當(dāng)我們把isa指向IntClass或者用實例變量來強(qiáng)制訪問IntClass的value1值時,其實是把value1和value2當(dāng)做了一個整體來讀取,即*(int *)(charObject+8)。注:一個指針占8個字節(jié),Class其實是一個struct objc_class的指針。


示意圖2

這個時候再回到最開始的問題,也就是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命令重寫看看。

示意圖3

我們可以看到viewDidLoad方法中有兩個隱藏參數(shù):self,_cmd在棧底,然后是super標(biāo)識符的構(gòu)成,self,ViewController類。
所以此時的棧分布圖如下:


示意圖4

注:引用自霜神博客。

證明圖如下所示:


示意圖5

所以,此時的obj對象的name屬性就是cls的地址再偏移8個字節(jié),也就是剛好是self的地址。所以此時輸出的是self的信息。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容