OC底层原理六: 内存对齐

OC底层原理 学习大纲

前期准备

1.lldb打印规则

po: 对象信息

(lldb) po person
<HTPerson: 0x101875c70>

p: 对象信息

(lldb) p person
(HTPerson *) $1 = 0x0000000101875c70

xmemory read的简写,读取内存信息 (iOS是小端模式,内存读取要反着读)
例如: e5 22 00 00 01 80 1d 00 应读取为0x001d8001000022e5

(lldb) memory read person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63  -[NSVisualTabPic
(lldb) x person
0x1024aef20: c9 21 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .!..............
0x1024aef30: 2d 5b 4e 53 56 69 73 75 61 6c 54 61 62 50 69 63  -[NSVisualTabPic

x/4gx: 打印4条16进制的16字符长度的内存信息

(lldb) x/4gx person
0x101875c70: 0x001d8001000022e5 0x0000000000000012
0x101875c80: 0x0000000100001010 0x0000000100001030

x/4gw: 打印4条16进制的8字符长度的内存信息

(lldb) x/4gw person
0x1024aef20: 0x000021c9 0x001d8001 0x00000000 0x00000000

p/t: 二进制打印

(lldb) p/t person
(HTPerson *) $2 = 0b0000000000000000000000000000000100000010010010101110111100100000
2.获取内存大小
  • sizeof:

    操作符。传入数据类型,输出内存大小。编译时固定
    只与类型相关,与具体数值无关。(如:bool 2字节,int 4字节,对象(指针)8字节)

  • class_getInstanceSize:

    runtime的api,传入对象,输出对象所占的内存大小,本质是对象中成员变量的大小

  • malloc_size:

    获取系统实际分配的内存大小,符合前面章节align16对齐标准

#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(@"[sizeof]                 内存大小: %lu字节", sizeof(objc));
        NSLog(@"[class_getInstanceSize]  内存大小: %lu字节", class_getInstanceSize([objc class]));
        NSLog(@"[malloc_size]            内存大小: %lu字节", malloc_size((__bridge const void *)(objc)));
    }
    return 0;
}
image.png
  • 今天我们就来了解,对象内部内存对齐

内存对齐

我们知道对象对外,苹果系统会采用align16字节对齐开辟内存大小,提高系统存取性能。

对象内部呢?

  • 对象的本质是结构体,这个在后续篇章中我们会详细了解。所以研究对象内部的内存,就是研究结构体内存布局

  • 内存对齐目的:最大程度提高资源利用率

我们从一个小案例开始入手

struct MyStruct1 {
    char a;       // 1字节
    double b;     // 8字节
    int c;        // 4字节
    short d;      // 2字节
    NSString * e; // 8字节(指针)
} MyStruct1;

struct MyStruct2 {
    NSString * a; // 8字节(指针)
    double b;     // 8字节
    int c;        // 4字节
    short d;      // 2字节
    char e;       // 1字节
} MyStruct2;

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"%lu - %lu", sizeof(MyStruct1), sizeof(MyStruct2));
    }
    return 0;
}

打印结果:

image.png
MyStruct1 和 MyStruct2 的构成元素都一样,为何打印出的内存大小不一致?
  • 结构体内部的元素排序影响内存大小。这就是内存字节对齐的作用。
结构体内存对齐规则

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

注意: 这里的8字节对齐是结构体内部对齐规则,对象在系统中对外实际分配的空间是遵循16字节对齐原则。

【三条结构体对齐规则】:
(先把规则写出来,我们下面用实例来理解)

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

  2. 数据成员为结构体:当结构体嵌套结构体时,作为数据成员的结构体的自身长度作为外部结构体的最大成员的内存大小,比如结构体a嵌套结构体b,b中有char、int、double等,则b的自身长度为8

  3. 最后结构体的内存大小必须是结构体中最大成员内存大小的整数倍,不足的需要补齐。

iOS 基础数据类型 字节数表

基础数据类型字节数

