1. instanceSize分析
instanceSize
函数是alloc
的核心方法之一,负责计算内存大小
1.1 探索objc
源码
1.1.1 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字节
1.1.2 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
1.1.3 align16
函数
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
16字节
对齐算法
1.1.4 alignedInstanceSize
函数
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
unalignedInstanceSize
:得到未对齐的实例对象大小word_align
:8字节
对齐
1.1.5 unalignedInstanceSize
函数
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
1.1.6 word_align
函数
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
8字节
对齐算法
WORD_MASK
定义:
# define WORD_MASK 7UL
1.2 instanceSize
流程图
2. 字节对齐
2.1 字节对齐算法
在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
2.2 为什么需要字节对齐?
内存以字节为基本单位,当CPU
存取数据时,以块为单位
读取未对齐数据,需要多次访问内存,极大降低CPU
的性能
如果数据存储在自然对齐的位置上,可以降低CPU
的存取次数。以空间换取时间,提升CPU
的访问速率
为什么是8字节
对齐?
在arm64
中,成员变量的数据类型最大占8字节
如果对齐规则大于8字节
,会造成内存空间的浪费。如果小于8字节
,读取占8字节
的数据类型,需要多次访问内存
故此,对齐规则为8字节
是最好的选择
3. 对象内存的影响因素
3.1 影响因素
对象的成员变量会影响其内存大小,实例方法和类方法,不会对其产生影响。而属性的本质是getter/setter
方法,所以也不会影响
在堆区分配的内存空间,首先存储对象isa
,之后依次排列对象的成员变量
3.2 打印内存数据
案例:
打开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
中,打印对象的内存数据
3.2.1 使用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
为对象的首地址
3.2.2 使用View Memory
LGPerson
对象的isa
和5个
成员变量
3.2.3 使用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
4. 结构体内存对齐
4.1 内存对⻬的原则
- 数据成员对⻬规则:结构(
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
5. 获取内存大小的三种方式
5.1 sizeof
sizeof
不是函数,而是一个操作符- 一般会传入数据类型,编译器在编译时期即可确定大小
sizeof
得到的大小,即是该类型占用的空间大小
5.2 class_getInstanceSize
class_getInstanceSize
是runtime
提供的api
- 作用:获取类的实例对象所占用的内存大小
- 本质:获取实例对象中成员变量的内存大小
- 采用
8字节
对齐,参照对象的属性大小
5.3 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
类型,本质是objc_class
类型的结构体指针,占8字节
typedef struct objc_class *Class;
objc_class
继承自最原始的objc_object
结构体
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
};
找到objc_object
结构体,只有一个成员变量isa
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
所以,万物皆对象,万物皆有isa
6. malloc分析
malloc
函数是alloc
的核心方法之一,负责开辟内存空间
项目中,只能找到malloc_size
的方法定义,它的代码实现在libmalloc
源码中
6.1 探索libmalloc
源码
进入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
函数,计算出16字节
内存对齐后的大小
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
进入segregated_next_block
函数,开辟内存空间
- 堆区开辟的空间是不连续的,期间可能因多线程、小于最大限制地址等原因,需要重新尝试
while
。当开辟空间成功,返回指针地址
6.2 内存对齐算法
在segregated_size_to_fit
函数中,内存对齐的算法为:
(size + 15) >> 4 << 4
算法作用为
16字节
对齐,保证分配的内存大小,必须是16
的整数倍,与算法(x + N) & ~N
有异曲同工之妙假设:传入的
size
为40
size + 15 = 55
,0011 0111
右移
4位
,0000 0110
左移
4位
,0011 0000
转换
10
进制为48
结构体内部,成员变量以8字节
对齐。但是在堆区分配对象的内存大小,以16字节
对齐
系统为什么要这样设计?
假设,堆区分配对象的内存大小,也按照8字节
对齐。读取时,遇到多个连续存储的8字节
对象,容易出现野指针或内存越界访问
再有,NSObject
自身占8字节
,自定义对象一般来说也会有自定义的成员变量,所以自定义对象的大小,在大部分情况下,不会小于16字节
所以,在堆区分配对象的内存大小,16字节
对齐为最好的选择
6.3 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字节