iOS-OC底层二 :对象内存对齐

一、内存对齐

获取内存大小的三种方式分别是:

  • sizeof:sizeof计算内存大小时,传入的主要对象是数据类型,这个在编译器的编译阶段(即编译时)就会确定大小而不是在运行时确定。最终得到的结果是该数据类型占用空间的大小。
  • class_getInstanceSize:用于获取类的实例对象所占用的内存大小,并返回具体的字节数,其本质就是获取实例对象中成员变量的内存大小
  • malloc_size:这个函数是获取系统实际分配的内存大小
extern size_t malloc_size(const void *ptr);/* Returns size of given ptr */
    
 /** 
 * 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);

通过下面例子验证一下:

#import <Foundation/Foundation.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(@"objc对象类型占用的内存大小:%lu",sizeof(objc));
        NSLog(@"objc对象实际占用的内存大小:%lu",class_getInstanceSize([objc class]));
        NSLog(@"objc对象实际分配的内存大小:%lu",malloc_size((__bridge const void*)(objc)));
        
    }
    return 0;
}

运行后的结果如下:

获取内存方式验证.png

分析结果原因:

  • sizeof打印出来8:因为objc是NSObject定义的实例对象,实例对象的本质是一个结构体指针,指针类型占用的空间是8字节。
  • class_getInstanceSize打印出来的也是8:因为objc对象没有属性,只有指针占用8字节,所以占用的真实内存大小是8字节。
  • malloc_size:计算机实际分配的内存大小是16字节。因为16字节对齐,在上一篇研究alloc源码时已经了解过。

结构体内存对齐

那么苹果是怎么计算一个对象占用多少字节的?也就是内存对齐究竟是怎么做的。

内存对齐的原则:

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

2、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)

3、收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补⻬

另外一种更加形象的描述:

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

例子一:结构体占用内存对比

#import <Foundation/Foundation.h>

//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;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        //计算 结构体占用的内存大小
        NSLog(@"%lu-%lu",sizeof(Mystruct1),sizeof(Mystruct2));
        
    }
    return 0;
}

运行的结果如下:


结构体顺序不一致导致占用内存不一样.png

分析原因:根据内存对齐规则我们模拟两个结构体的内存对齐过程:

结构体计算规则流程示例.png

Mystruct1的内存对齐过程如上图所示,Mystruct2的过程如下:

根据内存对齐规则计算MyStruct2的内存大小,详解过程如下:

  • 变量b:占8个字节,从0开始,此时min(0,8),即 0-7 存储 b
  • 变量c:占4个字节,从8开始,此时min(8,4),8可以整除4,即 8-11 存储 c
  • 变量d:占2个字节,从12开始,此时min(12, 2),12可以整除2,即12-13 存储 d
  • 变量a:占1个字节,从14开始,此时min(14,1),即 14 存储 a

因此MyStruct2的需要的内存大小为 15字节,而MyStruct2中最大变量的字节数为8,所以 MyStruct2 实际的内存大小必须是 8 的整数倍,15向上取整到16,主要是因为16是8的整数倍,所以 sizeof(MyStruct2) 的结果是 16

例子二:结构体嵌套结构体

代码如下:

//1、结构体嵌套结构体
struct Mystruct3{
    double b;   //8字节
    int c;      //4字节
    short d;    //2字节
    char a;     //1字节
    struct Mystruct2 str; 
}Mystruct3;

//2、打印 Mystruct3 的内存大小
NSLog(@"Mystruct3内存大小:%lu", sizeof(Mystruct3));
NSLog(@"Mystruct3中结构体成员内存大小:%lu", sizeof(Mystruct3.str));

运行结果:


结构体嵌套结构的运行结果.png

根据内存对齐规则,来一步一步分析Mystruct3内存大小的计算过程

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

因此MyStruct3的需要的内存大小为 32字节,而MyStruct3中最大变量为str, 其最大成员内存字节数为8,根据内存对齐原则,所以 MyStruct3 实际的内存大小必须是 8 的整数倍,32正好是8的整数倍,所以 sizeof(MyStruct3) 的结果是 32

例子三:结构体嵌套结构体二次验证

struct Mystruct4{
    int a;              //4字节 min(0,4)--- (0,1,2,3)
    struct Mystruct5{   //从4开始,存储开始位置必须是最大的整数倍(最大成员为8),min(4,8)不符合 4,5,6,7,8 -- min(8,8)满足,从8开始存储
        double b;       //8字节 min(8,8)  --- (8,9,10,11,12,13,14,15)
        short c;         //2字节,从16开始,min(16,2) -- (16,17)
    }Mystruct5;
    /*结构体Mystruct5结束,按照【原则三】`结构体的内存大小`必须是结构体中`最大成员内存大小`的整数倍,不足的需要补齐。
     当前结构体长度为10,补充6字节。(18,19,20,21,22,23)留空。
    */
    short d; //2字节,offset从24开始,能整除。 (24,25)存放
}Mystruct4;
/*结构体结束,按照【原则三】`结构体的内存大小`必须是结构体中`最大成员内存大小`的整数倍,不足的需要补齐。
 当前结构体长度为26,补充6字节。(26,27,28,29,30,31)留空。
*/

