1. instanceSize分析

instanceSize函数是alloc的核心方法之一,负责计算内存大小

1.1 探索objc源码

1.1.1 instanceSize函数

  1. inline size_t instanceSize(size_t extraBytes) const {
  2. if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
  3. return cache.fastInstanceSize(extraBytes);
  4. }
  5. size_t size = alignedInstanceSize() + extraBytes;
  6. if (size < 16) size = 16;
  7. return size;
  8. }
  • fastInstanceSize:编译器快速计算内存大小
  • alignedInstanceSize:得到对齐后的实例对象大小
  • extraBytes:额外字节数,传入的值为0
  • size:不能小于16字节

1.1.2 fastInstanceSize函数

  1. size_t fastInstanceSize(size_t extra) const {
  2. ASSERT(hasFastInstanceSize(extra));
  3. if (__builtin_constant_p(extra) && extra == 0) {
  4. return _flags & FAST_CACHE_ALLOC_MASK16;
  5. } else {
  6. size_t size = _flags & FAST_CACHE_ALLOC_MASK;
  7. return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
  8. }
  9. }
  • __builtin_constant_pGCC的内建函数。用于判断一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0
  • extra:额外字节数,传入的值为0
  • FAST_CACHE_ALLOC_DELTA16:来自setFastInstanceSize方法的8字节
  • align1616字节对齐

FAST_CACHE_ALLOC_DELTA16定义:

  1. #define FAST_CACHE_ALLOC_DELTA16 0x0008

1.1.3 align16函数

  1. static inline size_t align16(size_t x) {
  2. return (x + size_t(15)) & ~size_t(15);
  3. }
  • 16字节对齐算法

1.1.4 alignedInstanceSize函数

  1. uint32_t alignedInstanceSize() const {
  2. return word_align(unalignedInstanceSize());
  3. }
  • unalignedInstanceSize:得到未对齐的实例对象大小
  • word_align8字节对齐

1.1.5 unalignedInstanceSize函数

  1. uint32_t unalignedInstanceSize() const {
  2. ASSERT(isRealized());
  3. return data()->ro()->instanceSize;
  4. }

1.1.6 word_align函数

  1. static inline uint32_t word_align(uint32_t x) {
  2. return (x + WORD_MASK) & ~WORD_MASK;
  3. }
  • 8字节对齐算法

WORD_MASK定义:

  1. # define WORD_MASK 7UL

1.2 instanceSize流程图
image.png

2. 字节对齐

2.1 字节对齐算法

word_align函数中,字节对齐的算法为:

  1. (x + N) & ~N
  • &:与运算,都是1结果为1,反之为0
  • ~:取反,1变为00变为1

例如:8字节对齐,N必须为7

假设:传入的x10

x + N = 170001 0111

~N7取反,1111 1000

0001 0111 & 1111 1000 = 0001 0000

转换10进制16

2.2 为什么需要字节对齐?


内存以字节为基本单位,当CPU存取数据时,以块为单位

读取未对齐数据,需要多次访问内存,极大降低CPU的性能

如果数据存储在自然对齐的位置上,可以降低CPU的存取次数。以空间换取时间,提升CPU的访问速率
为什么是8字节对齐?

arm64中,成员变量的数据类型最大占8字节
image.png

如果对齐规则大于8字节,会造成内存空间的浪费。如果小于8字节,读取占8字节的数据类型,需要多次访问内存

故此,对齐规则为8字节是最好的选择

3. 对象内存的影响因素

3.1 影响因素

对象的成员变量会影响其内存大小,实例方法和类方法,不会对其产生影响。而属性的本质是getter/setter方法,所以也不会影响

在堆区分配的内存空间,首先存储对象isa,之后依次排列对象的成员变量

3.2 打印内存数据

案例:

