Objective-C(七)对象内存分析

导言

​ 本系列接《Effective Objective-C 2.0》一书中的系列文章。

接下来,会有以下系列的主题的文章,以实践探索Objective-C 2.0的一些特性,参考部分博文、官方文档、以及MJ小码哥的系列课程—非常推荐:

本文主要针对几个类来窥探实例对象在内存中的存储,我们从成员变量和属性入手,本文相关代码在这儿

我们平时编写的Objective-C代码,底层实现其实都是C\C++代码

屏幕快照 2018-11-08 下午7.24.53

所以Objective-C的面向对象都是基于C\C++的数据结构实现的

Objective-C的对象、类主要是基于C\C++的什么数据结构实现的——结构体。

一、 NSObject 占用多少内存

1
2
3
4
5
6
7
NSObject *obj = [[NSObject alloc] init];

// 获得NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zu", malloc_size((__bridge const void *)obj));

上面有两个函数:

创建一个实例对象,至少需要多少内存?

# import <objc/runtime.h>

class_getInstanceSize([NSObject class]);

创建一个实例对象,实际上分配了多少内存?

#import <malloc/malloc.h>

malloc_size((__bridge const void *)obj);

将Objective-C代码转换为C\C++代码

1
2
3
//如果需要链接其他框架,使用-framework参数。比如-framework UIKit
// xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

其中,我们可以发现NSObject转换为C++的底层结构体为:

1
2
3
4
//main.cpp
struct NSObject_IMPL {
Class isa; //typedef struct objc_class *Class;
};

我们直接通过断点调试也可以发现,obj的确只有一个isa成员变量。

2018-11-09_17-11-37

1.1 查看实时内存

下面我们通过查看obj对应的内存,来观察:

image-20181109171709554

从上图中可以看到,从0x1029000a0地址开始的8个字节,是有数据的,后面8个字节,都是0。

根据最开始打印的:

1
2
3
4
5
// 获得NSObject实例对象的成员变量所占用的大小 >> 8
NSLog(@"%zd", class_getInstanceSize([NSObject class]));

// 获得obj指针所指向内存的大小 >> 16
NSLog(@"%zu", malloc_size((__bridge const void *)obj));

我们猜测,前8个字节就是obj中isa占用的内存空间,后8个字节,是为了内存对齐而分配的填充字节。

1.2 结构体转换

为了验证这个猜测,我们将obj对象转换为对应的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct NSObject_IMPL {
Class isa;
};

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];

NSLog(@"%zd", class_getInstanceSize([NSObject class]));
NSLog(@"%zu", malloc_size((__bridge const void *)(obj)));

struct NSObject_IMPL *objImpl = (__bridge struct NSObject_IMPL *)obj;
NSLog(@"obj address: %p", obj);
NSLog(@"objImpl address: %p, objImpl isa: %p", objImpl, objImpl->isa);
NSLog(@"-----");
}
return 0;
}

再次运行,输出结果如下:

image-20181109180237592

从上图我们可以看出:

  • obj能转换为NSObject_IMPL结构体,地址一致;
  • isa的值为0x1dffffa4575141,与上一次运行一致,且只占8个字节。
  • obj最终占用16个字节,其中8个字节分配给isa,8个字节为内存对齐的填充字节。

在这里,为什么是16个字节,需要说明一下,iOS系统会给对象至少分配16*n字节的大小。

二、 更复杂的对象:BFPerson

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
@interface BFPerson : NSObject
{
@public
int _age;
int _male;
}
@property (nonatomic, assign) double height;
@end

@implementation BFPerson

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {

BFPerson *jack = [[BFPerson alloc] init];
jack->_age = 24;
jack->_male = 1;
jack.height = 185;
NSLog(@"jack age is %d, male: %d, height: %f", jack->_age, jack->_male, jack.height);

BFPerson *rose = [[BFPerson alloc] init];
rose->_age = 21;
rose->_male = 0;
rose.height = 165;
NSLog(@"rose age is %d, male: %d, height: %f", rose->_age, rose->_male, rose.height);

NSLog(@"%zd", class_getInstanceSize([BFPerson class]));
NSLog(@"%zd", malloc_size((__bridge const void *)jack));

}
return 0;
}

