iOS 内存对齐

前言

现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐

OC对象的内存占用

我们先来看看OC的对象所占用的内存

NSObject *obj = [[NSObject alloc] init];
NSLog(@"实际占用: class_getInstanceSize = %zd", class_getInstanceSize([NSObject class]));
NSLog(@"系统分配:malloc_size = %zd", malloc_size((__bridge const void *)(obj)));
NSLog(@"NSObject类型占用:sizeOf = %zd", sizeof(obj));

打印结果:

实际占用: class_getInstanceSize = 8
系统分配:malloc_size = 16
NSObject类型占用:sizeOf = 8

可以看到NSObject实际占用了8字节,而系统分配了16字节。
这个可以从源码中看一看,我下载是objc4-779
在NSObject.mm中查看

// Replaced by ObjectAlloc
+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

然后去看_objc_rootAllocWithZone的实现,在objc-runtime-new.mm中

NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

接着看_class-createInstanceFromZone的实现

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                              int construct_flags = OBJECT_CONSTRUCT_NONE,
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

再看size = cls->instanceSize(extraBytes)的实现

size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

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

到这里就能看到当实例对象不足16个字节,系统会分配给16个字节
所以我们可以得出结论
在64位架构下, 系统分配了16个字节给NSObject对象(通过malloc_size函数获得);
但NSObject对象内部只使用了8个字节的空间(可以通过class_getInstanceSize函数获得)。

内存对齐

内存对齐的规则

  • 结构体变量的首地址是其最长基本类型成员的整数倍;
  • 结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足;
  • 结构体的总大小为结构体最大基本类型成员变量大小的整数倍;
  • 结构体中的成员变量都是分配在连续的内存空间中。

下面我们创建一个OC类来验证一下

@interface MemoryObject : NSObject {
    int _age;
    NSString *_name;
    int _height;
}

//@property (nonatomic, assign) int age;
//@property (nonatomic, copy) NSString *name;
//@property (nonatomic, assign) int height;


@end

打印一下内存

MemoryObject *m = [[MemoryObject alloc] init];
NSLog(@"class_getInstanceSize = %zd", class_getInstanceSize([MemoryObject class]));
NSLog(@"malloc_size = %zd", malloc_size((__bridge const void *)(m)));
NSLog(@"sizeOf = %zd", sizeof(m));

输出

class_getInstanceSize = 32
malloc_size = 32
sizeOf = 8

我们把MemoryObject转成C++代码看下

xcrun -sdk iphonesimulator clang -rewrite-objc MemoryObject.m

在C++代码中我们可以看到MemoryObject的结构

struct MemoryObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 指针占用8个字节
    int _age;  //4
    NSString *_name;  //8
    int _height;  //4
};

如果没有内存对齐的话,内存占用应该是8+4+8+4 = 24个字节,现在根据内存对齐的规则(在结构体中,总大小为结构体对最大成员大小的整数倍,如不满足,最后填充字节以满足,可分配的最小内存是结构体中内存占用最大的成员变量的大小。)由于需要满足8的整数倍,所以最后填充字节分配32个字节


iOS 内存对齐_第1张图片
image.png

结构体中成员变量的内存都是连续分配,由于_age只有4字节,按照内存对齐规则需要分配8个字节,而_height在最后也会因为内存对齐填充成8字节

而系统实际分配了32个字节,因为之前看源码,系统每次至少分配16个字节,这里的32是16的整数倍。
如果我们调整一下顺序

@interface MemoryObject : NSObject {
    int _age;
    int _height;
    NSString *_name;
}

对应的C++代码

struct MemoryObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
    NSString *_name;
};

再次输出内存大小

class_getInstanceSize = 24
malloc_size = 32
sizeOf = 8

内存分配应该是这样的了


iOS 内存对齐_第2张图片
image.png

按照内存对齐规则,_age和_height加起来正好是8字节,不用系统填充,所以内存整个结构体占用24个字节,而系统需要满足16的倍数,还是32个字节。
所以成员变量的顺序是可以影响内存分配的
不过如果我们代码里用property声明的话

@property (nonatomic, assign) int age;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int height;

对应的C++代码是这样的

struct MemoryObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    int _age;
    int _height;
    NSString * _Nonnull _name;
};

顺序被调整过了,可见使用property的时候苹果是做了这方面的优化的

参考

关于NSObject对象的内存布局,看我就够了!

你可能感兴趣的:(iOS 内存对齐)