获取内存大小的三种方式
- sizeof
- class_getInstanceSize
- malloc_size
sizeof
sizeof
是一个操作符,不是函数,一把用于计算内存大小。传入的主要对象是数据类型(基本数据类型、对象、指针)
,这个在编译器的编译阶段
就会确定大小而不是在运行时。sizeof
最终得到的结果是该数据类型
占用空间的大小
class_getInstanceSize
这个方法在OC底层原理02 - alloc & init & new 源码分析分析时就已经分析了,是runtime
提供的api,用于获取类的实例对象所占用的内存大小
,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小
malloc_size
这个函数是获取系统实际分配的内存大小
我们通过运行以下代码验证上述所说
#import
#import
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *objc = [[NSObject alloc] init];
NSLog(@"objc对象类型占用的内存大小:%lu", sizeof(objc));
NSLog(@"objc对象实际占用的内存大小:%lu", class_getInstanceSize([objc class]));
NSLog(@"objc对象实际分配的内存大小:%lu", malloc_size((__bridge const void*)(objc)));
}
return 0;
}
运行结果如下
总结
sizeof
- 计算
类型占用的内存大小
,其中可以放基本数据类型
、对象
、指针
- 对于类似于
int
这样的基本数据
而言,sizeof
获取的就是数据类型占用的内存大小
,不同的数据类型所占用的内存大小是不一样的 - 而对于类似于
NSObject
定义的实例对象
而言,其对象类型
的本质就是一个结构体(即 struct objc_object)的指针
,所以sizeof(objc)
打印的是对象objc的指针大小
,我们知道一个指针的内存大小是8
,所以sizeof(objc) 打印是 8
- 对于
指针
而言,sizeof
打印的就是本身的内存大小8
class_getInstanceSize
计算对象实际占用的内存大小
,这个需要依据类的属性而变化
,如果自定义类没有自定义属性,仅仅只是继承自NSObject
,则类的实例对象实际占用的内存大小是8
malloc_size
计算对象实际分配的内存大小
,这个是由系统完成的,可以从上面的打印结果看出,实际分配的和实际占用的内存大小并不相等,这个问题可以通过OC底层原理02 - alloc & init & new 源码分析中的16字节对齐算法
来解释这个问题
结构体内存对齐
首先我们定义两个结构体,分别计算他们的内存大小。
struct HLStruct1 {
double a;
char b;
int c;
short d;
}struct1;
struct HLStruct2 {
double a;
int b;
char c;
short d;
}struct2;
//计算 结构体占用的内存大小
NSLog(@"%lu-%lu", sizeof(HLstruct1), sizeof(HLstruct2));
输出结果如下
变量
和变量类型
都是一致的,唯一的不同是定义变量的顺序不一致
,但是他们所占用的内存大小不相等
却不相同。这就是iOS中的内存字节对齐
内存对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)
。程序员可以通过预编译命令#pragma pack(n)
,n = 1, 2, 4, 8, 16来改变这一系数,其中的n
就是你要指定的“对齐系数”
。在iOS中,Xcode默认为#pragma pack(8)
,即8字节对齐
内存字节对齐原则
数据成员对齐规则
:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
数据成员为结构体
:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则
:结构体的总大小,即sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐
分析
下表是各数据类型在C和OC中所占内存大小
HLStruct1
和HLStruct2
的内存结构示意图
计算过程详解:
HLStruct1:
-
变量a
:占8
个字节,从0开始,即0-7字节存储变量a
; -
变量b
:占1
个字节,从8开始,即8字节储存变量b
-
变量c
:占4
个字节,9不能整除4,故往后顺移,直到12能整除4,所以从12开始,即12-15字节储存变量c
-
变量d
:占2
个字节,从16开始,即16-17字节储存变量d
因此HLStruct1
的需要的内存大小为18
字节,而HLStruct1
中最大变量
的字节数为8
,HLStruct1
实际的内存大小必须是8的整数倍
,18
向上取整到24
,所以sizeof(HLStruct1)
的结果是24
HLStruct2: -
变量a
:占8
个字节,从0开始,即0-7字节存储变量a
; -
变量b
:占4
个字节,从8开始,即8-11字节储存变量b
-
变量c
:占1
个字节,从12开始,即12字节储存变量c
-
变量d
:占2
个字节,13不能整除2,故往后顺移,直到14能整除2,所以从14开始,即14-15字节储存变量d
因此HLStruct2
的需要的内存大小为16
字节,而HLStruct2
中最大变量
的字节数为8
,HLStruct2
实际的内存大小必须是8的整数倍
,16
刚好为8
的倍数,所以sizeof(HLStruct2)
的结果是16
结构体嵌套结构体
上面的两个结构体只是简单的定义数据成员,下面来一个比较复杂的,结构体中嵌套结构体的内存大小计算情况
定义一个结构体HLStruct3,在HLStruct3中嵌套HLStruct2,如下所示
struct HLStruct3 {
double a;
int b;
char c;
short d;
int e;
struct HLStruct2 f;
}struct3;
输出
NSLog(@"%lu-%lu", sizeof(struct3), sizeof(struct3.f));
输出结果如下
struct3内存计算
-
变量a
:占8
个字节,从0开始,即0-7字节存储变量a
; -
变量b
:占4
个字节,从8开始,即8-11字节储存变量b
-
变量c
:占1
个字节,从12开始,即12字节储存变量c
-
变量d
:占2
个字节,13不能整除2,故往后顺移,直到14能整除2,所以从14开始,即14-15字节储存变量d
-
变量e
:占4
个字节,从16开始,即16-19字节储存变量e
-
变量f
:占16
个字节,f
是一个结构体
,根据内存对齐原则二,结构体成员要从其内部最大成员大小的整数倍
开始存储,而HLStruct2
中最大的成员大小为8
,所以f
要从8
的整数倍开始,当前是从20开始,所以不符合要求,需要往后移动到24,24是8的整数倍,符合内存对齐原则,即24-39存储变量f
因此HLStruct3
的需要的内存大小为40
字节,而HLStruct3
中最大变量
的字节数为8
,HLStruct3
实际的内存大小必须是8的整数倍
,40
刚好为8
的倍数,所以sizeof(HLStruct3)
的结果是40
二次验证
在定义一个结构体,如下所示
struct HLStruct4 {
short a;
double b;
}struct4;
struct HLStruct5 {
char a;
int b;
struct HLStruct4 c;
}struct5;
HLStruct4内存计算
-
变量a
:占2
个字节,从0开始,即0-2字节存储变量a
; -
变量b
:占8
个字节,3不能整除8,故往后顺移,直到8能整除8,所以从8开始,即8-15字节存储变量b
;
因此HLStruct4
的需要的内存大小为16
字节,而HLStruct4
中最大变量
的字节数为8
,HLStruct4
实际的内存大小必须是8的整数倍
,16
刚好为8
的倍数,所以sizeof(HLStruct4)
的结果是16
HLStruct5内存计算 -
变量a
:占1
个字节,从0开始,即0字节存储变量a
; -
变量b
:占2
个字节,1不能整除2,故往后顺移,直到2能整除2,所以从2开始,即2-3字节存储变量b
; -
变量c
:占16
个字节,HLStruct4
中最大的成员大小为8
,所以c
要从8
的整数倍开始,当前是从4开始,所以不符合要求,需要往后移动到8,8是8的整数倍,符合内存对齐原则,即8-23存储变量c
因此HLStruct5
的需要的内存大小为24
字节,而HLStruct4
中最大变量
的字节数为8
,HLStruct5
实际的内存大小必须是8的整数倍
,24
刚好为8
的倍数,所以sizeof(HLStruct5)
的结果是24
下图为输出及打印
内存优化(属性重排)
根据内存对齐原则,HLStruct1
补齐了9
个字节,而HLStruct2
只补齐1
个字节即可满足该规则,因此得出一个结论结构体内存大小与结构体成员内存大小的顺序有关
。
创建一个对象来探索
- 首先定义一个自定义类HLPerson类,并添加几个属性
@interface HLPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
- 在main中创建HLPerson的实例对象,并对其属性赋值
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
HLPerson *person = [HLPerson alloc];
person.name = @"HL";
person.nickName = @"Lay";
person.age = 17;
person.height = 180;
person.c1 = 'a';
person.c2 = 'b';
NSLog(@"%@", person);
}
return 0;
}
- 断点调试person
-
x person
:x
是memory read
的简写,读取内存信息
tips:iOS是小端模式,所以内存的读取要反着读,即cd 83 00 00 01 80 1d 00
应该读取为0x001d8001000083cd
-
x/8gx person
:以16进制打印8行内存信息,并分别打印其指向,此方法读取地址更为便捷
这里虽然打印了8行内存信息,但实际上person
对象变量并没有使用这么多内存,可以通过class_getInstanceSize
方法获取实际上该对象只占用了40
字节的内存,也就是上图中前五段内存,所以后三段全都为0x00000000,但是有几个属性值却并没有找到。
分析
没有找到age
、c1
及c2
对应的值,是不是苹果做了什么处理避免内存过度消耗,我们用没有正常输出信息的内存尝试解析下
结论
name
、nickname
、height
都是各自占用8
字节。可以直接打印出来;而age
是Int
占用4
字节,c1
和c2
是char
,各自占用1
字节。我们推测系统可能进行属性重排,将他们存放在了一个块区。
下图是HLPerson的内存分布情况
特殊的double
和float
把height
属性类型修改为double
//@property (nonatomic, assign) long height;
@property (nonatomic, assign) double height;
重新运行
直接po打印0x4066800000000000,并不能正确输出变量
height
的值,这是因为编译器po打印默认当做int类型处理
。
-
p/x (double)180
:将180转成double
类型然后以16进制进行打印,发现地址完全一样。
height
改成float
类型也可以用p/x (float)180
验证
封装2个验证函数
// float转换为16进制
void hl_float2HEX(float f) {
union uuf { float f; char s[4]; } uf;
uf.f = f;
printf("0x");
for (int i = 3; i >= 0; i--) {
printf("%02x", 0xff & uf.s[i]);
}
printf("\n");
}
// double转换为16进制
void hl_double2HEX(double d) {
union uud { double d; char s[8]; } ud;
ud.d = d;
printf("0x");
for (int i = 7; i >= 0; i--) {
printf("%02x", 0xff & ud.s[i]);
}
printf("\n");
}
打印验证
字节对齐到底采用多少字节对齐?
在objc4
源码中搜索class_getInstanceSize
,可以在runtime.h
找到:
/**
* Returns the size of instances of a class.
*
* @param cls A class object.
*
* @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
*/
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
// 其中 WORD_MASK 为
# define WORD_MASK 7UL
通过源码可以看出,对于一个对象
来说,其真正的对齐方式是8字节对齐
,8字节对齐已经足够满足对象的需求了
总结
class_getInstanceSize
:是采用8字节对齐
,参照的对象的属性内存大小
malloc_size
:采用16字节对齐
,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍
内存对齐算法
至此,我们已知的16字节对齐算法有两种
-
alloc
源码分析中的align16
-
malloc
源码分析中的segregated_size_to_fit
align16
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
segregated_size_to_fit
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;
}
算法原理:k + (16 - 1) >> 4 << 4 ,其中右移4+左移4
相当于将后4位抹零
,跟k / 16 * 16
一样 ,小于16就成0了