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:额外字节数,传入的值为0size:不能小于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,否则返回0extra:额外字节数,传入的值为0FAST_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 0x00000012000000010x10072d6a0: 0x0000000100004030 0x00000001000040500x10072d6b0: 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:8class_getInstanceSize:40malloc_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 vtableclass_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_tsegregated_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 quantaslot_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字节
