美文网首页iOS底层原理文集
iOS底层原理探究02-alloc真正流程 & 结构体内存对齐

iOS底层原理探究02-alloc真正流程 & 结构体内存对齐

作者: superFool | 来源:发表于2021-07-26 14:13 被阅读0次

    一、alloc的真实流程

    1.1 实际探索中发现alloc流程底层是先调用objc_alloc

    先下个断点看下alloc实际调用的是什么方法


    断住alloc方法

    查看汇编代码


    汇编代码

    可以看到汇编代码实际调用的是objc_alloc,这就很奇怪了,源代码里明明是alloc调用的_objc_rootAlloc 上源码

    + (id)alloc {
        return _objc_rootAlloc(self);
    }
    

    1.2分析objc_alloc的调用流程

    这是为啥呢,来搜一下objc_alloc吧看看能不能找到线索


    image.png

    在objc-runtime-new.mm文件里找到fixupMessageRef(message_ref_t *msg)函数里修改了alloc的imp 再看下调用方可以发现是在_read_images方法里调用的通过方法名和注释知道fixupMessageRef(message_ref_t *msg)函数是用来fix up objc_msgSend_fixup 修复消息错误的 也就是修复之前对alloc的hook


    image.png
    继续往上查找调用链发现有两个调用链
    • _objc_init_image -> _read_images -> fixupMessageRef
    • _objc_init -> _dyld_objc_notify_register(取map_images的地址作为参数调用) -> map_images -> map_images_nolock -> _read_images -> fixupMessageRef

    image的加载 和 _objc_init调用都是dyld加载应用程序的时候执行的,而在这个早的时机就需要修复之前的hook,那对alloc的hook只能更早,猜测是在编译阶段就hook了

    1.3验证真实的alloc流程

    llvm源码中对alloc的hook
    llvm的源码中也找到了对alloc hook的代码,证实了我们的猜想。

    结论
    llvm在编译阶段对alloc做了hook,创建对象时第一次调用alloc方法会进入objc_alloc方法然后进入callAlloc方法callAlloc方法里通过objc_msgSend再次调用alloc方法这次调用才会走alloc的正常流程(即iOS底层原理探究01-alloc底层原理里所说的流程)

    二、结构体内存对齐

    2.1对象大小的影响因素探索

    先来看看源码静态分析

    static ALWAYS_INLINE id
    _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
                                  int construct_flags = OBJECT_CONSTRUCT_NONE,
                                  bool cxxConstruct = true,
                                  size_t *outAllocatedSize = nil)
    {
        ASSERT(cls->isRealized());
    
        // Read class's info bits all at once for performance
        bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
        bool hasCxxDtor = cls->hasCxxDtor();
        bool fast = cls->canAllocNonpointer();
        size_t size;
    
        size = cls->instanceSize(extraBytes);
        if (outAllocatedSize) *outAllocatedSize = size;
    
        id obj;
        if (zone) {
            obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (slowpath(!obj)) {
            if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
                return _objc_callBadAllocHandler(cls);
            }
            return nil;
        }
    
        if (!zone && fast) {
            obj->initInstanceIsa(cls, hasCxxDtor);
        } else {
            // Use raw pointer isa on the assumption that they might be
            // doing something weird with the zone or RR.
            obj->initIsa(cls);
        }
    
        if (fastpath(!hasCxxCtor)) {
            return obj;
        }
    
        construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
        return object_cxxConstructFromClass(obj, cls, construct_flags);
    }
    
    
    

    2.2结构体内存对齐

    结构体内存对齐

    各数据类型占用的内存
    结构体内存对齐原则
    1. :数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
      ⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要
      从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,
      结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存
      储。 min(当前开始的位置m n) m = 9 n = 4
      9 10 11 12
    2. 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从
      其内部最⼤元素⼤⼩的整数倍地址开始存储.(struct a⾥存有struct b,b
      ⾥有char,int ,double等元素,那b应该从8的整数倍开始存储.)
    3. 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,.必须是其内部最⼤
      成员的整数倍.不⾜的要补⻬。

    根据对齐原则分析结构体的内存大小

    struct LGStruct1 {
        double a;       // 8    [0 7]
        char b;         // 1    [8]
        int c;          // 4    (9 10 11 [12 13 14 15]
        short d;        // 2    [16 17] 24
    }struct1;
    
    struct LGStruct2 {
        double a;       // 8    [0 7]
        int b;          // 4    [8 9 10 11]
        char c;         // 1    [12]
        short d;        // 2    (13 [14 15] 16
    }struct2;
    
    struct LGStruct3 {
        double a;               // 8    [0 ~ 7]
        int b;                  // 4    [8 9 10 11 12]
        char c;                 // 1    [13]
        short d;                // 2    [14 15]
        int e;                  // 4    [16 17 18 19]
        struct LGStruct1 str;   // 24   (20 21 22 23 [24 ~ 48)
    }struct3;
    

    struct1中a占用0 ~ 7八个字节,b占用第8个位置一个字节,c因为是4字节的要从4的整数倍开始存所以9、10、11位置留空要从12位置开始存占用12 ~ 15四字节,因为16位置是2的整数倍所以d占用16 ~ 17两字节,最后因为struct1中最大的成员是double类型的a占用8字节 所以整个结构体的大小要是8的整数倍,即整个结构体要占用24个字节 ,同理可以得到struct2 大小是16 struct3大小是 48

    接下来我们验证一下

    NSLog(@"%lu-%lu-%lu",sizeof(struct1),sizeof(struct2),sizeof(struct3));
    

    打印结果为


    内存大小验证

    打印结果跟我们的分析是一致的

    最后我们在通过一个问题引出下一篇对malloc的探索


    image.png
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface LGPerson : NSObject
                                               //隐藏的isa 8
    @property (nonatomic, copy) NSString *name;//8
    @property (nonatomic, copy) NSString *nickName;//8
    @property (nonatomic, assign) int age;//4
    @property (nonatomic, assign) long height;//8
    
    @end
    
    NS_ASSUME_NONNULL_END
    

    上面是LGPerson的定义 对象的本质是结构体指针(下一篇会详细讲),通过结构体字节对齐的规则计算得到LGPerson底层的结构体应该占用40个字节
    但是打印结果为啥是8 - 40 - 48呢,

    • 首先8是打印的person它是一个指针,8字节没有问题
    • class_getInstanceSize([LGPerson class])返回类对象至少需要多少空间,打印的是40 通过我们的计算也没有问题
    • malloc_size((__bridge const void *)(person)) 返回的是实际分配的内存空间,打印的是48,这就有问题了系统为啥要分配48呢,这个问题我们下一篇详细分析一下(因为malloc分配内存的时候是16字节对齐的,这里提前给懒人剧透一下)

    补充:内存打印指令
    x /nuf <addr>
    n表示要显示的内存单元的个数


    u表示一个地址单元的长度:
    b表示单字节
    h表示双字节
    w表示四字节
    g表示八字节


    f表示显示方式,可取如下值:
    x按十六进制格式显示变量
    d按十进制格式显示变量
    u按十进制格式显示无符号整型
    o按八进制格式显示变量
    t按二进制格式显示变量
    a按十六进制格式显示变量
    i指令地址格式
    c按字符格式显示变量
    f按浮点数格式显示变量

    相关文章

      网友评论

        本文标题:iOS底层原理探究02-alloc真正流程 & 结构体内存对齐

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