iOS-底层原理5:内存对齐

问题

一个NSObject对象占用了多少内存?

分析

先了解下获取内存的方式
  • sizeof
  • class_getInstanceSize
  • malloc_size
#import <Foundation/Foundation.h>
#import "LBHPerson.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        
        NSObject *objc = [[NSObject alloc] init];
        NSLog(@"内存大小:%lu",sizeof(objc));
        NSLog(@"内存大小:%lu",class_getInstanceSize([objc class]));
        NSLog(@"内存大小:%lu",malloc_size((__bridge const void*)(objc)));
        
    }
    return 0;
}
运行结果

三种方式获取的内存大小不一样,

三种获取内存方式的差别
方式 说明
sizeof 只与类型相关,与具体数值无关。(如:bool 2字节,int 4字节,对象(指针)8字节)
class_getInstanceSize runtime的api,用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小 8字节对齐
malloc_size 获取系统实际分配的内存大小 16字节对齐
源码分析

command+鼠标单击跳转到NSObject的定义

@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

简化一下

@interface NSObject  {
    Class isa ;
}

转成C++代码

Clang -rewrite-objc main.m -o main.cpp

或转成arm64架构的手机上运行的C++代码

//xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  输出的CPP文件
xcrun  -sdk  iphoneos clang  -arch arm64  -rewrite-objc main.m -o main-arm64.cpp

从转换的C++文件中找到对应的NSObject结构

struct NSObject_IMPL {
        //
    Class isa;
};

//class 是一个结构体指针
typedef struct objc_class *Class;

  • obj是一个指针,指针占8个字节,所以sizeof为8字节
  • NSObject是一个结构体,里面只包含一个isa指针,一个指针8个字节,所以class_getInstanceSize为8字节
  • class_getInstanceSize占8个字节,但malloc_size16字节对齐,大小应为16字节的倍数,所以为16字节;

但如果情况更复杂呢?例如结构体中包含多个变量,它们的内存又是大多?这就是本文要讲的重点内存对齐。

内存对齐

iOS-底层原理2:alloc、init、new探析中介绍过内存对齐,这里进行更深入的探索。

我们首先定义两个结构体,分别计算他们的内存大小

//1、定义两个结构体
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));

运行结果


运行结果

两个结构体只是其中变量顺序变化了一下,内存大小就发生了变化,这就是iOS中的内存字节对齐现象

内存对齐规则

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

内存对齐原则:

  • 数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)

可以理解为min(m, n) 的公式, 其中 m表示当前成员的开始位置, n表示当前成员所需要的位数。如果满足条件 m 整除 n (即 m % n == 0), n 从 m 位置开始存储, 反之继续检查 m+1 能否整除 n, 直到可以整除, 从而就确定了当前成员的开始位置。

  • 数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
  • 结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐
分析

各数据类型在C和OC中所占内存大小

根据内存对齐原则,用如图所示表示两个结构体的内存存储

结构体内存存储情况
结构体嵌套结构体

上面的两个结构体只是简单的定义数据成员,下面来一个比较复杂的,结构体中嵌套结构体的内存大小计算情况

定义一个结构体MyStruct3,在MyStruct3中嵌套MyStruct2,如下所示

//结构体嵌套结构体
struct Mystruct3{
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
    char a;     //1字节
    struct Mystruct2 str; 
}Mystruct3;
  • 分析Mystruct3的内存

变量b:占8个字节,从0开始,即0-7字节存储变量b
变量c:占4个字节,从8开始, 8%4 == 0,即8-11字节存储变量c;
变量d:占2个字节,从12开始,12%2==0,即12-13 存储变量d
变量a:占1个字节,从14开始,14%1==0,即 14 存储变量a;
变量str:str是一个结构体,根据内存对齐原则二,结构体成员要从其内部最大成员大小的整数倍开始存储,而MyStruct2中最大的成员大小为8,所以str要从8的整数倍开始,当前是从15开始,所以不符合要求,需要往后移动到16,16是8的整数倍,符合内存对齐原则,所以 16-31字节存储 str

因此MyStruct3的需要的内存大小为 32字节,根据内存对其原则三
Mystruct3实际的内存大小必须是最大成员b的整数倍,即必须是8的整数倍,所以sizeof(Mystruct3)的结果是 32;

根据分析MyStruct3结构体变量内存存储如图所示:

MyStruct3内存存储情况

输出

NSLog(@"%lu-%lu",sizeof(Mystruct3),sizeof(Mystruct3.str));

输出结果

输出结果

输出结果与分析的一致

二次验证

为了保险起见,我们再定义一个结构体,来验证我们结构体嵌套的内存大小

