OC 对象原理探索(二):内存对齐 & malloc

387 阅读7分钟

一、 对象的内存影响因素

先创建一个SSLPerson类:

@interface SSLPerson : NSObject {
    NSString *_hobby;
}

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;

- (void)eat;

@end

打印一下它的内存大小:

nickName属性注释掉:

//@property (nonatomic, copy) NSString *nickName;

查看打印结果:

eat方法注视掉:

//- (void)eat;

查看打印结果:

把成员变量_hobby注视掉:

//    NSString *_hobby;

查看打印结果:

通过上面的打印,我们发现属性成员变量影响了对象内存,方法没有影响,所以我们可以得出最终结论:只有成员变量会影响对象内存。

二、 x/nuf 指令

n表示要显示的内存单元的个数
—————
u表示一个地址单元的长度:
b表示单字节
h表示双字节
w表示四字节
g表示八字节
—————
f表示显示方式,可取如下值:
x按十六进制格式显示变量
d按十进制格式显示变量
u按十进制格式显示无符号整型
o按八进制格式显示变量
t按二进制格式显示变量
a按十六进制格式显示变量
i指令地址格式
c按字符格式显示变量
f按浮点数格式显示变量

三、 成员变量在内存中的存储情况

我们修改一下SSLPerson类:

@interface SSLPerson : 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 a;
@property (nonatomic) char b;

@end

创建SSLPerson类的实例p,为属性赋值,用x/8gx命令打印出这些值在内存中的存储情况:

找出ssl王老五180.5

找出24ab

注:aASCII码97,bASCII码98

通过打印结果我们发现24ab被存储到了同一个8字节内,这里就涉及到了内存对齐的一些原则,下面继续探究。

四、 结构体内存对齐

4.1 类型占用字节数表格

COC32位64位
boolBOOL(64位)11
signed char(__signed char)int8_t、BOOL(32)位11
unsigned charBoolean11
shortint16_t22
unsigned shortunichar22
int int32_tNSInteger(32位)、boolean_t(32位)44
unsigned intboolean_t(64位)、NSUInteger(32位)44
longNSInteger(64位)48
unsigned longNSUInteger(64位)48
long longint64_t88
floatCGFloat(32位)44
doubleCGFloat(64位)88

4.2 内存对齐原则

1、数据成员对齐规则

结构体struct或联合体union的数据成员,第一个数据成员放在offset0的位置,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说数组,结构体等)的整数倍数开始(比如int4个字节,则要从4的整数倍地址开始存储)。

2、结构体作为成员

如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储,(struct a中存有struct bb中有charintdouble等元素,那么b应该从8的整数倍开始存储),因为double为最大子元素,占用8个字节。

3、结构体总大小

结构体总大小,也就是sizeof的结果,必须是其内部最大成员整数倍,不足的要补齐。

4.3 举例验证

struct Struct1 {
    double a;       // 占8字节 存放在[0 7]
    char b;         // 占1字节 下一个索引8是1的整数倍,存放在[8]
    int c;          // 占4字节 下一个索引9不是4的整数倍,所以空出9,10,11,存放在 [12 13 14 15]
    short d;        // 占2字节 下一个索引16是2的整数倍,存放在[16 17]
}struct1;           // 总区间为[0...17],大小为18,取最大元素double8字节的整倍数,所以总大小为24

struct Struct2 {
    double a;       // 占8字节 存放在[0 7]
    int b;          // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
    char c;         // 占1字节 下一个索引12是1的整倍数,存放在[12]
    short d;        // 占2字节 下一个索引13不是2的整倍数,所以空出13 存放在[14 15]
}struct2;           // 总区间为[0...15],大小为16,取最大元素double8字节的整倍数,所以总大小为16

struct Struct3 {
    double a;               // 占8字节 存放在[0 7]
    int b;                  // 占4字节 下一个索引8是4的整数倍,存放在[8 9 10 11]
    char c;                 // 占1字节 下一个索引12是1的整数倍,存放在[12]
    short d;                // 占2字节 下一个索引13不是2的整数倍,所以空出13,存放在[14 15]
    int e;                  // 占4字节 下一个索引16是4的整数倍,存放在[16 17 18 19]
    struct Struct1 str1;    // 占24字节 下一个索引20不是str1中double8字节的整数倍,所以空出20 21 22 23,最后存放在[24.....47]
    struct Struct2 str2;    // 占16字节 下一个索引48是str2中double8字节整数倍,存放在[48.....63]
    short f;                // 占2字节 下一个索引64是2的整数倍,存放在[64,65]
}struct3;                   // 总区间为[0...66],大小为67,取最大元素double 8字节的整倍数,所以总大小为72

NSLog(@"Struct1:%lu -- Struct2:%lu -- Struct3:%lu",
            sizeof(struct1), sizeof(struct2), sizeof(struct3));

查看打印结果:

Struct1:24 -- Struct2:16 -- Struct3:72

五、 sizeofclass_getInstanceSizemalloc_size

修改SSLPerson类:

@interface SSLPerson : 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 "SSLPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        SSLPerson *person = [SSLPerson alloc];
        person.name      = @"lee";
        person.nickName  = @"ssl";
        
        NSLog(@"person %@",person);
        NSLog(@"sizeof %lu",sizeof(person));
        NSLog(@"person %lu",class_getInstanceSize([SSLPerson class]));
        NSLog(@"person %lu",malloc_size((__bridge const void *)(person)));
    }
    return 0;
}

查看打印结果:

解释:

  • sizeof这里计算的是person指针的大小,指针统一为8字节;
  • class_getInstanceSize计算的是isa指针加成员变量占用的内存:name(NSString``8字节) + nickName(NSString 8字节) + age(int 4字节) + height(long 8字节) + isa(来自NSObject 8字节) = 36字节,按照8字节对齐,最终为40字节;
  • malloc_size计算的是实际向系统申请开辟的内存空间:40字节向系统申请时,遵循16字节对齐原则,最终为48字节。

malloc是如何申请内存的呢,我们接下来通过源码来进行分析。

六、 malloc源码分析

6.1 源码下载

工程中点击malloc_size定位源码所在位置:

malloc 源码下载地址,我们以317.40.8版本为例进行分析。

6.2 源码分析

我们在main.m中添加代码,用40字节去申请内存:

#import <Foundation/Foundation.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
		
        // 用 40 字节去申请内存
        void *p = calloc(1, 40);
        NSLog(@"开辟字节数:%lu",malloc_size(p));
    }
    return 0;
}

查看打印结果:

确实还是开辟了48字节的空间,下面通过断点调试查看源码。

断点进入calloc

断点进入:_malloc_zone_calloc

通过返回值找到核心代码ptr = zone->calloc(zone, num_items, size),点击进入calloc(zone, num_items, size)发现找不到方法实现。

可以通过po的方式找到应该调用的方法default_zone_calloc

  • 为什么可以打印获取:因为有赋值,就会有存储值,就可以打印输出。
  • 第二种方式:除了输出的方式,还可以通过汇编找方法的方式找到方法的真实调用。

断点进入default_zone_calloc

还是找不到方法,通过p zone->calloc获取到方法nano_calloc

注:ppo打印的更详细。

断点进入nano_calloc

根据返回值定位核心代码:_nano_malloc_check_clear,断点进入:

根据返回值找到segregated_size_to_fit关键函数,断点进入:

这里是16字节对齐算法,40经过对齐后得到48,这就是最终会开辟48字节的原因。

七、 总结

  • 堆区中,对象的内存以16字节对齐;
  • 成员变量,以8字节对齐。