谈Objective-C类成员变量

#我是前言

Objective-C 是一门动态语言,所以它总是将一些决定工作从编译延迟到运行时,也就是说只有编译器是不够的,还需要一个运行时系统来执行编译后的代码。这就是 runtime 存在的意义,它是 Objective-C 框架的一块基石。
runtime 有两个版本:modeen 和 leagcy,我们现在使用的是 modern 版的。
本文 runtime 源码为objc4-646.tar.gz版本

在老版本的 runtime 中,如果修改了基类的成员变量布局(比如增加成员变量),子类需要重新编译。

父类NSObject,子类MyObject成员变量布局

如果苹果发布了新的 iOS SDK,NSObject 增加了几个成员变量,那么我们原先的代码将无法运行。因为 MyObject 成员变量布局在编译时就确定了,父类新增的成员变量的地址跟子类成员变量的内存区域重叠了。此时,我们只能重新编译 MyObject 的代码,程序才能在新版本系统上运行。如果 MyObject 存在于别人编写的静态库,那我们只能希望作者快点发布新版本了。

新版本后NSObject,MyObject的成员变量布局

非脆弱[Non-fragile]实例变量是新版 Objective-C 的一个新功能,应用于iPhone和64位Mac上。它们提供给框架开发者更多的灵活性,且不会失去二进制的兼容性

非脆弱成员变量

#如何寻址成员变量

点开 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 类实例实际上都是一个内存上指向objc_object结构体的指针,成员变量 isa 有指向objc_class结构体的指针Class cls;
  • class_ro_t结构体中可以找到成员变量const ivar_list_t * ivars,这个就是存储类所有成员变量的列表
  • class_ro_t结构体中成员变量const uint8_t * ivarLayout;const uint8_t * weakIvarLayout;的作用可以看一下孙源的这篇博客
@interface MyObject : NSObject {
    NSString *_age;
}
@end

使用 clang -rewrite-objc MyObject.h 将代码转化成 C++ 实现,你可以看到编译后的 MyObject 实例的内存布局:

struct MyObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSString *__strong _age;
};
struct NSObject_IMPL {
    __unsafe_unretained Class isa;
};
类实例内存布局

ivar_list_t 结构体的定义如下:

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的成员变量,这个就是成员变量在对象中的位置偏移量。在应用启动时,如果父类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 为每各类的每一个成员变量定义一个全局变量,用于存储该成员变量的偏移量
  • 根据成员变量的偏移量,可以直接找到成员变量的地址并赋值

这也是为什么结构体ivar_t的成员变量offsetint32_t *类型,因为保存的是该全局变量的地址。

#Non Fragile ivars

在前面部分我们已经知道该如何寻址成员变量,那么当基类的size变化时,runtime 是如何更新子类成员变量的offset呢?

在应用程序启动后,main 函数执行之前,runtime 在加载类的时候,会使用static Class realizeClass(Class cls)函数对类进行初始化,分配其读写数据的内存,返回类的真实结构

/* 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 是当前类的可读数据,ro 是类的 Ivar Layout,ro 的结构体定义在上面
  • 在初始化类时,如果父类 ro 的instanceSize比子类的instanceStart大的话,那么会调用moveIvars函数更新子类的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
  • 得到结构体中最大的成员变量的size:maxAlignment, 然后赋值:alignMask = maxAlignment - 1
  • 比较 diff 和 alignMask,通过算法 if (diff & alignMask) diff = (diff + alignMask) & ~alignMask; 对diff重新赋值

编译器在给结构体开辟空间时,首先找到结构体中最大的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为对齐模数。
为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。了解更多可以看这篇博客

  • 更新成员变量的 offset,ivar.newOffset = diff + ivar.oldOffset
  • 更新子类 ro 的 instanceStart 和 instanceSize,ro.newinstanceStart = ro.oldinstanceStart + diff,ro.newinstanceSize = ro.oldinstanceSize + diff
  • 当父类变大时会调用该函数来移动子类ivar,当父类变小时则子类ivar不变化

通过这个函数,即使父类size变大了,我们还是可以通过子类的 ro.instanceStart + ivar.offset 访问到成员变量

#不能动态添加成员变量

在 runtime 中有一个函数 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.

上面的大致意思是该函数只能在类注册之前使用,且不能为元类添加成员变量。

让我们设想一下如果 OC 允许动态增加成员变量:

@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已经确定
为 Father 添加新的成员变量 sex,则使用 Son 的实例对象 son 会出错误,因为 son.instanceStart < Father.instanceSize,即 father 成员变量的 sex 的内存区域会跟 son 的一部分重合

我们有时会在类目中动态的为类添加关联对象(添加对象),那这是为什么呢?
具体的你可以看一下我的另一篇博客 谈Objective-C关联对象
这里我简单解释一下:关联对象被保存在一个静态的map 中,以类实例的指针地址为映射,而不是保存在类实例的结构体中。

#引用

Objective-C类成员变量深度剖析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容