NSLog(@"Mystruct4内存大小:%lu", sizeof(Mystruct4));
NSLog(@"Mystruct5内存大小:%lu", sizeof(Mystruct4.Mystruct5));
结构体嵌套结构体二次验证.png

二、内存优化(属性重排)

我们知道对象在底层就是一个结构体对象。从上面结构体内存对齐的内容,得出结构体占用内存大小跟结构体中成员变量的顺序有关。那么对象的内存大小是否跟成员变量的顺序有关呢?

思考:如果是的话,那么我们平时写代码的时候,岂不是要时时刻刻注意,成员变量的顺序,所以答案肯定是否定的。苹果系统底层会帮我们自动进行内存优化

对象虽然会以结构体的形式进行存储,但是在保存时,苹果会计算出最优的存储顺序,达到减少内存消耗的目的。

例一:验证内存优化

新建两个类,Man和Women,分别添加两个int和一个long。将顺序打乱,如果纯粹按照结构体内存对齐,它将分别是16字节和24字节。代码如下:

#import <objc/runtime.h>
#import <malloc/malloc.h>

@interface Man : NSObject
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int wight;
@property (nonatomic, assign) long height;
@end

@implementation Man
@end

@interface Woman : NSObject
@property (nonatomic, assign) int wight;
@property (nonatomic, assign) long height;
@property (nonatomic, assign) int age;
@end

@implementation Woman
@end


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        Man *man = [[Man alloc] init];
        man.age = 18;
        man.height = 185;
        man.wight = 75;
        
        Woman *woman = [[Woman alloc] init];
        woman.age = 18;
        woman.height = 185;
        woman.wight = 75;
        
        NSLog(@"man对象类型占用的内存大小:%lu,woman对象类型占用的内存大小:%lu",sizeof(man),sizeof(woman));
        NSLog(@"man对象实际占用的内存大小:%lu,woman对象实际占用的内存大小:%lu",class_getInstanceSize([man class]),class_getInstanceSize([woman class]));
        NSLog(@"man对象实际分配的内存大小:%lu,woman对象实际分配的内存大小:%lu",malloc_size((__bridge const void*)(man)),malloc_size((__bridge const void*)(woman)));
    }
    return 0;
}

类的成员变量顺序不会影响类对象的内存大小.png

结果:类对象是指针,占8字节。一个int占4字节,两个int占8字节,一个long占8字节,加上指针占8字节,实际一共24字节。对象类型需要16字节对齐,必须是16的倍数,所以对象占用32字节。

分析:按照结构体内存对齐,Man和Woman类所占的内存空间肯定不一样,可结果确是完全一模一样,说明了苹果在类转换成结构体存储时,做了内存优化。

通过打印内存二次验证

通过打印内存验证.png

分析结果:第一个字节是ISA,第二个字节顺序不同了,第三个8字节一模一样,第四字节都是补充0。第二8字节0x12和0x4b顺序不同,正是苹果对内存做的优化。可以猜一下,苹果先是忽略了long,按int的顺序补齐。真正要研究它是怎么优化的过程,需要去看LLVM的处理逻辑,在加载类时的逻辑。

关于字节对齐的底层代码

我们可以通过objc4中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
  • 对于一个对象来说,其真正的对齐方式8字节对齐,8字节对齐已经足够满足对象的需求了
  • apple系统为了防止一切的容错,采用的是16字节对齐的内存,主要是因为采用8字节对齐时,两个对象的内存会紧挨着,显得比较紧凑,而16字节比较宽松,利于苹果以后的扩展。

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

  • alloc源码分析中的align16
  • malloc源码分析中的segregated_size_to_fit

align16: 16字节对齐算法

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
    }
    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;
}

三、改变编译器字节对齐方式

在一些与用硬件设备交互的场景中,比如C语言写的加密算法,可能需要直接iOS和安卓端按照设备端C语言编译器的处理方式,而不是通常情况下Xcode的编译处理方式。因为字节对齐的问题,加密位数的不一致,会导致校验失败。在安卓端可以通过JNI实现java和C两个编译环境,不需要处理这种问题。但是在iOS端,虽然是可以直接运行C语言,但却和极其在意内存使用的嵌入式设备端C语言编译器有区别。请看如下代码,还是上面的Mystruct1和Mystruct2:

改变字节对齐方式.png

分析结果:加了#pragma pack(2)之后,根本不理会所谓的字节对齐的内存存储方式。加了#pragma pack之后,就可以实现完全跟各种编译器匹配,直接使用C语言完成加密算法的校验。简直不要太爽。

#pragma pack的作用

程序编译器对结构的存储的特殊处理确实提高CPU存储变量的速度,但是有时候也带来了一些麻烦,我们也屏蔽掉变量默认的对齐方式,自己可以设定变量的对齐方式。

编译器中提供了#pragma pack(n)来设定变量以n字节对齐方式。n字节对齐就是说变量存放的起始地址的偏移量有两种情况:

第一、如果n大于等于该变量所占用的字节数,那么偏移量必须满足默认的对齐方式,

第二、如果n小于该变量的类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。

结构的总大小也有个约束条件,分下面两种情况:如果n大于所有成员变量类型所占用的字节数,那么结构的总大小必须为占用空间最大的变量占用的空间数的倍数;否则必须为n的倍数。

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

推荐阅读更多精彩内容