美文网首页一些收藏c/c++ 数据结构 等基础iOS高手
Objective-C对象内存分布是怎样确定的

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

作者: 01_Jack | 来源:发表于2020-01-08 10:00 被阅读0次

    对于一个类的实例变量来说,我们常说他的内存分布是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!

    相关文章

      网友评论

        本文标题:Objective-C对象内存分布是怎样确定的

        本文链接:https://www.haomeiwen.com/subject/vwqkactx.html