iOS底层探索

iOS 内存对齐

2020-09-08  本文已影响0人  Sheisone

在上一边文章中,我们在简单介绍内存对齐,今天我们更加深入一点:

一、获取内存大小的三种方式

先看下面这段代码:

@interface LPPerson : NSObject
@property (nonatomic) NSString *name;
@property (nonatomic) NSString *nickName;
@end
#import "ViewController.h"
#import "LPPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc]init];
    NSLog(@"person对象%lu--%lu--%lu",sizeof(person),
                                    class_getInstanceSize([person class]),
                                    malloc_size((__bridge const void*)(person)));
    
}
@end

看一下打印结果:

person对象8--24--32

这三种方式是干嘛的?为什么获取的结果是这样的呢?

sizeof是一个运算符,并不是一个函数。
sizeof 传进来的是类型,用来计算这个类型占多大内存,这个在编译器编译阶段就会确定大小并直接转化成 8 、16 、24 这样的常数,而不是在运行时计算。
这里person是一个对象,而对象类型中一般都有一个isa属性,一个isa指针所占用的字节数为8字节,所以对象类型占用的字节数为8字节

计算对象实际占用的内存大小,这个需要依据类的属性而变化,如果自定义类没有自定义属性,仅仅只是继承自NSObject,则类的实例对象实际占用的内存大小是8,可以简单理解为8字节对齐,参照的对象的属性内存大小。
这里person中有namenickName两个属性,对象本身8字节,每个nsstring类型占8字节,所以结果是24字节。

返回的是某个类的对象实际占用的内存空间大小。这个是由系统完成的,可以从上面的打印结果看出,实际分配的和实际占用的内存大小并不相等。malloc_size采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍。

到这里,有人觉得奇怪吗?为什么person需要24字节就够了,但是实际却分配了32字节呢?
这就是我们今天的主题--内存对齐

二、内存对齐

1、内存对齐的规则:

  • 数据成员的对齐规则可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即m % n == 0), n 从 m 位置开始存储, 反之继续检查m+1能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。
  • 数据成员为结构体:当结构体嵌套了结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8
  • 最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐。

直接阅读规则可能很难理解,下面我们通过一个案例来理解下

2、案例分析

接下来,我们首先定义两个结构体,分别计算他们的内存大小,来了解内存对齐的原理。

struct LPStruct1 {
    double a;   
    char b;    
    int c;      
    short d;    
}struct1;


struct LPStruct2 {
    double a;   
    int b;    
    char c;   
    short d;   
}struct2;
OC常用基本数据类型所占字节数
C OC 32位 64位
bool BOOL(64位) 1 1
signed char (_ _signed char)int8_t、BOOL(32位) 1 1
unsigned char Boolean 1 1
short int16_t 2 2
unsigned short unichar 2 2
int int32_t、NSInteger(32位)、boolean_t(32位) 4 4
unsigned int boolean_t(64位)、NSUInteger(32位) 4 4
long NSInteger(64位) 4 8
unsigned long NSUInteger(64位) 4 8
long long int64_t 8 8
float CGFloat(32位) 4 4
double CGFloat(64位) 8 8

好的,现在我们来分析下LPStruct1LPStruct2的所占内存情况。假设当前内存从0开始往后分配,我们先看下LPStruct1的内存情况:

image.png

再开看下LPStruct2的内存情况:

image.png

我们获取LPStruct1LPStruct2的内存大小,并打印下:

NSLog(@"LPStruct1:%lu----LPStruct2:%lu",sizeof(struct1),sizeof(struct2));
LPStruct1:24----LPStruct2:16

可以看到结果和我们分析的一致。
接下来我们增加点难度,如果结构体嵌套呢?
新增结构体LPStruct3

struct LPStruct3 {
    double a;  
    int b;      
    char c;    
    short d;   
    struct LPStruct2 strt;
}struct3;

同样的,我们按照内存对齐的规则来分析下:


image.png

其实,是不是很简单呢?相信你也看懂了吧!

三、iOS的内存优化--属性重排:

接下里我们修改下LPPerson类,新增几个属性:

@interface LPPerson : NSObject
@property (nonatomic) NSString *name;
@property (nonatomic) NSString *nickName;

@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end

Viewcontroller中给person对象赋值并打印新的person对象:

    [super viewDidLoad];
    
    LPPerson *person = [[LPPerson alloc]init];
    person.name      = @"LPeng";
    person.nickName  = @"LP";
    person.age       = 18;
    person.c1        = 'a';
    person.c2        = 'b';

    NSLog(@"%@",person);
    NSLog(@"person对象%lu--%lu--%lu",sizeof(person),
                                    class_getInstanceSize([person class]),
                                    malloc_size((__bridge const void*)(person)));
    
}

并在NSLog(@"%@",person);下面一行打上断点:

image.png

运行工程,来到断点的位置,然后执行如下操作:


image.png

x/数量格式字节数 内存地址:输出内存16进制 格式化排列(读取内存)
x/8gx :表示将后面的内存地址输出8条

我们将输出的8个地址分别po一下,发现有的有值,但是有的确实乱码。比如0x0000001200006261这个地址,我们将其分开再po一下:

image.png
age的值是18,但是98和97是什么?实际上98和97分别是b和a的ASCII码。
结果是不是和我们预计的不一样?实际上这是苹果在内部做了内存优化即属性重排。苹果通过这种方式来避免一些不必要内存浪费并且优化内存的读取。

我们再看下第二个打印的结果:

person对象8--40--48

40是因为来自于:personisa指针8字节,agec1c2占8字节,name8字节,nickName8字节,还有赋值的height属性为long所以也需要8字节。

48是因为系统实际分配的内存必须为16的整数倍即16进制对齐,所以为48字节。

假设不进行属性重拍,那么person的内存为:
personisa指针8字节,name8字节,nickName8字节,现在为24字节,age为4字节并且24可以被4整除,所以age占24-27。接下来height属性为long所以也需要8字节,但是28不能被8整除,所以需要往后走到32可以被8整除,即height占32-398个字节,接下来分别是c1c2分别占1一个字节,那么未属性重排的person要实际需要42个字节。

虽然两者最终系统都是分配48字节,但是重排之后,从读6个地址到读5个地址,读取效率明显提高了。从而达到了优化的结果。

四、为什么是16字节对齐?

我们可以通过objc4class_getInstanceSize的源码来进行分析:

/** 
 * Returns the size of instances of a class.
 * 
 * @param cls A class object.
 * 
 * @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
 */
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

⬇️

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

⬇️

// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

⬇️

static inline uint32_t word_align(uint32_t x) {
    //x+7 & (~7) --> 8字节对齐
    return (x + WORD_MASK) & ~WORD_MASK;
}


//其中 WORD_MASK 为
#   define WORD_MASK 7UL

实际上对于一个对象来说,其真正的对齐方式是8字节对齐,8字节对齐已经足够满足对象的需求了。
但是苹果系统为了防止一切的容错,采用的是16字节对齐的内存,主要是因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

觉得不错记得点赞哦!听说看完点赞的人逢考必过,逢奖必中。ღ( ´・ᴗ・` )比心

上一篇下一篇

猜你喜欢

热点阅读