一、instanceSize方法的底层实现
首先需要知道,对象的内存空间里存放的是对象的属性,所以计算内存大小即为计算所有属性所占的内存大小,先看下objc源码的底层实现:
size_t instanceSize(size_t extraBytes) const {
//方式一:编译器快速计算内存大小(16字节对齐)
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
//方式二:计算类中所有属性和方法的内存占用 + 额外的字节数0(8字节对齐)
size_t size = alignedInstanceSize() + extraBytes;
//CF requires all objects be at least 16 bytes.
//最少申请16字节的内存大小
if (size < 16) size = 16;
return size;
}
上面两个方式的区别,在于将对象所有属性所占的内存大小采取不同的对齐方式,方式一是16字节对齐,方式二是8字节对齐。
注意,在不同版本的objc源码中,instanceSize
方法的底层实现可能是不一样的,本文参照的是最新的objc-781
源码,里面会有这两种计算方式,但是实际运行,其实是以方式一来计算的,16字节内存对齐。而在老版本的objc源码中,比如objc-750
源码,里面只有方式二的计算方式,采取的是8字节对齐。
//objc-750源码中的instanceSize方法
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;
}
虽然这个方法的底层实现不会频繁更新,但大家以后在分析时,最好是参照最新的objc源码进行分析。话不多说,接下来分析这两种方式具体是怎么计算的。
1.1 十六字节对齐 - fastInstanceSize
从源码中可知,是在fastInstanceSize
方法里完成内存计算的:
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
编译器快速计算出需要的size后,最后会执行align16
方法,即『16字节对齐』。
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
系统在计算出内存size后,会先加15,再与个非15,这样就实现了16字节对齐。接下来通过两组二进制运算来直观地看看字节对齐后的结果:
//已知: 15的二进制为: 0000 1111
// ~15的二进制为:1111 0000
//假设计算出当前类需要的内存大小为12字节,则16字节对齐的计算过程如下:
0000 1100 // 12字节
0000 1111 // +15
0001 1011 // = 27
1111 0000 // ~15
0001 0000 // 27 & (~15) = 16
//假设计算出当前类需要的内存大小为24字节,则16字节对齐的计算过程如下:
0001 1000 // 24字节
0000 1111 // +15
0010 0111 // = 39
1111 0000 // ~15
0010 0000 // 39 & (~15) = 32
从上面两组二进制运算得知,12字节、24字节的内存经由16字节对齐后,实际申请内存大小分别为16字节、32字节。因此,不管对象属性实际需要多大的内存空间,在新版本中,系统都会以16字节的倍数来申请内存空间,并且最少16字节,这就是『16字节对齐』。
1.2 八字节对齐 - alignedInstanceSize
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
在实例化对象时,经由断点调试可知,extraBytes为0,所以内存大小依赖于alignedInstanceSize
方法计算:
uint32_t alignedInstanceSize() {
//step1:通过unalignedInstanceSize方法获取到需要的内存大小
//step2:通过word_align方法对内存大小进行8字节运算
return word_align(unalignedInstanceSize());
}
可以看到,class_getInstanceSize方法最终实现就两步,先获取到对象属性所占的内存大小,再将内存大小进行8字节对齐运算。
- unalignedInstanceSize
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
这一步获取到的是对象的属性所占的内存大小,ro是个很重要的知识点,会在后续介绍类和对象的结构时作详细讲解,本篇文章就不作过多拓展了。
- word_align
#define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
WORD_MASK的值为7,这里其实是对内存大小作8字节对齐运算。对齐计算和上文的16进制对齐一样,大家可以自己进行二进制演算,这里就不再推演了。在完成8字节对齐运算后,还会再判断size的大小,如果小于16,就申请16字节的内存。 也就是说,不管对象的属性实际需要多大的内存,在老版本中,系统都会以8字节的倍数来申请内存空间,并且最少16字节。
二、对内存进行字节对齐的原因
理解了内存对齐逻辑后,可能会有疑问,为什么要对内存进行字节对齐呢?为什么不按对象属性实际占用的内存大小来分配内存呢?经由字节对齐后,分配的空间要比实际需要的多,这样会不会造成内存浪费?
在解答这些疑问前,我们需要先清楚,对象里可能会有多种数据类型的属性,这些属性占用内存大小是不一样的,如果直接按照实际需要内存进行分配,则cpu在读取内存数据时需要先知道每个数据占用多大内存,这样才能保证读取的数据是完整的,读取效率较低。
下面这个表格是各种类型数据分别在32位和64位系统中占用内存的大小,单位:字节。(注:本文都是基于64位系统分析的)
C | OC | 32位 | 64位 |
---|---|---|---|
bool | BooL(64位) | 1 | 1 |
signed char | (_ _signed char)int8_t、BOOL(32位) | 1 | 1 |
unsigned char | Boolean | 1 | 1 |
short | int16_t | 2 | 2 |
unsigned short | unichar | 2 | 2 |
int、int32_t | NSInteger(32位)、boolean_ t(32位) | 4 | 4 |
unsigned int | boolean_ t(64位)、NSUInteger(32位) | 4 | 4 |
long | NSInteger(64位) | 4 | 8 |
unsigned long | NSUlnteger(64位) | 4 | 8 |
long long | int64_t | 8 | 8 |
float | CGFloat(32位) | 4 | 4 |
double | CGFloat(64位) | 8 | 8 |
从表格里清晰的看到,属性最多占用8字节内存。而对象里最常见的属性是指针类型,指针类型的数据在内存里也是占用8字节的大小。这样,以8字节的倍数来开辟内存空间,CPU再以8字节为一段进行读取,既能保证每个数据读取的完整性,又能提高读取效率,这就是最初系统8字节对齐的原因。
举个例子,创建一个Person对象,对象里有多种数据类型的属性,来看看字节对齐对分配内存的影响。(注:这里只是看下字节对齐的影响,就先不考虑isa指针了)
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, strong) NSString *sex; //8
@property (nonatomic, assign) int age; //4
@property (nonatomic, assign) long height; //8
@property (nonatomic) char c1; //1
@property (nonatomic) char c2; //1
@end
如果不作字节对齐,那么系统分配内存会是下面的情况:
系统如果以属性实际占用内存大小来分配,虽然有效利用了内存空间,但是在读取数据时,CPU就需要预先知道每个属性占用的内存大小,并按对应字节长度读取读取内存,否则读取的数据就不完整。
比如,在上图中,若读取age数据,如果此时CPU不按照4字节长度来读取,要么没读取到完整的age数据,要么会越界读到height的数据。
因此,系统如果以属性实际占用内存大小来分配,CPU读取内存数据时既不安全,效率也低。
所以系统会对内存进行字节对齐,内存分配情况大致如下(注:不是一定按照这个顺序的):
若进行字节对齐,在分配内存时,会进行属性重排,将age、c1、c2(总共6字节)这三个属性存在一个8字节段中,在读取数据时,CPU也是按照8字节为一段来读取,这样既能保证读取数据的完整性,又能提高读取效率。虽说会多开辟几字节的内存空间,但是保证了读取安全,提高了读取效率,完全是值得的。而且鉴于现在设备内存越来越大,这种『以空间换时间』来提高效率的方式,会越来越常见。
三、计算对象申请的内存空间大小
内存空间里保存的是对象的属性,所以计算内存空间大小即是统计对象所有属性所占的内存大小。需要注意的是,对象都是继承自NSObject的,NSObject里有个isa成员属性,是指针类型,需要占用8字节内存。
//NSObjct的结构
@interface NSObject {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
isa的具体用途后面会讲解,本文不再拓展。在接下来的内存计算中,都需要加上其占用的8字节内存。
按照前面的思路,计算对象实例化时申请的内存空间大小,即先统计所有属性所占的内存大小,然后再进行16字节对齐就可得到。那怎么印证这个计算规则呢?最直观的方法就是获取并打印对象的内存size,之前有看过其他文章,很多都是通过class_getInstanceSize
方法来获取的,先来看下这个方法的实现逻辑:
size_t class_getInstanceSize(Class cls){
if (!cls) return 0;
return cls->alignedInstanceSize();
}
可以看到,class_getInstanceSize
方法里面调用的是alignedInstanceSize
方法,就回到了上文1.2里面的八字节对齐处理了,所以如果是老版本,就可以正常使用,对于新版本的16字节对齐就不适用了。在新版本里可以改写这个方法的对齐运算,然后再使用,比如这样:
//将8字节对齐改成16字节对齐
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
// return cls->alignedInstanceSize();
return cls->instanceSize(0);
}
但为了更直观的区分8字节和16字节的对齐结果,这里我选择在class_createInstanceFromZone
方法里打印需要申请的内存大小。
- 先在objc-runtime-new.h文件里添加一个老版本8字节对齐的方法。
//这也是objc-750版本的instanceSize方法
size_t oldInstanceSize(size_t extraBytes) const {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
- 然后在
class_createInstanceFromZone
方法里再获取一个8字节对齐的结果,并将两种对齐方式的内存大小打印出来。
//这是原本的16字节对齐计算
size_t size;
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
//这里再获取一个8字节对齐结果
size_t size8;
size8 = cls->oldInstanceSize(extraBytes);
//打印两个结果
printf("size16 = %lu \nsize8 = %lu\n", size, size8);
- 最后实例化Person对象,打印结果。为了对比更加直观,也打印出通过
class_getInstanceSize
方法获取到的内存大小。
准备工作完成,那接下来分析在各种情况下创建Person对象需要申请的内存空间大小。
3.1 不含任何自定义属性的Person对象
//不含自定义属性的Person对象
@interface Person : NSObject
@end
//创建Person对象,打印内存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印结果
size16 = 16 //16字节对齐的结果
size8 = 16 //8字节对齐的结果
class_getInstanceSize = 8 //获取的8字节对齐的内存
person对象里只有一个isa属性,占8字节,那属性总共占用8字节的内存大小:
-
size16 = 16
:经由16字节对齐后,结果为16字节。 -
size8 = 16
:经由8字节对齐后,结果为8字节。但oldInstanceSize方法最后对内存大小进行了判断,如果小于16,就返回16。 -
class_getInstanceSize = 8
:这个方法只作了8字节对齐,所以返回为8。
3.2 自定义指针类型属性的Person对象
//声明两个指针类型的属性
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, strong) NSString *sex; //8
@end
//创建Person对象,打印内存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印结果
size16 = 32 //16字节对齐的结果
size8 = 24 //8字节对齐的结果
class_getInstanceSize = 24 //获取的8字节对齐的内存
person对象里有三个指针类型的数据(isa,name,sex),属性总共占用24字节的内存大小:
-
size16 = 32
:经由16字节对齐后,结果为32字节。 -
size8 = 24
:经由8字节对齐后,结果为24字节。 -
class_getInstanceSize = 24
:这个方法只作了8字节对齐,所以返回为24。
3.3 自定义多种数据类型属性的Person对象
//Person类里自定义了多种类型的属性
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, strong) NSString *sex; //8
@property (nonatomic, assign) int age; //4
@property (nonatomic, assign) long height; //8
@property (nonatomic) char c1; //1
@property (nonatomic) char c2; //1
@end
//创建Person对象,打印内存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印结果
size16 = 48 //16字节对齐的结果
size8 = 40 //8字节对齐的结果
class_getInstanceSize = 40 //获取的8字节对齐的内存
person对象里的属性总共占用38字节的内存大小:
-
size16 = 48
:经由16字节对齐后,结果为48字节。 -
size8 = 40
:经由8字节对齐后,结果为40字节。 -
class_getInstanceSize = 40
:这个方法只作了8字节对齐,所以返回为40。
3.4 对象里包含结构体成员属性
在Person对象里添加结构体成员,再看下打印结果。
//声明一个结构体struct1,根据前面的分析可知,内存为24。
struct Struct1 {
double a; // 8
char b; // 1
int c; // 4
short d; // 2
};
//在person最后面添加一个struct1的类
@interface Person : NSObject
@property (nonatomic, strong) NSString *name; //8
@property (nonatomic, assign) int age; //4
@property (nonatomic, assign) long height; //8
@property (nonatomic) char c1; //1
@property (nonatomic) struct Struct1 str; //24
@end
//创建Person对象,打印内存大小
Person *person = [[Person alloc] init];
NSLog(@"class_getInstanceSize = %lu", class_getInstanceSize([Person class]));
//打印结果
size16 = 64 //16字节对齐的结果
size8 = 56 //8字节对齐的结果
class_getInstanceSize = 56 //获取的8字节对齐的内存
person对象其他属性占用了29个字节,在对象内存里存放在位置0-28 。结构体str成员,本应该在位置29处开始存放,但根据结构体内存对齐规则,当结构体作为成员时,则结构体成员要从其内部最⼤元素所占内存⼤⼩的整数倍地址开始存储,结构体str里最大成员占用8字节内存,所以str需要从8的整数倍开始存储,即从位置32开始存放,存放位置为32-55,所以最后person对象需要56字节内存:
-
size16 = 64
:经由16字节对齐后,结果为64字节。 -
size8 = 56
:经由8字节对齐后,结果为56字节。 -
class_getInstanceSize = 56
:这个方法只作了8字节对齐,所以返回为56。
通过分析上述示例的打印结果,均可印证前面说的内存大小的计算规则:先统计所有属性所占的内存大小,然后再进行16字节对齐。简单数据类型的属性所占内存比较容易统计,但对于比较复杂的数据结构,比如说结构体(struct
),它的内存计算就需要遵循『内存对齐规则』。
四、总结
对象的内存大小依赖其包含的属性,所以在实例化对象时,系统会先统计对象所有属性所占的内存大小,再经由16字节对齐(老版本是8字节对齐),计算出需要申请的内存空间大小,所以对象的内存大小只会是16的整数倍,并且最少申请16字节大小的内存。
对内存大小作16字节对齐或8字节对齐,主要是为了保证cpu读取内存数据的安全,以及提高读取效率。
若数据类型为结构体(
struct
),在计算内存大小时需要遵循结构体内存对齐原则,详细介绍可参考下方推荐阅读里的『数据结构 -- 结构体Struct』一文。
五、帮助
在打印内存大小时,可能会需要下面这三个方法:
-
sizeof
:是一个运算符,用来计算传进来的数据类型占用多大的内存,在编译时即可完成运算。 -
class_getInstanceSize
:获取对象实例化时需要申请的内存大小,采取的是8字节对齐方式,调用时需要先引用#import
。 -
malloc_size
:获取对象实例化时系统实际开辟的内存大小,采取的是16字节对齐方式,调用时需要先引用#import
。