iOS 底层原理 iOS 进阶之路

OC底层原理六: 内存对齐

2020-09-13  本文已影响0人  markhetao

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.获取内存大小
#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 内存计算

内存优化(属性重排)

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

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

属性重排案例

@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
#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;
}
image.png

特殊的doublefloat

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

@property(nonatomic, assign) double     height;
image.png
我们发现直接po打印0x4067c00000000000,打印不出来height的数值190。 这是因为编译器po打印默认当做int类型处理。

如果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字节对齐

总结

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

_class_createInstanceFromZone核心方法.png

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

上一篇 下一篇

猜你喜欢

热点阅读