談Objective-C類成員變量

#我是前言

Objective-C 是一門動態(tài)語言,所以它總是將一些決定工作從編譯延遲到運行時实檀,也就是說只有編譯器是不夠的惶洲,還需要一個運行時系統(tǒng)來執(zhí)行編譯后的代碼。這就是 runtime 存在的意義膳犹,它是 Objective-C 框架的一塊基石。
runtime 有兩個版本:modeen 和 leagcy签则,我們現(xiàn)在使用的是 modern 版的须床。
本文 runtime 源碼為objc4-646.tar.gz版本

在老版本的 runtime 中,如果修改了基類的成員變量布局(比如增加成員變量)渐裂,子類需要重新編譯豺旬。

父類NSObject,子類MyObject成員變量布局

如果蘋果發(fā)布了新的 iOS SDK柒凉,NSObject 增加了幾個成員變量族阅,那么我們原先的代碼將無法運行。因為 MyObject 成員變量布局在編譯時就確定了膝捞,父類新增的成員變量的地址跟子類成員變量的內(nèi)存區(qū)域重疊了坦刀。此時,我們只能重新編譯 MyObject 的代碼蔬咬,程序才能在新版本系統(tǒng)上運行鲤遥。如果 MyObject 存在于別人編寫的靜態(tài)庫,那我們只能希望作者快點發(fā)布新版本了林艘。

新版本后NSObject盖奈,MyObject的成員變量布局

非脆弱[Non-fragile]實例變量是新版 Objective-C 的一個新功能,應(yīng)用于iPhone和64位Mac上狐援。它們提供給框架開發(fā)者更多的靈活性钢坦,且不會失去二進制的兼容性

非脆弱成員變量

#如何尋址成員變量

點開 runtime 的源碼究孕,讓我們找到 ivar 的定義:

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

// 類實例
struct objc_object {
private:
    isa_t isa;
// ...省略
}

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    // ...省略
}

// 類定義
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;  
    // ...省略
}

struct class_data_bits_t {

    // ...省略
public:
    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    // ...省略
}

struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;
    // ...省略
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;
    const method_list_t * baseMethods;
    const protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    const property_list_t *baseProperties;
};

image
  • 每個 OC 類實例實際上都是一個內(nèi)存上指向objc_object結(jié)構(gòu)體的指針,成員變量 isa 有指向objc_class結(jié)構(gòu)體的指針Class cls;
  • class_ro_t結(jié)構(gòu)體中可以找到成員變量const ivar_list_t * ivars爹凹,這個就是存儲類所有成員變量的列表
  • class_ro_t結(jié)構(gòu)體中成員變量const uint8_t * ivarLayout;const uint8_t * weakIvarLayout;的作用可以看一下孫源的這篇博客
@interface MyObject : NSObject {
    NSString *_age;
}
@end

使用 clang -rewrite-objc MyObject.h 將代碼轉(zhuǎn)化成 C++ 實現(xiàn)蚊俺,你可以看到編譯后的 MyObject 實例的內(nèi)存布局:

struct MyObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *__strong _age;
};
struct NSObject_IMPL {
    __unsafe_unretained Class isa;
};
類實例內(nèi)存布局

ivar_list_t 結(jié)構(gòu)體的定義如下:

struct ivar_list_t {
    uint32_t entsize;
    uint32_t count;
    ivar_t first;
};

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    // ...省略
};

我們可以看到ivar_t有名為offset的成員變量,這個就是成員變量在對象中的位置偏移量逛万。在應(yīng)用啟動時泳猬,如果父類size變大時,runtime 會通過修改 offset宇植,更新成員變量的偏移量得封,來正確的找到成員變量的地址。


@interface MyObject : NSObject {
    NSString *_age;
}
@end

@implementation MyObject
- (void)test
{
    self -> _age = @"hhh";
}
@end

使用命令行clang -F -cc1 -S -emit-llvm -fblocks MyObject.m指郁,將代碼編譯成 IR(intermediate representation)忙上。
注意要加-F,好多人的博客里面都少了這個標志闲坎,會報錯疫粥。在 stackoverflow 找到答案。
下面是編譯后的代碼:

@"OBJC_IVAR_$_MyObject._age" = hidden global i64 8, section "__DATA, __objc_ivar", align 8
// ...
%6 = load i64, i64* @"OBJC_IVAR_$_MyObject._age", align 8, !invariant.load !8
%7 = bitcast %0* %5 to i8*
%8 = getelementptr inbounds i8, i8* %7, i64 %6
%9 = bitcast i8* %8 to %1**
store %1* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to %1*), %1** %9, align 8

可以簡化成如下的代碼

int32_t g_ivar_MyClass_age = 8;  // 全局變量
*(NSString *)((uint8_t *)obj + g_ivar_MyObject_age) = @"hhh";

  • 編譯時腰懂,LLVM 為每各類的每一個成員變量定義一個全局變量梗逮,用于存儲該成員變量的偏移量
  • 根據(jù)成員變量的偏移量,可以直接找到成員變量的地址并賦值