打开LGPerson.h文件,写入以下代码:

  1. @interface LGPerson : NSObject
  2. @property (strong,nonatomic) NSString *name;
  3. @property (strong,nonatomic) NSString *nick;
  4. @property (assign,nonatomic) int age;
  5. @property (assign,nonatomic) bool age1;
  6. @property (assign,nonatomic) double height;
  7. @end


打开main.m文件,写入以下代码:

  1. int main(int argc, const char * argv[]) {
  2. @autoreleasepool {
  3. LGPerson *per= [LGPerson alloc];
  4. per.name = @"Zang";
  5. per.nick = @"Z";
  6. per.age = 18;
  7. per.age1 = 1;
  8. per.height = 180.0;
  9. NSLog(@"%@",per);
  10. }
  11. return 0;
  12. }

lldb中,打印对象的内存数据

3.2.1 使用x命令

  1. x per
  2. -------------------------
  3. 0x10072d690: 4d 83 00 00 01 80 1d 01 01 00 00 00 12 00 00 00 M...............
  4. 0x10072d6a0: 30 40 00 00 01 00 00 00 50 40 00 00 01 00 00 00 0@......P@......
  • xmemory read指令的简写,作用是内存读取并打印
  • iOS为小端模式,内存的读取从右往左
  • 0x10072d690为对象的首地址

3.2.2 使用View Memory

image.png

LGPerson对象的isa5个成员变量
image.png

3.2.3 使用x/nfu命令

  1. x/6g per
  2. -------------------------
  3. 0x10072d690: 0x011d80010000834d 0x0000001200000001
  4. 0x10072d6a0: 0x0000000100004030 0x0000000100004050
  5. 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:浮点数格

成员变量的存储结构:
image.png

  • 最前面0x10072d690为对象首地址,后面的0x011d80010000834d是成员变量的值,0x10072d690地址指向0x011d80010000834d的值

标记1:存储对象isa,和ISA_MASK进行&运算,才能正常打印

ISA_MASK定义:

  1. # define ISA_MASK 0x00007ffffffffff8ULL


打印isa

  1. po 0x011d80010000834d & 0x00007ffffffffff8
  2. -------------------------
  3. LGPerson

标记28字节中存储了ageage1两个属性

ageint类型,占4字节

age1bool类型,占1字节

两个属性的大小之和,未超过8字节。为了避免内存的浪费,系统做了内存对齐优化,将两个属性并存到一个8字节

打印age

  1. po 0x00000012
  2. -------------------------
  3. 18


打印age1

  1. po 0x00000001
  2. -------------------------
  3. 1

标记3:存储name属性

  1. po 0x0000000100004030
  2. -------------------------
  3. Zang

标记4:存储nick属性

  1. po 0x0000000100004050
  2. -------------------------
  3. Z

标记5:存储height属性。heightdouble类型,需要进行格式化打印

  1. e -f f -- 0x4066800000000000
  2. -------------------------
  3. 180


或者

  1. p/f 0x4066800000000000
  2. -------------------------
  3. 180

4. 结构体内存对齐

4.1 内存对⻬的原则

  • 数据成员对⻬规则:结构(struct)或联合(union)的数据成员,第⼀个数据成员放在offset0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,例如:数组、结构体等)的整数倍开始
    ◦ 例如:int4字节,则要从4的整数倍地址开始存储。如果当前开始存储的位置为9,需要空出91011,在12的位置才可存储
  • 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储
    ◦ 例如:struct a⾥存有struct bb⾥有charintdouble等元素,那b应该从8的整数倍开始存储
  • 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,必须是其内部最⼤成员的整数倍,不⾜的要补⻬

案例1

  1. struct LGStruct1 {
  2. double a;
  3. char b;
  4. int c;
  5. short d;
  6. }struct1;
  • a8字节,存储在0~7位置
  • b1字节,存储在8位置。因为81的倍数,满足条件
  • c4字节9~11都不是4的倍数,无法存储,将其空出。所以c存储在12~15位置
  • d2字节,存储在16~17位置
  • 最后进行收尾⼯作,满足内部最⼤成员的整数倍,补⻬至24
  1. NSLog(@"struct1:%lu",sizeof(struct1));
  2. -------------------------
  3. struct124

