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)都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足;
- 结构体的总大小为结构体最大基本类型成员变量大小的整数倍;
- 结构体中的成员变量都是分配在连续的内存空间中。
@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个字节
文章图片
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
内存分配应该是这样的了
文章图片
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对象的内存布局,看我就够了!
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 2020-04-07vue中Axios的封装和API接口的管理
- iOS中的Block
- 记录iOS生成分享图片的一些问题,根据UIView生成固定尺寸的分享图片
- 2019-08-29|2019-08-29 iOS13适配那点事
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- iOS面试题--基础
- Java内存泄漏分析系列之二(jstack生成的Thread|Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析)
- 接口|axios接口报错-参数类型错误解决
- iOS|iOS 笔记之_时间戳 + DES 加密