iOS alloc底层原理:内存分布

354 阅读9分钟

对象的本质其实就是结构体(可以参考iOS对象的本质,本文中就不多做论述了。)所以在分析对象内存分布时,也会对结构体内存对齐进行一些讲解,以便于更容易去理解对象内存分布以及内存优化。

一、对象内存影响因素

1、打印内存大小的三种方式

1.1、sizeof

  • sizeof:是一个运算符。
  • 用来获取类型(int、结构体、指针等)的大小
  • 这些数值在编译时生成,运行时直接获取。

1.2、class_getInstanceSize

  • 计算类最少需要的大小。
  • 一般以8字节对齐。
  • 使用是需要声明#import <objc/runtime.h>。

1.3、malloc_size

  • 堆空间实际分配给对象的内存大小。
  • 在Mac、iOS中总大小以16字节对齐。
  • 使用时需要声明#import <malloc/malloc.h>。

2、验证影响因素

2.1、属性

(1)、创建一个继承于NSObject的JLObject类,

@interface JLObject : NSObject

@end

(2)、记录其内存占用,实际使用8字节,对象开辟空间16字节。

截屏2021-06-09 下午10.36.18.png (3)、添加一个属性name。

@interface JLObject : NSObject
@property (nonatomic, copy) NSString *name;
@end

(4)、打印内存大小,发现实际使用变成了16字节字节,开辟空间大小不变。 截屏2021-06-09 下午4.04.54.png

2.2、成员变量

(1)、在NSObject中添加一个nickName成员变量

@interface JLObject : NSObject{
    NSString * nickName;
}
@property (nonatomic, copy) NSString *name;
@end

(2)、打印结果:增加一个字符串类型的ivar,实际使用24字节,超过了16字节,对象开辟空间翻倍变为48字节。

截屏2021-06-09 下午4.08.03.png

2.3、方法

(1)、为NSObject添加一个hello方法

@interface JLObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
- (void)hello;
@end

@implementation JLObject
- (void)hello{}
@end

(2)、如图打印的结果依然的24,

所以添加方法并不会对对象的内存造成影响(实例方法存在于类里,类方法存在于元类中)。

截屏2021-06-09 上午11.19.07.png

属性=成员变量+setter、getter方法,由于方法不会影响,所以属性对对象内存的影响其实是ivar造成的。

2.4、isa

当我们创建对象时,打印结果显示,已经使用了8字节的空间,那这8字节是哪里来的。进入到NSObject中,发现它有一个成员变量isa,在64位的系统上isa是一个64位的指针,占用8字节。 截屏2021-06-10 下午9.11.03.png

3、结论

对象实际使用的内存大小由ivars决定。

二、结构体

1、各类型所占字节长度

signed char.png

2、结构体

结构体是由一批数据组合而成的一种新的数据类型。组成结构型数据的每个数据称为结构型数据的“成员”(引用于百度百科)。所占内存长度是各成员占的内存长度的总和。

2.1、结构体内存对齐原则

  • 1:第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。
  • 2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)。
  • 3:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。

举个栗子:

struct JLObject {
    double a;            [0-7]    //8字节,从0开始,   放到[0-7]。
    char b;	         [8]      //1字节,8可以整除1,放到[8]。
    int c;	         [12-15]  //4字节,9不能整除4,找到距离最近可整除4的数为12,放到[12-15].
    short d;	         [16-17]  //2字节,16可整除2, 放到[16-17]
}object;                 总大小:24//实际占用18个字节,总大小应该是最大成员的整数倍,所以此处为24

//结构体嵌套的情况
struct JLObject1 {
    double a;	         [0-7]    //8字节,起始位置为0,放到[0-7]
    int b;        	 [8-11]   //4字节,8可以整除4, 放到[8-11]
    char c;		 [12]     //1字节,12可以整除1,放到[12]
    short d;		 [14-15]  //2字节,13不能整除2,下一个整倍数14,所以放到[14-15]
    int e;		 [16-19]  //4字节,16可以整除4,所以放到[16-19]
    struct JLObject obj; [24-47] //最大元素double8字节,所以要从下一个8的倍数24开始,JLObject总大小是24,所以 结果是[24-47]
}object1; 		 总大小:48

打印验证一下:

截屏2021-06-09 下午2.29.55.png

2.2、对齐原因