案例2

  1. struct LGStruct2 {
  2. double a;
  3. int b;
  4. char c;
  5. short d;
  6. }struct2;
  • a8字节,存储在0~7位置
  • b4字节,存储在8~11位置
  • c1字节,存储在12位置
  • d2字节,13不是2的倍数,无法存储,将其空出。所以d存储在14~15位置
  • 最后进行收尾⼯作,满足内部最⼤成员的整数倍,补⻬至16
  1. NSLog(@"struct2:%lu",sizeof(struct2));
  2. -------------------------
  3. struct216

案例3

  1. struct LGStruct3 {
  2. double a;
  3. int b;
  4. char c;
  5. short d;
  6. int e;
  7. struct LGStruct1 str;
  8. }struct3;
  • a8字节,存储在0~7位置
  • b4字节,存储在8~11位置
  • c1字节,存储在12位置
  • d2字节,13不是2的倍数,无法存储,将其空出。所以d存储在14~15位置
  • e4字节,存储在16~19位置
  • str为结构体类型,最大成员占8字节。包含结构体成员,从其内部最⼤元素⼤⼩的整数倍地址开始存储。所以str的起始位置为24str结构体内存对齐后占24字节,所以LGStruct3的大小为24 + 24 = 48
  1. NSLog(@"struct3:%lu",sizeof(struct3));
  2. -------------------------
  3. struct348

5. 获取内存大小的三种方式

5.1 sizeof

  • sizeof不是函数,而是一个操作符
  • 一般会传入数据类型,编译器在编译时期即可确定大小
  • sizeof得到的大小,即是该类型占用的空间大小

5.2 class_getInstanceSize

  • class_getInstanceSizeruntime提供的api
  • 作用:获取类的实例对象所占用的内存大小
  • 本质:获取实例对象中成员变量的内存大小
  • 采用8字节对齐,参照对象的属性大小

5.3 malloc_size

  • 作用:获取系统实际分配的内存大小
  • 采用16字节对齐,参照整个对象的大小
  • 实际分配的内存大小,必须是16的整数倍

案例


打开LGPerson.h文件,写入以下代码:

  1. @interface LGPerson : NSObject
  2. @property (nonatomic, copy) NSString *name;
  3. @property (nonatomic, copy) NSString *nickName;
  4. @property (nonatomic, assign) int age;
  5. @property (nonatomic, assign) long height;
  6. @end


打开main.m文件,写入以下代码:

  1. #import <Foundation/Foundation.h>
  2. #import "LGPerson.h"
  3. #import <objc/runtime.h>
  4. #import <malloc/malloc.h>
  5. int main(int argc, const char * argv[]) {
  6. @autoreleasepool {
  7. LGPerson *person = [LGPerson alloc];
  8. NSLog(@"sizeof:%lu",sizeof(person));
  9. NSLog(@"class_getInstanceSize:%lu",class_getInstanceSize([LGPerson class]));
  10. NSLog(@"malloc_size:%lu",malloc_size((__bridge const void *)(person)));
  11. }
  12. return 0;
  13. }
  14. -------------------------
  15. //打印结果:
  16. sizeof8
  17. class_getInstanceSize40
  18. malloc_size48
  • sizeof8,因为person对象,本质是指针地址,占8字节
  • class_getInstanceSize40LGPerson的成员变量大小为36字节8字节对齐后,占40字节
  • malloc_size48,系统分配的内存大小,经过16字节对齐后,占48字节

扩展内容

LGPerson中,即使没有任何成员变量,class_getInstanceSize依然会占8字节

因为LGPerson继承自NSObject,默认存在isa成员变量

  1. @interface NSObject <NSObject> {
  2. #pragma clang diagnostic push
  3. #pragma clang diagnostic ignored "-Wobjc-interface-ivars"
  4. Class isa OBJC_ISA_AVAILABILITY;
  5. #pragma clang diagnostic pop
  6. }