這也是為什么結(jié)構(gòu)體ivar_t的成員變量offsetint32_t *類型绣溜,因為保存的是該全局變量的地址慷彤。

#Non Fragile ivars

在前面部分我們已經(jīng)知道該如何尋址成員變量,那么當基類的size變化時怖喻,runtime 是如何更新子類成員變量的offset呢底哗?

在應(yīng)用程序啟動后,main 函數(shù)執(zhí)行之前锚沸,runtime 在加載類的時候跋选,會使用static Class realizeClass(Class cls)函數(shù)對類進行初始化,分配其讀寫數(shù)據(jù)的內(nèi)存哗蜈,返回類的真實結(jié)構(gòu)

/* realizeClass
* Performs first-time initialization on class cls, 
* including allocating its read-write data.
* Returns the real class structure for the class. 
* Locking: runtimeLock must be write-locked by the caller
*/
static Class realizeClass(Class cls) {
    class_rw_t *rw = cls->data();
    //...省略
    if (ro->instanceStart < super_ro->instanceSize) {
        // Superclass has changed size. This class's ivars must move.
        // Also slide layout bits in parallel.
        // This code is incapable of compacting the subclass to 
        //   compensate for a superclass that shrunk, so don't do that.
        class_ro_t *ro_w = make_ro_writeable(rw);
        ro = rw->ro;
        moveIvars(ro_w, super_ro->instanceSize, 
                  mergeLayouts ? &ivarBitmap : nil, 
                  mergeLayouts ? &weakBitmap : nil);
        gdb_objc_class_changed(cls, OBJC_CLASS_IVARS_CHANGED, ro->name);
        layoutsChanged = YES;
    } 
    // ...省略
}

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    const ivar_list_t * ivars;
    // ...省略
};

  • rw 是當前類的可讀數(shù)據(jù)前标,ro 是類的 Ivar Layout,ro 的結(jié)構(gòu)體定義在上面
  • 在初始化類時恬叹,如果父類 ro 的instanceSize比子類的instanceStart大的話候生,那么會調(diào)用moveIvars函數(shù)更新子類的instanceSize以及子類成員變量的偏移量

再讓我們看一下 moveIvars 的源碼:

/***********************************************************************
* moveIvars
* Slides a class's ivars to accommodate the given superclass size.
* Also slides ivar and weak GC layouts if provided.
* Ivars are NOT compacted to compensate for a superclass that shrunk.
* Locking: runtimeLock must be held by the caller.
**********************************************************************/
static void moveIvars(class_ro_t *ro, uint32_t superSize, 
                      layout_bitmap *ivarBitmap, layout_bitmap *weakBitmap)
{
    rwlock_assert_writing(&runtimeLock);

    uint32_t diff;
    uint32_t i;

    assert(superSize > ro->instanceStart);
    diff = superSize - ro->instanceStart;

    if (ro->ivars) {
        // Find maximum alignment in this class's ivars
        uint32_t maxAlignment = 1;
        for (i = 0; i < ro->ivars->count; i++) {
            ivar_t *ivar = ivar_list_nth(ro->ivars, i);
            if (!ivar->offset) continue;  // anonymous bitfield

            uint32_t alignment = ivar->alignment();
            if (alignment > maxAlignment) maxAlignment = alignment;
        }

        // Compute a slide value that preserves that alignment
        uint32_t alignMask = maxAlignment - 1;
        if (diff & alignMask) diff = (diff + alignMask) & ~alignMask;

        // Slide all of this class's ivars en masse
        for (i = 0; i < ro->ivars->count; i++) {
            ivar_t *ivar = ivar_list_nth(ro->ivars, i);
            if (!ivar->offset) continue;  // anonymous bitfield

            uint32_t oldOffset = (uint32_t)*ivar->offset;
            uint32_t newOffset = oldOffset + diff;
            *ivar->offset = newOffset;

            if (PrintIvars) {
                _objc_inform("IVARS:    offset %u -> %u for %s (size %u, align %u)", 
                             oldOffset, newOffset, ivar->name, 
                             ivar->size, ivar->alignment());
            }
        }

        // Slide GC layouts
        uint32_t oldOffset = ro->instanceStart;
        uint32_t newOffset = ro->instanceStart + diff;

        if (ivarBitmap) {
            layout_bitmap_slide(ivarBitmap, 
                                oldOffset >> WORD_SHIFT, 
                                newOffset >> WORD_SHIFT);
        }
        if (weakBitmap) {
            layout_bitmap_slide(weakBitmap, 
                                oldOffset >> WORD_SHIFT, 
                                newOffset >> WORD_SHIFT);
        }
    }

    *(uint32_t *)&ro->instanceStart += diff;
    *(uint32_t *)&ro->instanceSize += diff;

    if (!ro->ivars) {
        // No ivars slid, but superclass changed size. 
        // Expand bitmap in preparation for layout_bitmap_splat().
        if (ivarBitmap) layout_bitmap_grow(ivarBitmap, ro->instanceSize >> WORD_SHIFT);
        if (weakBitmap) layout_bitmap_grow(weakBitmap, ro->instanceSize >> WORD_SHIFT);
    }
}
  • 首先計算 superSize 與 instanceStart 之間的差值 diff
  • 得到結(jié)構(gòu)體中最大的成員變量的size:maxAlignment, 然后賦值:alignMask = maxAlignment - 1
  • 比較 diff 和 alignMask绽昼,通過算法 if (diff & alignMask) diff = (diff + alignMask) & ~alignMask; 對diff重新賦值

