動態實例變量:解決脆弱的基類問題

原文鏈接:http://www.cocoawithlove.com/2010/03/dynamic-ivars-solving-fragile-base.html 
作者:ani_di 
版權所有,轉載務必保留此鏈接 http://blog.csdn.net/ani_di

動態實例變量:解決脆弱的基類問題

在現代Objective-C運行時(iPhone OS或64位Mac OS X),你可以動態的添加實例變量到一個類中而不用事先定義。動態變量還可以幫助數據隱藏或抽象,甚至可以解決子類和基類擁有相同的實例變量名而不會引用相同底層數據的混亂情況。

簡介

本文是有關Objective-C中的動態實例變量。動態實例變量用於解決脆弱的基類問題,即實例變量的佈局。

脆弱的基類問題是基類的很小修改就會破壞子類。實例變量的佈局決定了你不能在基類中添加變量而子類不重新編譯。

添加實例變量需要在對象指針位置增加變量的偏移,而子類的實例變量總是位於父類後面,而且子類變量的訪問通常是在編譯時就確定好了,這意味着你不能修改父類的大小,除非子類重新編譯以重新計算引用。

大多數面向對象的語言都有類似的問題,添加實例變量幾乎不可能在二進制上兼容。

Objectives-C結合現代Objective-C運行時是少數幾個在編譯語言環境去解決此問題。

動態不是意味着“任何時候”:動態只的是,實例變量的佈局在編譯時沒有確定。事實上,這裏的動態是從子類的角度(對於基類,實例變量同之前一樣)。 實例變量只能在類關聯註冊之前(即還沒有任何類實例創建)。參考蘋果文檔class addlvar

脆弱的基類的實例變量佈局

我們先看一個由佈局引起錯誤的例子。

在基類添加實例變量會破壞所有子類

此例子是一個動態鏈接庫(比如蘋果寫的Cocoa)中允許子類繼承:

    @interface LibraryBaseObject : NSObject
    {
        NSString *baseObjectIVar;
    }
    @end

庫的使用者創建他自己的子類

    @interface UserSubObject : LibraryBaseObject
    {
        NSString * userSubObjectIVar;
    }
    @end

這工作的很好,直到庫作者想在 LibraryBaseObject 添加新特性

    @interface LibraryBaseObject : NSObject
    {
        NSString *baseObjectIVar;
        id newFeatureObject;
    }
    @end

傳統的方法會破壞所有已存在的子類,因爲子類沒有分配足夠的空間容納 userSubObjectIVar,它在編譯時決定的偏移等於 NSObject + LibraryBaseObject。

簡單的用新的頭文件重新編譯,所有的偏移就可以正確更新。否則沒有其他辦法。

Greg Parker 的 Hamster Emporium:[objc explain]有個很不錯的文章,Non-fragile ivars裏面的圖表展示了實例變量佈局的問題

以前解決脆弱的基類的方法

最常見的方法是這樣定義你的基類

    @interface LibraryBaseObject : NSObject
    {
        id private;
    }
    @end

把所有的數據保存在private類中,這樣 LibraryBaseObject 就只佔用一個指針的大小。

當然,這種方法有三個問題:

1. 你必須一開始就添加private 
2. 兩級解引用的性能問題 
3. 每次使用 private前,都要強制轉換到正確的class

修復方法:讓編譯時的值成爲動態的值

之前,我所說的訪問實例變量的方法是:

1. 在對象指針前加上實例變量的偏移,得到絕對偏移  
2. 從絕對偏移指向的內存中解引用

對於程序員,這種方法在Algol之後都不是新內容:大部分編譯語言訪問struct, record或instance都是如此。

顯然,要解決此問題,一些東西要在運行時改變。Objective-C運行時的解決方法是“基類實例變量區域的大小”可以在運行時查詢。

現代Objective-C運行時訪問實例變量變爲下面幾步:

1. 在對象指針加上子類實例變量的偏移
2. 加上父類實例變量區域
3. 解引用

一但這樣完成,基類的實例變量區域就可以自由增長,子類的偏移隨着變動。

所有實例變量都是動態的:程序中所有的實例變量都遵循這樣規則,這意味着現代Objective-C運行時的偏移從未在編譯時確定

性能影響

你可能擔心,這種改變會讓一個常見的任務(訪問變量)變慢。

是的,加上額外偏移會減慢一些,但非常小。

編譯器已對此做了優化,父類區域大小會保存在寄存器中,而且相同變量一般不會重複計算(譯註:此處省略一些原文CPU計算)

Synthesizing變量

下一個問題是,我們如何利用這種優勢來給一個已存在的類添加實例變量?你可以使用 synthesized property。他可以在實現中創建出實例變量而聲明中卻不存在。

比如,開始是這樣的

    @interface MyIvarlessObject : NSObject
    {
    }
    @end

你可以通過修改聲明動態添加變量

    @interface MyIvarlessObject : NSObject
    {
    }
    @property (nonatomic, copy) NSString *myProperty;
    @property (nonatomic, copy) NSString *anotherProperty;
    @end

並在實現中添加下面的代碼

    @synthesize myProperty=myIvar; // a dynamic ivar named myIvar will be generated
    @synthesize anotherProperty; // a dynamic ivar named anotherProperty will be generated

由於沒有匹配到myIvar或 anotherProperty,這兩個實例變量將動態創建。

@synthesize 語句相當於聲明。現在你可以這樣寫

    - (id)init
    {
        self = [super init];
        if (self)
        {
            myIvar = [[NSString alloc] initWithString:@"someString"];
            anotherProperty = [[NSString alloc] initWithString:@"someOtherString"];
        }
        return self;
    }

如果你想隱藏 property 的聲明或是不想改變頭文件,你可以在實現中增加一個私有 category 在實現裏面。像這樣:

    @interface MyIvarlessObject ()
    @property (nonatomic, copy) NSString *myProperty;
    @property (nonatomic, copy) NSString *anotherProperty;
    @end

這需要放在 @implement 上面。

名字相同的多個實例變量

想象下面的基類

    @interface BaseObject : NSObject
    {
    }
    @property (nonatomic, copy) NSString *propertyOne;
    @end

    @implementation BaseObject
    @synthesize propertyOne=myIvar;
    @end

子類

    @interface SubObject : BaseObject
    {
    }
    @property (nonatomic, copy) NSString *propertyTwo;
    @end

    @implementation SubObject
    @synthesize propertyTwo=myIvar;
    @end

兩個類都 @synthesize 變量 myIvar。

有些令人奇怪,兩個類裏面的myIvar並不相同——他們是不同的實例變量,在內存中的位置不同。

爲了解決種問題:如果 @synthesize 聲明的實例變量與子類衝突,則基類的實例變量依然脆弱。爲了支持這一想法,@synthesize 聲明的總是當中 @private。

結論

通常你不用擔心實例變量的內存佈局。只需要在編寫或升級動態庫,向前兼容時注意。

動態實例變量是“現代”運行時的特性;32位的Mac OS X不支持。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章