这一次,对象更复杂,而且继承了NSObject,那么其中实例对象中成员变量分配了多少字节,实际占用了多少自己呢?

最后输出:

image-20181109181326036

通过将上面代码转换为C++代码,我们可以得到BFPerson的结构:

其中,BFPerson_IMPL包含了NSObject_IMPL结构体,所以最后可以规整为:

image-20181109182017738

可以看到,第一个变量还是isa指针,后面跟着我们定义的两个成员变量,及定义的一个属性。

我们知道属性最后会转换一个对应的成员变量,所以总共有三个成员变量。

其中isa,我们知道是一个占用8字节,所以得到下面的各个成员变量的占用字节数:

image-20181109182338345

这和我们打印该实例对象占用的为24个字节相符,但是系统还是给它分配了32个字节。

我们通过两种方式来查看内存中的实例对象,是不是按我们预想的方式存储这些成员变量。

2.1 实时查看内存

如下;

image-20181110151749966

  • 查看BFPerson类,jack对象地址0x100683240
  • jack实例对象成员对象占用24个字节,系统分配32个字节。
    • 实例对象起始8个字节为:71 12 00 00 01 80 1D 00,为isa值;
    • 接着4个字节:18 00 00 00,为_age值,即_age = 24;
    • 接着4个字节:01 00 00 00,为_male值,即_male=1;
    • 紧接着8个字节:00 00 00 00 00 20 67 40,为浮点数185的IEEE表示,即height=185;
    • 最后8个字节为填充字节
      • 应用于系统对齐,均为0:00 00 00 00 00 00 00 00;
      • 非结构体内存对齐,结构体内存对齐,指结构体的内存占用是其中最大内存占用的变量的整数倍,其中isa占8个字节,但结构体最后占用24字节,已经是其整数倍。
  • Mac CPU是小端模式。
  • 同样可观察rose对象是否符合预期,不赘述。

2.2 结构体转换

我们通过前面转换C++代码结构体的分析,将jack转换为我们自定义的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

struct NSObject_IMPL {
Class isa;
};

struct BFPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
int _male;
double _height;
};

struct BFPerson_IMPL *jackImpl = (__bridge struct BFPerson_IMPL *)jack;
NSLog(@"jack age is %d, male: %d, height: %f", jackImpl->_age, jackImpl->_male, jackImpl->_height);

输出如下,可以看到结果完全一致,所以我们的符合我们的猜想。

image-20181110152805292

三、继承的对象:BFProgrammer

继承关系如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface BFPerson : NSObject
{
@public
int _age;
int _male;
}
@property (nonatomic, assign) double height;
@end

@implementation BFPerson
@end

@interface BFProgrammer : BFPerson
{
@public
char *company;
}
@end

@implementation BFProgrammer
@end

测试代码如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BFPerson *jack = [[BFPerson alloc] init];
jack->_age = 24;
jack->_male = 1;
jack.height = 185;
NSLog(@"jack age is %d, male: %d, height: %f", jack->_age, jack->_male, jack.height);
NSLog(@"jack instance size: %zd", class_getInstanceSize([BFPerson class]));
NSLog(@"jack malloc size: %zd", malloc_size((__bridge const void *)jack));

BFProgrammer *tony = [[BFProgrammer alloc] init];
tony->_age = 28;
tony->_male = 1;
tony.height = 178;
tony->company = "Google";
NSLog(@"tony age is %d, male: %d, height: %f, company: %s", tony->_age, tony->_male, tony.height, tony->company);

NSLog(@"tony instance size:%zd", class_getInstanceSize([BFProgrammer class]));
NSLog(@"tony malloc size: %zd", malloc_size((__bridge const void *)tony));

对应的结果:

image-20181110170304953

3.1 结构体

image-20181110194752711

下面我们分析下结果:

对于tony这个程序员:

  • 其成员变量大小为32字节,相对于jack这个BFPerson,多了8字节的内存变量。那么这8个字节,用于存放char *company的指针。

    • 最后实例内存变量占用32字节,系统实际分配也为32字节。

3.2 company 的内存分析

我们现在更进一步,从内存中直接读取tony的公司名称:Google。

3.2.1 Google 的ASCII字符

