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:额外字节数,传入的值为0size:不能小于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,否则返回0extra:额外字节数,传入的值为0FAST_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打印
isapo 0x011d80010000834d & 0x00007ffffffffff8 ------------------------- LGPerson
标记2:8字节中存储了age和age1两个属性
age为int类型,占4字节
age1为bool类型,占1字节两个属性的大小之和,未超过
8字节。为了避免内存的浪费,系统做了内存对齐优化,将两个属性并存到一个8字节中打印
agepo 0x00000012 ------------------------- 18打印
age1po 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位置- 最后进行收尾⼯作,满足内部最⼤成员的整数倍,补⻬至
24NSLog(@"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位置- 最后进行收尾⼯作,满足内部最⼤成员的整数倍,补⻬至
16NSLog(@"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 = 48NSLog(@"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字节








网友评论