iOS|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|iOS 内存对齐
文章图片
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|iOS 内存对齐
文章图片
image.png
【iOS|iOS 内存对齐】按照内存对齐规则,_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对象的内存布局,看我就够了!

    推荐阅读