編譯器在給結(jié)構(gòu)體開辟空間時唯鸭,首先找到結(jié)構(gòu)體中最大的基本數(shù)據(jù)類型,然后尋找內(nèi)存地址能是該基本數(shù)據(jù)類型的整倍的位置硅确,作為結(jié)構(gòu)體的首地址目溉。將這個最寬的基本數(shù)據(jù)類型的大小作為對齊模數(shù)明肮。
為結(jié)構(gòu)體的一個成員開辟空間之前,編譯器首先檢查預(yù)開辟空間的首地址相對于結(jié)構(gòu)體首地址的偏移是否是本成員的整數(shù)倍缭付,若是柿估,則存放本成員,反之陷猫,則在本成員和上一個成員之間填充一定的字節(jié)秫舌,以達到整數(shù)倍的要求,也就是將預(yù)開辟空間的首地址后移幾個字節(jié)绣檬。了解更多可以看這篇博客

  • 更新成員變量的 offset足陨,ivar.newOffset = diff + ivar.oldOffset
  • 更新子類 ro 的 instanceStart 和 instanceSize,ro.newinstanceStart = ro.oldinstanceStart + diff娇未,ro.newinstanceSize = ro.oldinstanceSize + diff
  • 當父類變大時會調(diào)用該函數(shù)來移動子類ivar墨缘,當父類變小時則子類ivar不變化

通過這個函數(shù),即使父類size變大了零抬,我們還是可以通過子類的 ro.instanceStart + ivar.offset 訪問到成員變量

#不能動態(tài)添加成員變量

在 runtime 中有一個函數(shù) class_addIvar()可以為類添加成員變量, 下面是該方法的一部分注釋:

his function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
The class must not be a metaclass. Adding an instance variable to a metaclass is not supported.

上面的大致意思是該函數(shù)只能在類注冊之前使用镊讼,且不能為元類添加成員變量。

讓我們設(shè)想一下如果 OC 允許動態(tài)增加成員變量:

@interface Father : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *age;
@end

@interface Son : Father
@property (nonatomic, copy) NSArray *toys;
@end

當Father初始化之后平夜,instanceStart蝶棋,instanceSize,offset已經(jīng)確定褥芒。
為 Father 添加新的成員變量 sex嚼松,則使用 Son 的實例對象 son 會出錯誤,因為 son.instanceStart < Father.instanceSize锰扶,即 father 成員變量的 sex 的內(nèi)存區(qū)域會跟 son 的一部分重合

我們有時會在類目中動態(tài)的為類添加關(guān)聯(lián)對象(添加對象),那這是為什么呢?
具體的你可以看一下我的另一篇博客 談Objective-C關(guān)聯(lián)對象寝受。
這里我簡單解釋一下:關(guān)聯(lián)對象被保存在一個靜態(tài)的map 中坷牛,以類實例的指針地址為映射,而不是保存在類實例的結(jié)構(gòu)體中很澄。

#引用

Objective-C類成員變量深度剖析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末京闰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子甩苛,更是在濱河造成了極大的恐慌蹂楣,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讯蒲,死亡現(xiàn)場離奇詭異痊土,居然都是意外死亡,警方通過查閱死者的電腦和手機墨林,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門赁酝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來犯祠,“玉大人,你說我怎么就攤上這事酌呆『庠兀” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵隙袁,是天一觀的道長痰娱。 經(jīng)常有香客問我,道長菩收,這世上最難降的妖魔是什么梨睁? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮坛梁,結(jié)果婚禮上而姐,老公的妹妹穿的比我還像新娘。我一直安慰自己划咐,他們只是感情好拴念,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著褐缠,像睡著了一般政鼠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上队魏,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天公般,我揣著相機與錄音,去河邊找鬼胡桨。 笑死官帘,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的昧谊。 我是一名探鬼主播刽虹,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呢诬!你這毒婦竟也來了涌哲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤尚镰,失蹤者是張志新(化名)和其女友劉穎阀圾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狗唉,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡初烘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片账月。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡综膀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出局齿,到底是詐尸還是另有隱情剧劝,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布抓歼,位于F島的核電站讥此,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏谣妻。R本人自食惡果不足惜萄喳,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蹋半。 院中可真熱鬧他巨,春花似錦、人聲如沸减江。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽辈灼。三九已至份企,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間巡莹,已是汗流浹背司志。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留降宅,地道東北人骂远。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像腰根,于是被迫代替她去往敵國和親吧史。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容