OC底层原理--内存对齐

既然是底层原理系列,内存肯定是我们绕不过的一个知识点,今天这篇文章主要是通过源码来探索下OC底层是怎么进行内存对齐的

既然要探索内存相关的东西,那么首先我们要先掌握获取内存的三种方式如下:

  • sizeof() : 传入的参数为数据类型,获取对应类型所占用的内存.例如:int long double [NSObject alloc]
  • class_getInstanceSize : 获取实例对象的内存大小,即实际需要的内存,大小随类的属性而变化
  • malloc_size : 计算实际分配的内存的大小

我们都知道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 NSInterger(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

接下来我们自定义两个结构体,看下他们的内存情况,如下:

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

struct LYStruct2 {
    double b;
    int a;
    short d;
    char c;
} struct2;

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

通过sizeof我们可以发现对于元素相同的两个结构体,会由于元素的先后位置不同而导致他们的内存不同.由此我们证明了确实存在内存对齐的现象

既然确实存在内存对齐,那么内存对齐就一定会遵循一定的规则,接下来我们一起来看下内存对齐的规则到底是怎样的

结构体内存对齐规则

  • 数据成员对⻬规则:结构struct(或联合体union)的数据成员,第一个数据成员放在offset0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。 min(当前开始的位置m n) m = 9 n = 4 9 10 11 12,即要从第12位开始存储该int
  • 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char, int, double等元素,那b应该从8(最大元素位double,占8位)的整数倍开始存储.)

  • 收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬.例如结构体中最大元素为double,那么sizeof得出的结果一定是8的整数倍.

有了以上规则,接下来我们一起验证下这些规则的准确性

结构体对齐规则验证

以最开始我们定义的struct1struct2为例,具体如下:

根据第一条以及第三条规则,我们得出如下内存布局图

15999881111613.jpg

解析:

struct1
  • int a占用4个字节,从0开始布局为:0~3
  • double b占用8个字节,min(4, 8),由于4不是8的整数倍,所以需要从8开始布局为:8~15
  • char c占用1个字节,min(16, 1),16刚好是1的整数倍,所以从16开始布局为:16
  • short d占用2个字节,min(17, 2),17不是2的整数倍,所以从18开始布局为:18~19
  • 根据第三条规则,struct1中的最大元素为double b,占8字节,所以结构体总大小需为8的整数倍,自动补齐得到结构体的大小为24
struct2
  • double b占用8个字节,从0开始布局为:0~7
  • int a占用4个字节,min(8, 4),由于8是4的整数倍,所以从8开始布局为:8~11
  • short d占用2个字节,min(12, 2),12是2的整数倍,所以从12开始布局为:12~13
  • char c占用1个字节,min(14, 1),14刚好是1的整数倍,所以从14开始布局为:14
  • 根据第三条规则,struct2中的最大元素为double b,占8字节,所以结构体总大小需为8的整数倍,自动补齐得到结构体的大小为16

至此结合我们的打印结果我们已经证明了规则第一条以及第三条的正确性,接下来我们来看下结构体嵌套结构体的情况,如下:

struct LYStruct1 {
    int a;
    double b;
    char c;
    short d;
    struct LYStruct2 {
        double a;
        int b;
        short d;
        char c;
    } struct2;
} struct1;

NSLog(@"struct1:%lu", sizeof(struct1));
struct1:40
15999888873218.jpg
  • 根据规则二,结构体str2需要从其最大成员的整数倍开始存储,最大成员为double b,占8字节,所以要从24开始存储

以上,关于结构体的内存对齐我们就全部了解了,接下来我们继续探索下OC对象的内存情况

OC对象内存对齐以及内存优化

OC内存对齐

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。在iOS中,Xcode默认为#pragma pack(8),即8字节对齐

但是细心的小伙伴应该也发现了,我们在探索alloc源码时提到过字节对齐是以16字节进行对齐的,那么到底是8字节还是16字节呢??

对于这个问题,我们需要区分两个概念,一个是对象实际需要的内存,另一个是系统开辟的内存.还记得我们最开始提到的获取内存的三种方式么,现在我们就通过这几种方式来验证一下,代码如下:

@interface LYPerson : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@end


LYPerson *person = [[LYPerson alloc] init];
        
person.name      = @"LiuYan";
person.nickName  = @"LY";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([LYPerson class]),malloc_size((__bridge const void *)(person)));

2020-09-13 17:43:13.212922+0800 结构体内存对齐[4787:185539] <LYPerson: 0x600003e7c7b0> - 8 - 40 - 48

通过上面的代码我们发现,实际开辟的内存根实际需要的内存并不相同,为了探索为什么会这样,我们查看class_getInstanceSizemalloc_size源码发现

class_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
malloc_size核心源码
#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

通过源码我们可以发现class_getInstanceSize确实采用的的8字节对齐,malloc_size采用的是16字节对齐.

但是理论上8字节对齐已经够用了的,苹果为什么在分配内存的时候还要采用16字节对齐呢,这一点主要是因为apple系统为了防止一切的容错,因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展

内存优化

通过前面对结构体内存对齐的分析,我们知道内存的大小跟属性的先后顺序是有关系的,因此苹果在底层做了一些关于内存优化的操作来提高性能,即属性重排,大概就是将占用内存大的属性尽可能放在前面.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345