提高访问速度:
  • 未对齐的内存,处理器需要做多次内存访问,而对齐的内存仅需要一次。 如图,因为内存未进行对齐,所以cpu不能准确定位到内存边界,可能就会出现下面的情况,先读取一次内存,将2、3、4数据放进寄存器,再读取一次内存,将5放进寄存器,还需要把多余的数据剔除,这会大大降低访问效率。

截屏2021-06-10 下午3.07.48.png

平台移植:
  • 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2.3、内存优化

不同的排列占用内存大小也不同,系统对对象也有相应的优化,后面具体分析。

截屏2021-06-09 下午7.22.11.png

重排后内存占用由24缩减到16。 截屏2021-06-09 下午7.24.46.png

  • 由于内存对齐的影响,完全相同的类型组合,会因为排列不同,而影响到最终的内存占用。

三、LLDB指令

po:输出值 或者 对象的地址
p/f(或e -f f --):输出浮点型的值
x/nuf <addr>
  x:读取内存的命令。
  n:表示要显示的内存单元的个数
  u:表示一个地址单元的长度:
    b(byte):表示单字节
    h(half word):表示双字节
    w(word):表示四字节
    g(giant word):表示八字节
  f:表示显示方式,可取如下值:
    x:十六进制格式
    d:十进制格式
    u:十进制格式无符号整型
    o:八进制格式
    t:二进制格式
    a:十六进制格式
    i:指令地址格式
    c:字符格式
    f:浮点数格式

四、对象内存分析

1、创建对象

//创建JLObject与其子类JLStudent,并为之添加占用不同内存长度的属性,如char、short、int、double、NSString等,用来验证不同类型在对象中的排列规则。

@interface JLObject : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
@property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic) double height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end

@interface JLStudent : JLObject
@property (nonatomic, copy) NSString *studentId;
@property (nonatomic) char c;
@property (nonatomic) short s;
@property (nonatomic) int  ranking;
@property (nonatomic) int  grade;
@end

//创建对象并赋值

JLStudent *obj = [JLStudent alloc];
obj.name      = @"zjl";
obj.nickName  = @"好想秃顶";
obj.age       = 18;
obj.height    = 180.0;
obj.a         = 'a';
obj.b         = 'b';
obj.studentId = @"12345678";
obj.c         = 'c';
obj.ranking   = 1;
obj.grade     = 6;
obj.s         = 2;

2、打印结果并分析

//打印obj内存

(lldb) x/10gx obj
0x600000944b40: 0x0000000106369b18 0x0000001200006261
0x600000944b50: 0x0000000106364038 0x0000000106364058
0x600000944b60: 0x0000000000000000 0x4066800000000000
0x600000944b70: 0x0000000100020063 0x0000000000000006
0x600000944b80: 0x0000000106364078 0x0000000000000000

按顺序依次将地址所指向的内容打印出来

注意: 系统对内存进行了重新排列,以降低结构体对内存的占用;iOS打印内存为小端模式,所以需要倒过来看:

  • 0x0000001200006261实际包含了0x61、0x62、0x00000012。
  • 0x0000000100020063则需要拆分为0x63、0x2、0x00000001.

从打印结果我们可以发现,首先第一个位置是isa,然后存储都是父类JLObject中的数据,最后是JLStudent中的数据。

(lldb) po 0x0000000106369b18          JLObject //isa指针

//JLObject
(lldb) po 0x61                        97 //char:a = 'a'(参看ASCII码)
(lldb) po 0x62                        98//char:b = 'b'(参看ASCII码)
(lldb) po 0x00000012                  18 //int:age
(lldb) po 0x0000000106364038          zjl// NSString:name
(lldb) po 0x0000000106364058          好想秃顶//NSString:nickName
(lldb) e -f f -- 0x4066800000000000   (long) $13 = 180 //double:height,打印浮点数需要用到e -f f --或p/f

//JLStudent
(lldb) po 0x63                        99//char:c = 'c'(参看ASCII码)
(lldb) po 0x2                         2 //short:s
(lldb) po 0x00000001                  1 //int:ranking
(lldb) po 0x0000000000000006          6 //int:grade
(lldb) po 0x0000000106364078          12345678 //NSString:studentId

3、结论:

  • 对象内存受isa与ivars影响。
  • 对象内存中的排列顺序依次为isa、各级父类的ivar、本类的ivar。
  • 排列时系统会对结构体内存进行优化重排(降低结构体占用内存的大小):
    • 类型先短后长,如上述打印按照char、short、int,NSString的优先级排列,
    • 相同长度的按照书写顺序排列,如父类JLObject中的name、nickName、height,

截屏2021-06-09 下午3.02.17.png