Google是个C语言字符串常量,其存储在内存中,采用ASCII字符编码,其最后的结构为:

从上面图中,我们发现tony所在地址为0x103300700,根据其结构体,我们可以知道company所在地址为:

compyan地址 = 0x103300700 + isa+ _age + _male + _height

​ = 0x103300700 + 8 + 4 + 4 + 8

= 0x103300718

3.2.2 lldb查看内存

对应的指令如下:

1
2
3
4
5
6
(lldb) x/2wx 0x103300718
0x103300718: 0x00000f52 0x00000001
(lldb) x/4wx 0x0000000100000f52
0x100000f52: 0x676f6f47 0x7400656c 0x20796e6f 0x20656761
(lldb) x/4wx 0x103300700
0x103300700: 0x00001391 0x001d8001 0x0000001c 0x00000001

3.2.3 分析

image-20181110192444681

  • 32字节成员变量,均符合分析;
  • company字符指针地址为0x0000000100000f52,指向Google字符串,Google占7个字节,注意是7个字节,最后一个字符为’\0’;
  • 注意大小端的书写方式,不要写反,或者读错。
    • lldb显示的为正常可读的大端模式,及高位显示高字节,低位显示低字节;
    • 内存中显示未小端模式,52 0F 00 00 01 00 00 00,改为我们正常的读写大端模式:0x0000000100000f52。即从右向左读取每个字节即可。

四、源码分析

下面我们分析上面我们常用的打印语句:

1
2
NSLog(@"%zd", class_getInstanceSize([NSObject class]));
NSLog(@"%zu", malloc_size((__bridge const void *)(obj)));

alloc以及class_getInstanceSize源码z在Apple souce objc4库中。

4.1 alloc

[NSObject alloc]代码调用流程:

image-20181110212729733

  • calloc返回的为最后内存分配的字节数
    • _class_createInstanceFromZone中的size是从instanceSize获取;
    • _class_createInstanceFromZone中的size 是实例变量占用的空间,不是最后分配的内存空间;
    • _class_createInstanceFromZone中的size 是经过结构体字节对齐的空间(instanceSize为字节对齐的空间);
  • instanceSize获取的是alignedInstanceSize
    • alignedInstanceSize小于16字节,会补齐为16字节。
      • _class_createInstanceFromZone中size = 16
    • alignedInstanceSize大于16字节,直接返回alignedInstanceSize
      • _class_createInstanceFromZone中的size = alignedInstanceSize

可以看到,其中当上图最后一步中的alignedInstanceSize,即经过结构体字节对齐后字节仍小于16字节,就会补齐为16字节。

为什么需要补齐16字节呢?

代码文档有一行注释:

CF requires all objects be at least 16 bytes.

或者我们可以理解为OC对象为了提高系统分配及查找地址的效率,而做的一个这样的规定。

这也是上面第一个实例NSObject分析中,为什么实例对象实际占用8字节,会分配16字节的原因。alignedInstanceSize实际实际返回8字节,但calloc中的时候,size为16字节。

4.2 class_getInstanceSize

1
2
3
4
5
6
7
8
9
size_t class_getInstanceSize(Class cls)
{
if (!cls) return 0;
return cls->alignedInstanceSize();
}

uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}

可以看出,class_getInstanceSize最后返回的就是上面所说的,经过字节对齐,但是在alloccalloc之前的大小。

所以我们看到其实际返回的是成员变量实际需要的空间大小。

4.3 calloc

calloc代码在另外一个库中——libmalloc

其中源码比较难以理解,所以直接给出结论。

  • calloc返回的是系统实际分配的内存,最后返回的大小一定是16的倍数

  • 所以BFPerson实例中,成员变量占24字节,但最后经过calloc返回的是32字节(16*2);

  • malloc_size返回的是对象指针所指向的大小,就是calloc实际分配的内存大小;

    extern size_t malloc_size(const void *ptr);

    ​ / Returns size of given ptr /

五、查看内存

5.1 LLDB

LLDB的使用请参考:待补–iOS调试(二)LLDB

5.2 View Memory

Debug -> Debug Workfllow -> View Memory (Shift + Command + M)

image-20181109181240334

参考

链接

示例源码