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

2020-10-13  本文已影响0人  没有昵称的昵称没有

计算内存方法

首先我们要知道计算内存大小的三种方式:

接下来我们定义一个LGPerson类,分析这三种方法。代码如下:

LGPerson * p = [LGPerson alloc];
LGPerson * q;
NSLog(@"对象类型占用内存大小=%lu",sizeof(p));
NSLog(@"对象类型占用内存大小=%lu",sizeof(q));
NSLog(@"对象实际内存大小====%lu",class_getInstanceSize([p class]));
NSLog(@"对象实际内存大小====%lu",class_getInstanceSize([q class]));
NSLog(@"对象实际分配内存大小=%lu",malloc_size((__bridge const void *)(p)));
NSLog(@"对象实际分配内存大小=%lu",malloc_size((__bridge const void *)(q)));

打印结果:

2020-09-29 14:02:17.810194+0800 KCObjc[20870:761876] 对象类型占用内存大小=8
2020-09-29 14:02:17.810897+0800 KCObjc[20870:761876] 对象类型占用内存大小=8
2020-09-29 14:02:17.811068+0800 KCObjc[20870:761876] 对象实际内存大小====8
2020-09-29 14:02:17.811165+0800 KCObjc[20870:761876] 对象实际内存大小====0
2020-09-29 14:02:17.811265+0800 KCObjc[20870:761876] 对象实际分配内存大小=16
2020-09-29 14:02:17.811352+0800 KCObjc[20870:761876] 对象实际分配内存大小=0

由打印结果可以分析出

#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

LGPerson类中没有其他的属性和变量,但是继承了NSObject,NSObject中有一个isa指针,所以内存大小是8字节。

内存对齐原则

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

下表是各种数据类型在iOS中的占用内存大小,根据对应类型来计算结构体中内存大小


数据类型对应的字节数表格

结构体对齐

如下代码,我们用实例进行探究结构体对齐:

struct LGStruct1{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
} LGStruct1;

struct LGStruct2{
    long    a; // 8
    char    d; // 1
    int     b; // 4
    short   c; // 2
} LGStruct2;

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

打印结果

2020-09-29 15:52:17.811352+0800 KCObjc[20870:761876] -----16------24

由上述代码可看出两个结构体定义的变量和变量类型都一致,唯一的区别只是在于定义变量的顺序不一致,那么为什么会占用的内存大小不相等呢?其实这就是iOS中的内存对齐原则。下面我们就根据内存对齐原则来进行简单的分析和计算LGStruct1内存大小的详细过程:

因此LGStruct1的内存大小是15字节,而LGStruct1中最大的变量是a8个字节,所以LGStruct1需要实际内存必须是8的倍数(内存对齐原则)15字节不是8的倍数,15向上取整到16 ,所以系统自动填充成16字节,最终sizeof(LGStruct1)的大小是16.

image.png
LGStruct2内存大小的详细过程

结构体嵌套结构体

上面的2个示例只是简单的定义数据成员,如果我们在结构体中嵌套结构体结果又会是怎样的?我们继续探究,看下面代码:

struct LGStruct3{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
    struct LGStruct1 Str;
}LGStruct3;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"LGStruct1----%lu",sizeof(LGStruct1));
        NSLog(@"LGStruct2----%lu",sizeof(LGStruct2));
        NSLog(@"LGStruct3----%lu",sizeof(LGStruct3));
    }
    return 0;
}
//结果
2020-09-30 11:02:46.509957+0800 001-内存对齐原则[22800:939799] LGStruct1----16
2020-09-30 11:02:46.511196+0800 001-内存对齐原则[22800:939799] LGStruct2----24
2020-09-30 11:02:46.512537+0800 001-内存对齐原则[22800:939799] LGStruct3----32

LGStruct3内存大小存储情况的详细过程

因此LGStruct3的内存大小是32字节,而LGStruct1中最大变量为Str,其最大成员内存字节数为8,所以LGStruct3内存必须是8的倍数,328的倍数,最终sizeof(LGStruct3)的大小是32
其内存存储情况如下图所示

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

内存优化(属性重排)

从上述的示例中,我们可以得出一个结论即结构体的内存大小与结构体成员内存大小的顺序有关

第二种方式就是苹果采用的将类中的属性进行重排,来达到优化内存的目的。以下面这个示例来进行说明苹果中属性重排,即内存优化:

//LGPerson.h
@interface LGPerson : 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

//LGPerson.m
@implementation LGPerson

@end
int main(int argc, char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        person.name      = @"Cooci";
        person.nickName  = @"KC";
        person.age       = 18;
        person.c1        = 'a';
        person.c2        = 'b';

        NSLog(@"%@",person);
    }
    return 0;
}
@property (nonatomic, assign) double height;
//赋值身高
person.height    = 178;
image.png
我们发现直接po打印0x4066400000000000,打印不出height的数值178。 这是因为编译器po打印默认当做int类型处理。p/x (double)178:我们以16进制打印double类型值打印,发现完全相同

8字节对齐与16字节对齐

前面我们提及了8字节对齐16字节对齐,这时我们就有疑问,什么时候在哪里采用哪种字节对齐,接下来我们继续源码探索

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

可以看到:

16字节内存对齐算法

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

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
#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;
}

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

malloc中16字节对齐算法原理
为什么需要16字节对齐

原因有一下几点:

总结

综合前文提及的获取内存大小的方式

上一篇下一篇

猜你喜欢

热点阅读