Objective-C对象内存分布是怎样确定的

对于一个类的实例变量来说,我们常说他的内存分布是isa + ivars。为什么内存是这样分布的?他是怎样确定的?

本文采用源码为当前最新:objc4-756.2libmalloc-166.251.2


开胃菜

比如有这么段代码:

@interface A : NSObject
@property (nonatomic, assign) BOOL b;
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) char c;
@end

@implementation A
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        A *a = [A new];
        a.b = YES;
        a.name = @"test";
        a.c = 'p';
    }
    return 0;
}

可以看到,变量a存储的内容有如下规律:

  1. 标记1a->isa数据相同
  2. 标记2a.b数据相同
  3. 标记3a.c数据相同
  4. 标记4a.name数据相同

这就是文章开头说的isa + ivars

标记3后面的空字节是匿名成员变量,为了内存对齐,bc分别是BOOLchar类型,都只占1个字节,为了节省内存两者相邻。name是个指针,占8个字节放在后面。

内存对齐

再来看内存占用情况:

可以看到,A类的实例变量占用内存为24字节(isa 8字节 + b 1字节 + c 1字节 + 匿名成员变量 6字节 + name 8字节),而变量a实际申请了32字节的内存。

如果不存在内存对齐,变量a占用内存应为isa 8字节 + b 1字节 + name 8字节 + c1字节 = 18字节,之所以添加6字节匿名成员变量,这与cpu的数据总线相关:

  1. 对于64位cpu来说,一次可交换64bit数据,即8字节
  2. 对于32位cpu来说,一次可交换32bit数据,即4字节

objc-runtime-new.h中有如下代码:

struct objc_class : objc_object {
    ...

    // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() {
        assert(isRealized());
        return data()->ro->instanceSize;
    }

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }

    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;
        return size;
    }

    ...
}

instanceSize函数中的参数extraBytes为0,unalignedInstanceSize函数中的data()->ro->instanceSize为当前实例变量占用内存大小。显然,当最终size小于16时,会给size赋值成16,所以oc对象最小占用内存就是16字节(经word_align函数计算后仍为16)。

再来看word_align函数,他在objc-os.h中:

在64位cpu下,WORD_MASK的值为7,32位cpu的值为3(x + WORD_MASK) & ~WORD_MASK又代表什么意思?

对于(a + (b -1)) & ~(b - 1)来说,最终得到就是大于等于a最小b的倍数,举个例子:

a = 7
b = 3
(a + (b -1)) & ~(b - 1) = 9

大于等于73的最小倍数就是9,所以(x + WORD_MASK) & ~WORD_MASK在64位cpu上总是8的倍数,在32位cpu上总是4的倍数。

objc-class.mm中有如下代码:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

这与上边的推理相互印证。

申请内存会调用libmalloc中的代码:

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

可知,SHIFT_NANO_QUANTUM为4,NANO_REGIME_QUANTA_SIZE1<<416

这里又出现一个新算法:

k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;                           

其实与上文中的(a + (b -1)) & ~(b - 1)算法相同,这里也是为了得到大于等于size的最小16的倍数。

由于变量a实际占用24字节,并不是16的倍数,所以此处得到32个字节。

运行时注册类与成员变量验证

为了更好地理解这一过程,这里用runtime注册A类,以及添加成员变量:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class A = objc_allocateClassPair(NSObject.class, "A", 0);
        class_addIvar(A, "_b", sizeof(BOOL), 0, @encode(BOOL));
        class_addIvar(A, "_c", sizeof(char), 0, @encode(char));
        class_addIvar(A, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
        objc_registerClassPair(A);

        id a = [A new];
        [a setValue:@(YES) forKey:@"_b"];
        [a setValue:@"test" forKey:@"_name"];
        [a setValue:@('p') forKey:@"_c"];

        NSLog(@"%p", [[a valueForKey:@"_b"] boolValue]);
        NSLog(@"%p", [[a valueForKey:@"_c"] charValue]);
        NSLog(@"%p", [a valueForKey:@"_name"]);
    }
}

先来直观地感受一下:

这与上文的结果完全一致。

