美文网首页IOS开发知识点
OC对象底层探索 — 由字节对齐到对象内存的分配

OC对象底层探索 — 由字节对齐到对象内存的分配

作者: Dezi | 来源:发表于2020-02-21 20:03 被阅读0次

用于记录iOS底层学习,以备后续回顾

OC对象底层探索

前言

我们继续来探索对象是如何申请、开辟、优化内存大小的。
要想了解对象的内存优化首先要知道内存对齐原则(理论加实践一点点的搞懂)。

一、内存对齐

1.1 内存对齐的三个规则

  • a. 数据成员对齐规则:结构体(struct)(或联合体(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数组、结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。
  • b. 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(struct a里存有struct b,b里有char,int,double等元素,那b应该从8的整数倍开始存储)
  • c. 最后判断:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足要补齐

1.2 内存对齐代码示例探究

经过实际探究,下列示例已涵盖内存对齐的全部原则并加以备注

struct DZStruct1 {
    char a;     // 1    [0]
    double b;   // 8    [8, 15]
    int c;      // 4    [16, 19]
    short d;    // 2    [20, 22]
} MyStruct1; // sizeof(MyStruct1) = 24字节

struct DZStruct2 {
    double b;   // 8    [0, 7]
    char a;     // 1    [8]
    int c;      // 4    [12, 15] 按顺序从9开始,但是起始位置需要从该成员大小的整数倍开始,所以起始位置9需要往大了走,到12为4的整数倍
    short d;    // 2    [16, 17]
} MyStruct2; // sizeof(MyStruct2) = 24字节,结构体的总大小,必须是其内部最大成员的整数倍,不足要补齐所以要从17补齐到24为8的整数倍

struct DZStruct3 {
    char a;     // 1    [0]
    struct DZStruct1 struct1;   // 24   [8, 31] 按顺序从1开始,但是起始位置需要从该结构体(DZStruct1)内部最大元素大小的整数倍开始存储,所以从1往大了走,到8为8的整数倍
    double b;   // 8    [32, 39]
    short d;    // 2    [40, 41]
} MyStruct3; // sizeof(MyStruct3) = 48字节

1.3 字节基础补充

a. 先简单补充一点字节相关基础知识:
sizeof()是运算符,编译的时候就是一个确定的数据会替换为常数,返回的是一个类型所占内存的字节大小。

b. 了解获取内存的三个方法

  • sizeof是运算符,编译的时候就替换为常数,返回的是一个类型所占内存的大小
  • class_getInstanceSize传入一个类对象,返回一个对象的实例至少需要多少内存,它等价于sizeof,需要导入#import <objc/runtime.h>
  • malloc_size返回系统实际分配的内存大小,需要导入#import <malloc/malloc.h>

二、探索对象申请内存和系统分配内存

2.1 首先对无成员变量对象进行探索

DZTeacher.h文件

#import <Foundation/Foundation.h>
@interface DZTeacher : NSObject

main.m文件

#import <Foundation/Foundation.h>
#import "DZTeacher.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZTeacher  *p = [DZTeacher alloc];
        NSLog(@"申请内存大小为:%lu——-系统开辟内存大小为:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
    }
    return 0;
}
分析结果:无成员变量对象应该分配内存:isa的8字节(isa后续会进行详细探索)
实际打印结果 : 
申请内存大小为:8——-系统开辟内存大小为:16

class_getInstanceSize 内部实现如下:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize(); // 直接返回字节对齐大小
}

回忆上一篇文章, alloc 初始化往下探索其中有一个方法_class_createInstanceFromZone,内部会调用size_t size = cls->instanceSize(extraBytes),具体实现如下:

size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;  // 最小返回16
    return size;
}

我们通过查看OC源码可知,class_getInstanceSize返回类的成员变量占据内存的大小,而malloc_size获取obj指针指向内存的大小
分析得出:系统在创建一个对象的时候,对象的isa指针占据8个字节,但是系统会为其分配最少16字节的内存空间,所以如果该对象没有成员变量, class_getInstanceSize 会输出 8 个字节,malloc_size 会输出最少 16 个字节。

2.2 添加成员变量后对象内存探索

DZTeacher.h文件

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@interface DZTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic, strong) NSString *hobby;

main.m文件

#import <Foundation/Foundation.h>
#import "DZTeacher.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        DZTeacher  *p = [DZTeacher alloc];
        // isa ---- 8
        p.name = @"Dezi";   // sizeof(p.name) = 8
        p.age  = 18;            // 4
        p.height = 185;         // 8
        p.hobby  = @"女";       // 8
        NSLog(@"%@",p);
        NSLog(@"申请内存大小为:%lu——-系统开辟内存大小为:%lu",class_getInstanceSize([p class]),malloc_size((__bridge const void *)(p)));
    }
    return 0;
}
分析结果:
1. 成员变量应该分配内存:8字节 + 4字节 + 8字节 + 8字节 = 28字节
2. 再加上isa的8字节 = 36字节(isa后续会进行详细探索)
3. 根据字节对齐原则最大8字节,所以36往大走补充到40字节
4. 所以内存空间应该分配40字节
5. 注意:对象开辟空间的时候成员变量就会编译进来,所以成员变量未赋值也会分配内存

实际打印结果 : 
申请内存大小为:40——-系统开辟内存大小为:48

根据上方打印信息,我们的分析是对的,类对象至少需要40字节,那为什么实际分配内存大小为48字节呢?下来我们继续探索。

2.2 calloc探索

经过对源码的一步步探索,我们发现,在obj = (id)calloc(1, size);这个方法的时候,对象内存大小发生了变化

calloc方法探索 malloc.png

根据上图我们发现calloc方法在malloc源码里边,那我们打开新的源码继续分析:

DZCallocTest.png

之后进入calloc流程,首先调用malloc_zone_calloc方法

calloc.png

在其内部调用zone->calloc初始化并且返回了一个ptr指针

malloc_zone_calloc.png

断点在此处我们发现递归了,那我们在这里打印zone->calloc,我们找到malloc.c文件249行的default_zone_calloc方法

zone->calloc.png

加上断点我们发现此处又是zone->calloc,继续打印zone->calloc发现nano_malloc.c文件中878行的nano_calloc方法

default_zone_calloc.png

在malloc的源码中搜索nano_calloc,于nano_calloc.c文件中找到该方法,其中的核心代码_nano_malloc_check_clear进行内存申请,并且返回一个指针p

zone->calloc.png

_nano_malloc_check_clear内部发现segregated_size_to_fit方法输入的size是40,输出的是48,所以这个方法是开辟内存的算法

_nano_malloc_check_clear.png

segregated_size_to_fit方法进行分析,发现对齐原则是16字节对齐,所以输入实际需要的40,经过16字节对齐后输出的为48

segregated_size_to_fit.png nano_zone_common.png

2.3 总结+流程图

对象内存大小的申请是按照8字节对齐,不满16字节时按照16字节计算;若大于16字节时,calloc实际开辟内存则是按照16字节对齐。
感觉8字节对齐是为了属性之间内存安全提高容错空间,16字节对齐是为了保证对象之间内存安全提高容错空间。

calloc.png

相关文章

网友评论

    本文标题:OC对象底层探索 — 由字节对齐到对象内存的分配

    本文链接:https://www.haomeiwen.com/subject/bfhzoctx.html