isaClass类型,本质是objc_class类型的结构体指针,占8字节

  1. typedef struct objc_class *Class;


objc_class继承自最原始的objc_object结构体

  1. struct objc_class : objc_object {
  2. objc_class(const objc_class&) = delete;
  3. objc_class(objc_class&&) = delete;
  4. void operator=(const objc_class&) = delete;
  5. void operator=(objc_class&&) = delete;
  6. // Class ISA;
  7. Class superclass;
  8. cache_t cache; // formerly cache pointer and vtable
  9. class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
  10. ...
  11. };

找到objc_object结构体,只有一个成员变量isa

  1. struct objc_object {
  2. Class _Nonnull isa OBJC_ISA_AVAILABILITY;
  3. };

所以,万物皆对象,万物皆有isa

6. malloc分析

malloc函数是alloc的核心方法之一,负责开辟内存空间

项目中,只能找到malloc_size的方法定义,它的代码实现在libmalloc源码中
image.png

6.1 探索libmalloc源码


进入calloc函数

  1. void *
  2. calloc(size_t num_items, size_t size) {
  3. return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
  4. }

进入_malloc_zone_calloc函数
image.png

源码中只能找到calloc的函数声明,但是无法进入

  1. 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
image.png

  • 找到函数的真身:default_zone_calloc


    在全局搜索calloc时,虽然找不到函数实现,但是找到了calloc赋值代码。有赋值必然会存储值,通过打印也许可以得到线索

或者,尝试Always Show Disassembly查看汇编代码
image.png

  • 也可得到相同线索:default_zone_calloc


    来到default_zone_calloc函数

    1. static void *
    2. default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size) {
    3. zone = runtime_default_zone();
    4. return zone->calloc(zone, num_items, size);
    5. }

又遇到了zone->calloc函数,继续使用lldb打印
image.png

来到nano_malloc函数
image.png

进入_nano_malloc_check_clear函数
image.png

  • segregated_size_to_fit:计算内存大小
  • segregated_next_block:开辟内存空间

进入segregated_size_to_fit函数,计算出16字节内存对齐后的大小

  1. static MALLOC_INLINE size_t
  2. segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey) {
  3. size_t k, slot_bytes;
  4. if (0 == size) {
  5. size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
  6. }
  7. k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
  8. slot_bytes = k << SHIFT_NANO_QUANTUM; // multiply by power of two quanta size
  9. *pKey = k - 1; // Zero-based!
  10. return slot_bytes;
  11. }

NANO_REGIME_QUANTA_SIZESHIFT_NANO_QUANTUM定义:

  1. #define SHIFT_NANO_QUANTUM 4
  2. #define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
  • 1左移4位,即:16

进入segregated_next_block函数,开辟内存空间
image.png

  • 堆区开辟的空间是不连续的,期间可能因多线程、小于最大限制地址等原因,需要重新尝试while。当开辟空间成功,返回指针地址

6.2 内存对齐算法


segregated_size_to_fit函数中,内存对齐的算法为:

  1. (size + 15) >> 4 << 4

算法作用为16字节对齐,保证分配的内存大小,必须是16的整数倍,与算法(x + N) & ~N有异曲同工之妙

假设:传入的size40

size + 15 = 550011 0111

右移4位0000 0110

左移4位0011 0000

转换10进制为48

结构体内部,成员变量以8字节对齐。但是在堆区分配对象的内存大小,以16字节对齐

系统为什么要这样设计?

假设,堆区分配对象的内存大小,也按照8字节对齐。读取时,遇到多个连续存储的8字节对象,容易出现野指针或内存越界访问

再有,NSObject自身占8字节,自定义对象一般来说也会有自定义的成员变量,所以自定义对象的大小,在大部分情况下,不会小于16字节

所以,在堆区分配对象的内存大小,16字节对齐为最好的选择

6.3 malloc流程图

image.png

总结

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字节