instanceSize分析
instanceSize
函数是alloc
的核心方法之一,负责计算内存大小打开
objc4-818.2
源码进入
instanceSize
函数inline size_t instanceSize(size_t extraBytes) const { if (fastpath(cache.hasFastInstanceSize(extraBytes))) { return cache.fastInstanceSize(extraBytes); } size_t size = alignedInstanceSize() + extraBytes; if (size < 16) size = 16; return size; }
fastInstanceSize
:编译器快速计算内存大小alignedInstanceSize
:得到对齐后的实例对象大小extraBytes
:额外字节数,传入的值为0
size
:不能小于16字节
进入
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; return align16(size + extra - FAST_CACHE_ALLOC_DELTA16); } }
__builtin_constant_p
:GCC
的内建函数。用于判断一个值是否为编译时常数,如果参数EXP
的值是常数,函数返回1
,否则返回0
extra
:额外字节数,传入的值为0
FAST_CACHE_ALLOC_DELTA16
:来自setFastInstanceSize
方法的8字节
align16
:16字节
对齐
FAST_CACHE_ALLOC_DELTA16
定义:#define FAST_CACHE_ALLOC_DELTA16 0x0008
进入
align16
函数,16字节
对齐算法static inline size_t align16(size_t x) { return (x + size_t(15)) & ~size_t(15); }
进入
alignedInstanceSize
函数uint32_t alignedInstanceSize() const { return word_align(unalignedInstanceSize()); }
unalignedInstanceSize
:得到未对齐的实例对象大小word_align
:8字节
对齐
进入
unalignedInstanceSize
函数uint32_t unalignedInstanceSize() const { ASSERT(isRealized()); return data()->ro()->instanceSize; }
进入
word_align
函数,8字节
对齐算法static inline uint32_t word_align(uint32_t x) { return (x + WORD_MASK) & ~WORD_MASK; }
WORD_MASK
定义:# define WORD_MASK 7UL
instanceSize
流程图
字节对齐
在
word_align
方法中,字节对齐算法为:(x + N) & ~N
&
:与运算,都是1
结果为1
,反之为0
~
:取反,1
变为0
,0
变为1
例如:
8字节
对齐,N
必须为7
假设:传入的
x
为10
x + N = 17
,0001 0111
~N
:7
取反,1111 1000
0001 0111 & 1111 1000 = 0001 0000
转换
10进制
为16
为什么需要字节对齐?
内存以字节为基本单位,当
CPU
存取数据时,以块为单位读取未对齐数据,需要多次访问内存,极大降低
CPU
的性能如果数据存储在自然对齐的位置上,可以降低
CPU
的存取次数。以空间换取时间,提升CPU
的访问速率
为什么是
8字节
对齐?在
arm64
中,成员变量的数据类型最大占8字节
如果对齐规则大于
8字节
,会造成内存空间的浪费。如果小于8字节
,读取占8字节
的数据类型,需要多次访问内存故此,对齐规则为
8字节
是最好的选择
对象内存的影响因素
对象的成员变量会影响其内存大小,实例方法和类方法,不会对其产生影响。而属性的本质是
get/set
方法,所以也不会影响在堆区分配的内存空间,首先存储对象
isa
,下面依次排列对象的成员变量
案例:
打开
LGPerson.h
文件,写入以下代码:@interface LGPerson : NSObject @property (strong,nonatomic) NSString *name; @property (strong,nonatomic) NSString *nick; @property (assign,nonatomic) int age; @property (assign,nonatomic) bool age1; @property (assign,nonatomic) double height; @end
打开
main.m
文件,写入以下代码:int main(int argc, const char * argv[]) { @autoreleasepool { LGPerson *per= [LGPerson alloc]; per.name = @"Zang"; per.nick = @"Z"; per.age = 18; per.age1 = 1; per.height = 180.0; NSLog(@"%@",per); } return 0; }
在
lldb
中,打印对象的内存数据【方式一】使用
x
指令x per ------------------------- 0x10072d690: 4d 83 00 00 01 80 1d 01 01 00 00 00 12 00 00 00 M............... 0x10072d6a0: 30 40 00 00 01 00 00 00 50 40 00 00 01 00 00 00 0@......P@......
x
是memory read
指令的简写,作用是内存读取并打印iOS
为小端模式,内存的读取从右往左0x10072d690
为对象的首地址
【方式二】使用
View Memory
LGPerson
对象的isa
和5个
成员变量
【方式三】使用
x/nfu
指令x/6g per ------------------------- 0x10072d690: 0x011d80010000834d 0x0000001200000001 0x10072d6a0: 0x0000000100004030 0x0000000100004050 0x10072d6b0: 0x4066800000000000 0x0000000000000000
x/nfu
指令属于有规律打印,iOS
为小端模式,所以打印结果与x per
刚好相反x
:每一段以16进制
打印n
:打印的内存单元个数u
:地址单元的长度
◦g
:八字节
◦w
:四字节
◦h
:双字节
◦b
:单字节f
:格式化打印方式
◦x
:十六进制
◦d
:十进制
◦u
:十进制,无符号整型
◦o
:八进制
◦t
:二进制
◦a
:十六进制 + 字符串
◦i
:指令格式
◦c
:字符格式
◦f
:浮点数格
- 最前面
0x10072d690
为对象首地址,后面的0x011d80010000834d
是成员变量的值,0x10072d690
地址指向0x011d80010000834d
的值
标记1
:存储对象isa
,和ISA_MASK
进行&
运算,才能正常打印
ISA_MASK
定义:# define ISA_MASK 0x00007ffffffffff8ULL
打印
isa
po 0x011d80010000834d & 0x00007ffffffffff8 ------------------------- LGPerson
标记2
:8字节
中存储了age
和age1
两个属性
age
为int
类型,占4字节
age1
为bool
类型,占1字节
两个属性的大小之和,未超过
8字节
。为了避免内存的浪费,系统做了内存对齐优化,将两个属性并存到一个8字节
中打印
age
po 0x00000012 ------------------------- 18
打印
age1
po 0x00000001 ------------------------- 1
标记3
:存储name
属性po 0x0000000100004030 ------------------------- Zang
标记4
:存储nick
属性po 0x0000000100004050 ------------------------- Z
标记5
:存储height
属性。height
为double
类型,需要进行格式化打印e -f f -- 0x4066800000000000 ------------------------- 180
或者
p/f 0x4066800000000000 ------------------------- 180
结构体内存对齐
内存对⻬的原则
- 数据成员对⻬规则:结构(
struct
)或联合(union
)的数据成员,第⼀个数据成员放在offset
为0
的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,例如:数组、结构体等)的整数倍开始
◦ 例如:int
为4字节
,则要从4
的整数倍地址开始存储。如果当前开始存储的位置为9
,需要空出9
、10
、11
,在12
的位置才可存储- 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储
◦ 例如:struct a
⾥存有struct b
,b
⾥有char
、int
、double
等元素,那b
应该从8
的整数倍开始存储- 收尾⼯作:结构体的总⼤⼩,也就是
sizeof
的结果,必须是其内部最⼤成员的整数倍,不⾜的要补⻬
案例1:
struct LGStruct1 { double a; char b; int c; short d; }struct1;
a
占8字节
,存储在0~7
位置b
占1字节
,存储在8
位置。因为8
是1
的倍数,满足条件c
占4字节
,9~11
都不是4
的倍数,无法存储,将其空出。所以c
存储在12~15
位置d
占2
字节,存储在16~17
位置- 最后进行收尾⼯作,满足内部最⼤成员的整数倍,补⻬至
24
NSLog(@"struct1:%lu",sizeof(struct1)); ------------------------- struct1:24
案例2:
struct LGStruct2 { double a; int b; char c; short d; }struct2;
a
占8字节
,存储在0~7
位置b
占4字节
,存储在8~11
位置c
占1字节
,存储在12
位置d
占2
字节,13
不是2
的倍数,无法存储,将其空出。所以d
存储在14~15
位置- 最后进行收尾⼯作,满足内部最⼤成员的整数倍,补⻬至
16
NSLog(@"struct2:%lu",sizeof(struct2)); ------------------------- struct2:16
案例3:
struct LGStruct3 { double a; int b; char c; short d; int e; struct LGStruct1 str; }struct3;
a
占8字节
,存储在0~7
位置b
占4字节
,存储在8~11
位置c
占1字节
,存储在12
位置d
占2
字节,13
不是2
的倍数,无法存储,将其空出。所以d
存储在14~15
位置e
占4字节
,存储在16~19
位置str
为结构体类型,最大成员占8字节
。包含结构体成员,从其内部最⼤元素⼤⼩的整数倍地址开始存储。所以str
的起始位置为24
。str
结构体内存对齐后占24字节
,所以LGStruct3
的大小为24 + 24 = 48
NSLog(@"struct3:%lu",sizeof(struct3)); ------------------------- struct3:48
获取内存大小的三种方式
【方式一】
sizeof
sizeof
不是函数,而是一个操作符- 一般会传入数据类型,编译器在编译时期即可确定大小
sizeof
得到的大小,即是该类型占用的空间大小
【方式二】
class_getInstanceSize
class_getInstanceSize
是runtime
提供的api
- 作用:获取类的实例对象所占用的内存大小
- 本质:获取实例对象中成员变量的内存大小
- 采用
8字节
对齐,参照对象的属性大小
【方式三】
malloc_size
- 作用:获取系统实际分配的内存大小
- 采用
16字节
对齐,参照整个对象的大小- 实际分配的内存大小,必须是
16
的整数倍
案例
打开
LGPerson.h
文件,写入以下代码:@interface LGPerson : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *nickName; @property (nonatomic, assign) int age; @property (nonatomic, assign) long height; @end
打开
main.m
文件,写入以下代码:#import <Foundation/Foundation.h> #import "LGPerson.h" #import <objc/runtime.h> #import <malloc/malloc.h> int main(int argc, const char * argv[]) { @autoreleasepool { LGPerson *person = [LGPerson alloc]; NSLog(@"sizeof:%lu",sizeof(person)); NSLog(@"class_getInstanceSize:%lu",class_getInstanceSize([LGPerson class])); NSLog(@"malloc_size:%lu",malloc_size((__bridge const void *)(person))); } return 0; }
打印结果:
sizeof:8 class_getInstanceSize:40 malloc_size:48
sizeof
为8
,因为person
对象,本质是指针地址,占8字节
class_getInstanceSize
为40
,LGPerson
的成员变量大小为36字节
,8字节
对齐后,占40字节
malloc_size
为48
,系统分配的内存大小,经过16字节
对齐后,占48字节
在
LGPerson
中,即使没有任何成员变量,class_getInstanceSize
依然会占8字节
因为
LGPerson
继承自NSObject
,默认存在isa
成员变量@interface NSObject <NSObject> { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-interface-ivars" Class isa OBJC_ISA_AVAILABILITY; #pragma clang diagnostic pop }
isa
为Class
类型,本质是结构体指针,所以占8字节
typedef struct objc_class *Class;
objc_class
继承自最原始的objc_object
结构体struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY; };
所以,万物皆对象,类也是一个对象
malloc分析
malloc
函数是alloc
的核心方法之一,负责开辟内存空间项目中,只能找到
malloc_size
的方法定义,它的代码实现在libmalloc
源码中
打开
libmalloc-317.40.8
源码进入
calloc
函数void * calloc(size_t num_items, size_t size) { return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX); }
进入
_malloc_zone_calloc
函数
源码中只能找到
calloc
的函数声明,但是无法进入void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
在项目中,搜索
calloc
关键字,没有找到任何线索这种情况,可以尝试打印
zone->calloc
- 找到函数的真身:
default_zone_calloc
在全局搜索
calloc
时,虽然找不到函数实现,但是找到了calloc
赋值代码。有赋值必然会存储值,通过打印也许可以得到线索
或者,尝试
Always Show Disassembly
查看汇编代码
- 也可得到相同线索:
default_zone_calloc
来到
default_zone_calloc
函数static void * default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) { zone = runtime_default_zone(); return zone->calloc(zone, num_items, size); }
又遇到了
zone->calloc
函数,继续使用lldb
打印
来到
nano_malloc
函数
进入
_nano_malloc_check_clear
函数
segregated_size_to_fit
:计算内存大小segregated_next_block
:开辟内存空间
进入
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; }
NANO_REGIME_QUANTA_SIZE
和SHIFT_NANO_QUANTUM
定义:#define SHIFT_NANO_QUANTUM 4 #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
1
左移4位
,即:16
内存对齐的算法:
(size + 15) >> 4 << 4
算法作用为
16字节
对齐,保证分配的内存大小,必须是16
的整数倍,与(x + N) & ~N
算法有异曲同工之妙假设:传入的
size
为40
size + 15 = 55
,0011 0111
右移
4位
,0000 0110
左移
4位
,0011 0000
转换
10
进制为48
进入
segregated_next_block
函数,开辟内存空间
- 堆区开辟的空间是不连续的,期间可能因多线程、小于最大限制地址等原因,需要重新尝试
while
。当开辟空间成功,返回指针地址
结构体内部,成员变量以
8字节
对齐。但是在堆区分配对象的内存大小,以16字节
对齐系统为什么要这样设计?
假设,堆区分配对象的内存大小,也按照
8字节
对齐。读取时,遇到多个连续存储的8字节
对象,容易出现野指针或内存越界访问再有,
NSObject
自身占8字节
,自定义对象一般来说也会有自定义的成员变量,所以自定义对象的大小,在大部分情况下,不会小于16字节
所以,在堆区分配对象的内存大小,
16字节
对齐为最好的选择
malloc
流程图
总结
instanceSize
分析
- 命中缓存,执行
fastInstanceSize
函数。编译器快速计算内存大小,进行16字节
对齐- 否则,执行
alignedInstanceSize
函数,进行8字节
对齐- 返回
size
,不能小于16字节
字节对齐
- 算法:
(x + N) & ~N
- 目的是以空间换取时间,提升
CPU
的访问速率- 选择
8字节
对齐,因为arm64
中,成员变量的数据类型最大占8字节
对象内存的影响因素
- 对象的成员变量,影响其内存大小
- 打印对象的内存数据
◦x
指令
◦View Memory
◦x/nfu
指令iOS
为小端模式,内存的读取从右往左- 对象
isa
,和ISA_MASK
进行&
运算,才能正常打印- 浮点类型,需要进行格式化,才能正常打印
◦e -f f -- xxx
◦p/f xxx
结构体内存对齐
- 存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩的整数倍开始
- 如果包含结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储
- 收尾⼯作,必须是其内部最⼤成员的整数倍,不⾜的要补⻬
获取内存大小的三种方式
sizeof
:得到的大小,即是该类型占用的空间大小class_getInstanceSize
:获取类的实例对象所占用的内存大小,采用8字节
对齐,参照对象的属性大小malloc_size
:获取系统实际分配的内存大小,采用16字节
对齐,参照整个对象的大小
malloc
分析
segregated_size_to_fit
:计算内存大小segregated_next_block
:开辟内存空间- 内存对齐的算法:
(size + 15) >> 4 << 4
- 对象的内存大小以
16字节
对齐,有效减少野指针和内存越界访问的情况。自定义对象的大小,在大部分情况下,不会小于16字节