文章

OC的内存分配,并使用LLDB操作指针来进行窥探

起源

面试的时候,iOS面试题已经不局限在OC层面了,会问到很多内存相关的C层面的问题。

问你类占内存大小

@interface ClassA : NSObject {
    int a;
    int b;
}
@end
class_getInstanceSize(ClassA.self) // 64为机型就是 16, 32位机型就是12

占内存大小16个字节,其中isa占8个字节,a和b各占4个字节,总共16字节,如果是32位机型就是12字节,isa占4个字节。

注释掉一个b还是占用16,因为是按8进行对齐的,实际总共12,对齐到16。再注释掉a,就是8字节。

增加一个属性@property (nonatomic, assign) int c; // 则变成24,实际是20,向8对齐。

增加NSString *d属性,是8字节。然后再算对齐。

增加一个函数方法,不增加内存大小。

考察内存对齐

NSInteger a = 10;
NSInteger b = 20;
&b-&a = -1;

内存分配是高地址向低地址分配,所以

(lldb) p &a
(NSInteger *) $12 = 0x00007ffee21a3578
(lldb) p &b
(NSInteger *) $13 = 0x00007ffee21a3570

所以 &b-&a = -8byteCode,实际输出-8 / 8 = -1

同类指针(当然也只有同类指针允许相减,如a和b)相减得到的整数值,等于两指针减的距离除以sizeof(声明指针的类型),然后取整(此处即(&b - &a) / sizeof(NSInteger)) = -1)

我们再看个内存对齐的占用情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct myStruct1 {
    char a; // 1
    double b; //8
    int c; //4
    short d; //2
}myStruct1;
struct myStruct2 {
    double b;//8
    int c;//4
    short d;//2
    char a;//1
}myStruct2;
NSLog(@"%lu-%lu",sizeof(myStruct1),sizeof(myStruct2));// 24-16

struct myStruct3 {
    double b;//8
    int c;//4
    short d;//2
    char a;//1
    struct myStruct2 str;
}myStruct3;
NSLog(@"%lu",sizeof(myStruct3));//32

内存动态分配

iOS中的对象都是使用动态内存分配的,即在运行时动态分配内存。对象的内存分配首先通过alloc方法分配对象存储空间,然后通过init方法对对象进行初始化。

当我们调用一个类的alloc方法时,它会在内存中分配一块足够大的地方,用来存储该类的实例变量。大小由类中的实例变量的数量和类型决定。

分配的内存包括一个isa指针,指向该对象的类,以及实例变量所占用的内存,isa指针是指向其类的指针,他是一个指针变量,因此在分配内存时为骑分配内存空间。

alloc只分配内存,不初始化实例变脸。如果没有调用init方法,实例变量可能包含未定义的值。

使用LLDB进行查看

NSInteger a = 10;
NSInteger b = 20;

// 这里的内存是由低到高布局的,每个相差sizeof(int)的偏移量
int c[3] = {0, 1, 2};
int d[3] = {0, 1, 2};

NSInteger e = 30;
char f = 'a';
long g = 40;
int h = 10;
NSLog(@"111");

LGPerson *p = [[LGPerson alloc] init];
p.name=@"lb";
p.nickName=@"LB";
p.age=18;
p.height=180;

我们使用LLDB看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
(lldb) p &a
(NSInteger *) $12 = 0x00007ffee21a3578
(lldb) p &b
(NSInteger *) $13 = 0x00007ffee21a3570
(lldb) p &c
(int (*)[3]) $14 = 0x00007ffee21a359c
(lldb) p c
(int [3]) $15 = ([0] = 0, [1] = 1, [2] = 2)
(lldb) p *c
(int) $16 = 0
(lldb) p &c + 1 // + 1 偏移的就是c的大小也就是12,超出数组了,数组布局到11
(int (*)[3]) $17 = 0x00007ffee21a35a8
(lldb) p *$17 // 所以这里错乱了
(int [3]) $18 = ([0] = -1254948660, [1] = 1745639534, [2] = -501598080)
(lldb) p d
(int [3]) $19 = ([0] = 0, [1] = 1, [2] = 2)
(lldb) p &d
(int (*)[3]) $20 = 0x00007ffee21a3590
(lldb) p &d + 1
(int (*)[3]) $21 = 0x00007ffee21a359c
(lldb) p &e
(NSInteger *) $22 = 0x00007ffee21a3568
(lldb) p &f
(char *) $23 = 0x00007ffee21a3567 "a\U0000001e"
(lldb) p &g
(long *) $24 = 0x00007ffee21a3558
(lldb) p &g
(long *) $25 = 0x00007ffee21a3558
(lldb) p &h
(int *) $26 = 0x00007ffee21a3554
(lldb) p p
(LGPerson *) $27 = 0x0000600003857040
(lldb) x/8gx 0x600003857040 // x内存查看命令,显示8段,g按8字节分,x按16进制显示
0x600003857040: 0x000000010debb040 0x000000b400000012
0x600003857050: 0x000000010de42578 0x000000010de42598
0x600003857060: 0x0000d3c828ee7060 0x3030367830200195
0x600003857070: 0x3430373538333030 0x0000000000003e30
(lldb) po 0x000000b400000012 // 这里打印出来的不是lb,因为触发了内存优化,进行了属性重拍
773094113298
(lldb) po 0x000000010de42578
lb
(lldb) po 0x000000010de42598
LB
(lldb) po 0x000000b4 // 进行了属性重拍优化内存
180
(lldb) po 0x00000012
18
(lldb) x/8wx p // x内存查看命令,显示8段,w按4字节分,x按16进制显示
0x600003857040: 0x0debb040 0x00000001 0x00000012 0x000000b4
0x600003857050: 0x0de42578 0x00000001 0x0de42598 0x00000001

参考文章

iOS动态调试学习使用lldb的x命令读取内存

OC 底层原理(7)- 类原理(类的属性存储,类的方法存储)(随记)有很多操作指针读取类属性存储方式的案例。

从源代码看 ObjC 中消息的发送文章的末尾也有使用地址来做处理的手段

通过Runtime源码了解Objective-C中的方法存储操作获取class_rw_t中的methods

Runtime 一: OC 方法的底层数据结构和缓存机制

OC内存对齐原理

本文由作者按照 CC BY 4.0 进行授权