class_addIvar函数的第4个参数,00log2(sizeof(NSString *)究竟是怎样确定的?不知道你发现没有,0其实与log2(sizeof(char))log2(sizeof(BOOL))是相等的,这里填写的应该都是log2(sizeof([数据类型]))吗? 成员变量的添加顺序可以调换吗?

回到源码:

BOOL 
class_addIvar(Class cls, const char *name, size_t size, 
              uint8_t alignment, const char *type)
{
    if (!cls) return NO;

    if (!type) type = "";
    if (name  &&  0 == strcmp(name, "")) name = nil;

    mutex_locker_t lock(runtimeLock);

    checkIsKnownClass(cls);
    assert(cls->isRealized());

    // No class variables
    if (cls->isMetaClass()) {
        return NO;
    }

    // Can only add ivars to in-construction classes.
    if (!(cls->data()->flags & RW_CONSTRUCTING)) {
        return NO;
    }

    // Check for existing ivar with this name, unless it's anonymous.
    // Check for too-big ivar.
    // fixme check for superclass ivar too?
    if ((name  &&  getIvar(cls, name))  ||  size > UINT32_MAX) {
        return NO;
    }

    class_ro_t *ro_w = make_ro_writeable(cls->data());

    // fixme allocate less memory here

    ivar_list_t *oldlist, *newlist;
    if ((oldlist = (ivar_list_t *)cls->data()->ro->ivars)) {
        size_t oldsize = oldlist->byteSize();
        newlist = (ivar_list_t *)calloc(oldsize + oldlist->entsize(), 1);
        memcpy(newlist, oldlist, oldsize);
        free(oldlist);
    } else {
        newlist = (ivar_list_t *)calloc(sizeof(ivar_list_t), 1);
        newlist->entsizeAndFlags = (uint32_t)sizeof(ivar_t);
    }

    uint32_t offset = cls->unalignedInstanceSize();
    uint32_t alignMask = (1<<alignment)-1;
    offset = (offset + alignMask) & ~alignMask;

    ivar_t& ivar = newlist->get(newlist->count++);
#if __x86_64__
    // Deliberately over-allocate the ivar offset variable. 
    // Use calloc() to clear all 64 bits. See the note in struct ivar_t.
    ivar.offset = (int32_t *)(int64_t *)calloc(sizeof(int64_t), 1);
#else
    ivar.offset = (int32_t *)malloc(sizeof(int32_t));
#endif
    *ivar.offset = offset;
    ivar.name = name ? strdupIfMutable(name) : nil;
    ivar.type = strdupIfMutable(type);
    ivar.alignment_raw = alignment;
    ivar.size = (uint32_t)size;

    ro_w->ivars = newlist;
    cls->setInstanceSize((uint32_t)(offset + size));

    // Ivar layout updated in registerClass.

    return YES;
}

static ivar_t *getIvar(Class cls, const char *name)
{
    runtimeLock.assertLocked();

    const ivar_list_t *ivars;
    assert(cls->isRealized());
    if ((ivars = cls->data()->ro->ivars)) {
        for (auto& ivar : *ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            // ivar.name may be nil for anonymous bitfields etc.
            if (ivar.name  &&  0 == strcmp(name, ivar.name)) {
                return &ivar;
            }
        }
    }

    return nil;
}

添加Ivar的流程很简单,需要注意的有以下几点:

  1. 参数name与type可以为空,这也就是匿名成员变量(用来占位的)
  2. 在添加Ivar前有这么句判断if ((name && getIvar(cls, name)) || size > UINT32_MAX) return NO,而从getIvar源码可以看到,匿名成员变量不会被匹配到,所以匿名成员变量可以添加多个(对应着内存优化,多处占位)
  3. checkIsKnownClass(cls)assert(cls->isRealized())用来检测当前添加成员变量的类是否已经存在,这也是为什么无法给已注册的类添加成员变量的原因(通过objc_setAssociatedObject添加的关联变量并不在被添加类中)
  4. 添加成员变量时,总是先获取当前变量所占空间,再通过alignment参数来控制偏移,并且有如下算法:
uint32_t offset = cls->unalignedInstanceSize();
uint32_t alignMask = (1<<alignment)-1;
offset = (offset + alignMask) & ~alignMask;

显然,在这种算法下,参数alignment并不总是log2(sizeof([数据类型])),你需要计算来达到最优布局,添加成员变量的顺序也不能调换,比如先添加_b再添加_name最后添加_c,那么_c一定在_name之后,而不会与_b相邻,A类在这种成员变量布局下会浪费不必要的内存

  1. 最终offset的值会绑定到ivar_t结构体的offset指针中存储,结构体定义如下:
struct ivar_t {
#if __x86_64__
    // *offset was originally 64-bit on some x86_64 platforms.
    // We read and write only 32 bits of it.
    // Some metadata provides all 64 bits. This is harmless for unsigned 
    // little-endian values.
    // Some code uses all 64 bits. class_addIvar() over-allocates the 
    // offset for their benefit.
#endif
    int32_t *offset;
    const char *name;
    const char *type;
    // alignment is sometimes -1; use alignment() instead
    uint32_t alignment_raw;
    uint32_t size;

    uint32_t alignment() const {
        if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
        return 1 << alignment_raw;
    }
};

所以Ivar存有想知道的一切,如偏移量等。从而,可以通过偏移量结合实例变量所在地址定位到成员变量存储数据的位置,通过name得知成员变量的名称,通过type得知成员变量的类型(怎样解析存储的数据),通过size得知成员变量的大小,甚至可以得到alignment

isa

前面一直在说ivar,最后来说isa,为什么可以通过实例->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
    ...
}

struct objc_object {
private:
    isa_t isa;

public:
    ...
}

typedef struct objc_class *Class;

isa是结构体objc_class的成员变量,*Classobjc_class的结构体指针,*aClass的指针,所以a即Class的实例,而Class又是个结构体指针,所以可以通过->取到结构体的成员变量。


Have fun!

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

推荐阅读更多精彩内容