struct Mystruct4{
    double a;
    short b;
    char c;
    struct Mystruct5{   
        short e;
        char f;  
    }Mystruct5;
}Mystruct4;
  • 分析
    变量a:占8字节,从0开始,即0-7字节存储变量a
    变量b:占2字节,从8开始,min(8,2),可以整除,即 8-9存储b;
    变量c:占1字节,从10开始,min(10,1),可以整除,即10存储c;
    结构体Mystruct5:所以从11开始,根据内存对齐原则二,即存储开始位置必须是最大的整数倍(最大成员为2),min(11,2)不能整除,继续往后移动,到12, min(12,2)满足,所以Mystruct5开始位置为12
    • 变量e:占2字节,从12开始min(12,2),可以整除,即12-13存储e;
    • 变量f:占1字节,从14开始min(14,1),可以整除,即14存储f;

因此Mystruct4中需要的内存大小是15字节,根据内存对其原则三Mystruct4实际的内存大小必须是最大成员a的整数倍,即必须是8的整数倍,所以sizeof(Mystruct4)的结果是 16
Mystruct5内存大小是3字节,根据内存对其原则三,Mystruct5实际的内存大小必须是最大成员e的整数倍,即必须是2的整数倍,所以sizeof(Mystruct5)的结果是 4

输出

NSLog(@"%lu--%lu",sizeof(Mystruct4),sizeof(Mystruct4.Mystruct5));

输出结果

内存优化

MyStruct1补齐了9个字节,而MyStruct2只补齐一个字节即可满足字节对齐规则,这里得出一个结论结构体内存大小与结构体成员内存大小的顺序有关

如果结构体中数据成员是根据内存从大到小的顺序定义的,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足堆存对齐规则,这种方式就是苹果中采用的,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的

分析

先介绍下LLDB常见的指令,方便接下来的断点调试

LLDB常见命令

step1:定义一个自定义LBHPerson类,并定义几个属性

@interface LBHPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

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

@implementation LBHPerson

@end

step2:在main函数中创建类的实例并给属性赋值

int main(int argc, char * argv[]) {
    @autoreleasepool {
        LBHPerson *person = [LBHPerson alloc];
        person.name      = @"liu";
        person.nickName  = @"666";
        person.age       = 18;
        person.height    = 190;
        person.c1        = 'a';
        person.c2        = 'b';

        NSLog(@"%@",person);
    }
    return 0;
}

step3:断点调试person

  • x personxmemory read的简写,读取内存信息 (iOS是小端模式,内存读取要反着读)
    95 47 00 00 01 80 1d 00应读取为0x001d800100004795

  • x/8gx person:16进制打印8行内存信息

这里打印了8行内存信息,但实际上person对象变量并没有使用这么多内存,可以通过class_getInstanceSize方法获取实际上对象变量只是用了40字节的内存,就是上图中前五段内存,但是有几个属性值并没有找到。

分析:没有找到age、c1及c2对应的值,是不是苹果做了什么处理避免内存过度消耗,我们用没有正常输出信息的内存尝试解析下

结论:name、nickname、height都是各自占用8字节。可以直接打印出来;而age是Int占用4字节,c1和c2是char,各自占用1字节。我们推测系统可能属性重排,将他们存放在了一个块区

特殊的doublefloat

height属性类型修改为double

//@property (nonatomic, assign) long height; 
@property (nonatomic, assign) double height; 

重新运行

直接po打印0x4067c00000000000,并不能正确输出变量 height的值,这是因为编译器po打印默认当做int类型处理

  • p/x (double)190:190转成double类型然后以16进行打印,发现地址完全一样。
    height改成float类型也可以用p/x (float)190验证

封装2个验证函数:

// float转换为16进制
void lbh_float2HEX(float f){
    union uuf { float f; char s[4];} uf;
    uf.f = f;
    printf("0x");
    for (int i = 3; i>=0; i--) {
        printf("%02x", 0xff & uf.s[i]);
    }
    printf("\n");
}

// double转换为16进制
void lbh_double2HEX(double d){
    union uud { double d; char s[8];} ud;
    ud.d = d;
    printf("0x");
    for (int i = 7; i>=0; i--) {
        printf("%02x", 0xff & ud.s[i]);
    }
    printf("\n");
}

结果

字节对齐到底采用多少字节对齐?

objc4源码中搜索class_getInstanceSize,可以在runtime.h找到:

/** 
 * 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字节对齐已经足够满足对象的需求了

总结
  • class_getInstanceSize:是采用8字节对齐,参照的对象的属性内存大小
  • malloc_size:采用16字节对齐,参照的整个对象的内存大小,对象实际分配的内存大小必须是16的整数倍,采用8字节对齐,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

16字节内存对齐算法

目前已知的16字节内存对齐算法有两种

  • alloc源码分析中的align16
  • malloc源码分析中的segregated_size_to_fit
align16: 16字节对齐算法

iOS-底层原理2:alloc、init、new探析
一文中已经讲解过

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
segregated_size_to_fit: 16字节对齐算法
#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
    }
//  (size + 15) >> 4
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
//  << 4
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

算法原理:k + 15 >> 4 << 4,其中 右移4 + 左移4相当于将后4位抹零,跟 k/16 * 16一样 ,是16字节对齐算法,小于16就成0了

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

推荐阅读更多精彩内容