MyStruct1 内存计算
MyStruct2 内存计算

结构体中的结构体

struct MyStruct3 {
    NSString * a; // 8字节(指针)
    double b;     // 8字节
    int c;        // 4字节
    short d;      // 2字节
    char e;       // 1字节
    struct MyStruct2 str;
} MyStruct3;

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        NSLog(@"MyStruct3内存大小: %lu", sizeof(MyStruct3));
        NSLog(@"MyStruct3中的结构体(MyStruct2)内存大小 %lu", sizeof(MyStruct2));
    }
    return 0;
}
image.png
MyStruct3 内存计算

内存优化(属性重排)

  • 我们观察到MyStruct1MyStruct2的成员属性一样,但是在内存管理上,MyStruct2MyStruct1利用率更高(白色空白区域更少)。

  • MyStruct2intshortchar 4 + 2 + 1组合,空间利用得更合理。

  • 苹果会进行属性重排,对属性进行合理排序,尽可能保持保持属性之间的内存连续,减少padding(白色部分,属性之间置空的内存)。

如果你还记得align16对齐方式,你应该能理解属性重排的好处了

  • align16, 是空间换取时间,保障系统在处理对象时能快速存取
  • 属性重排,保障一个对象尽可能少的占用内存资源。

属性重排案例

  • 创建HTPerson
@interface HTPerson : NSObject

@property(nonatomic, copy)   NSString * name;
@property(nonatomic, copy)   NSString * nickname;
@property(nonatomic, assign) int        age;
@property(nonatomic, assign) long       height;
@property(nonatomic, assign) char       c1;
@property(nonatomic, assign) char       c2;

@end
  • main.m 加入测试代码
#import "HTPerson.h"

int main(int argc, const char * argv[]) {
    
    @autoreleasepool {
        
        HTPerson * person = [[HTPerson alloc]init];
        person.age      = 18;
        person.height   = 190;
        person.name     = @"mark";
        person.nickname = @"哈哈";
        person.c1       = 'A';
        person.c2       = 'B';
        
        NSLog(@"%@", person);
    }
    return 0;
}
  • x/8gx person: 16进制打印8行内存信息

    image.png

  • 我们分析属性,namenicknameheight都是各自占用8字节。可以直接打印出来。

  • age是Int占用4字节,c1c2是char,各自占用1字节。我们推测系统可能属性重排,将他们存放在了一个块区。

image.png

特殊的doublefloat

我们尝试把height属性类型修改为double

@property(nonatomic, assign) double     height;

image.png

我们发现直接po打印0x4067c00000000000,打印不出来height的数值190。 这是因为编译器po打印默认当做int类型处理。

  • p/x (double)190:我们以16进制打印double类型值打印,发现完全相同。

如果height熟悉换成float,也是一样的使用p/x (float)190验证。

我们可以封装2个验证函数:

// float转换为16进制
void ht_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");
}

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

为什么对象内部字节对齐是8字节

我们在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);

objc-class.mm可以找到:

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

进入alignedInstanceSize:

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

进入word_align

#ifdef __LP64__ // 64位操作系统
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL  // 7字节遮罩
#   define WORD_BITS 64 
#else 
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

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

可以看到,系统内部设定64位操作系统,统一使用8字节对齐

总结

  • 外部处理,系统面对的对象太多,我们统一按照align16内存块来存取,效率很快。(所以malloc_size读取的都是16的倍数)

  • 但为了避免浪费太多内存空间。系统会在每个对象内部进行属性重排,并使用8字节对齐,使单个对象占用的资源尽可能小。(所以class_getInstanceSize读取的都是8的倍数)

  • 外部使用16字节对齐,给类留足够间距,避免越界访问,对象内部使用8字节对齐完全足够。

至此, OC底层原理三:探索alloc (你好,alloc大佬 )中提到的三大核心方法,我们已掌握了initstanceSize计算内存大小。

_class_createInstanceFromZone核心方法.png

下一节: ` OC底层原理七: malloc源码分析

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