iOS开发底层之内存对齐详解-03

Posted iOS_developer_zhong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发底层之内存对齐详解-03相关的知识,希望对你有一定的参考价值。

前言

由于上一篇文章没有详细分析Alloc的内存对齐,本篇幅详细讲解Alloc的核心-内存对齐。
我们先来回顾下Alloc的整个流程。
在这里插入图片描述

详细分析Alloc底层到底做了什么?

  1. 我们直接从 callAlloc 入手, 前面两部从 objc源码中可直接看下,进入这个核心方法,源码展示📢看注释
//核心推荐处
static ALWAYS_INLINE id
callAlloc(Class cls, bool ch`在这里插入代码片`eckNil, bool allocWithZone=false)
{
//有可用的编译器优化,我们当前使用的版本
#if __OBJC2__     

    //
    if (slowpath(checkNil && !cls)) return nil;
    
    判断一个类是否有自定义的 +allocWithZone 实现,没有则走到if里面的实现
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    // __OBJC2_ 版本不支持
    // No shortcuts available.
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

笔记

  • slowpath是什么?
//x很可能为真, fastpath 可以简称为 真值判断
#define fastpath(x) (__builtin_expect(bool(x), 1)) 
  • fastpath是什么?
//x很可能为假,slowpath 可以简称为 假值判断
#define slowpath(x) (__builtin_expect(bool(x), 0)) 

__builtin_expect() 是 GCC (version >= 2.96)提供给程序员使用的,目的是将“分支转移”的信息提供给编译器,这样编译器可以对代码进行优化,以减少指令跳转带来的性能下降.
__builtin_expect(bool(x),1) 表示 x 的值为真的可能性更大;
__builtin_expect(bool(x),0) 表示 x 的值为假的可能性更大。

也就是说,使用likely(),执行 if 后面的语句的机会更大,使用 unlikely(),执行 else 后面的语句的机会更大。通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着前面的代码,从而减少指令跳转带来的性能上的下降。

  1. 跳入 _objc_rootAllocWithZone源码中
id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}
  1. 跳入 _class_createInstanceFromZone源码中 ,重点来了。 注意看注释
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)
{
    // 断言 检查是否已经存在,
    // 合理运用断言,可以大大减少bug哦,注意设置xcode设置断言的幻境,在生产版本关闭下。 
    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;
    
    // 1. 计算当前所需要开辟的内存大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    // 2.申请内存,并返回对应内存指针
    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);
}

现在开始分析Alloc中的内存对齐

第一步

  1. 第一步计算当前所需要开辟的内存大小
    进入源码前,先记录下结论:
    对象所需要开辟的内存大小与什么有关? 属性,成员变量,方法,协议?
    内存的大小与成员变量与属性有关系。 与方法没没关系。

可以通过LLDB命令
po class_getInstanceSize(ZgrStudent.class) 打印当前的类的占用内存大小。

  1. 进入源码分析size = cls->instanceSize(extraBytes) ,解读下源码
   // 额外开辟的大小 extraBytes
    inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);
        }

        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        // 注意: 最少16个字节分配
        if (size < 16) size = 16;
        return size;
    }
   
    size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }
	
	/// #   define WORD_MASK 7UL   (十进制 7)
	/// 字节对齐,以8字节对齐
	/*
	算法:
	如: x = 8 
	则公式为 : (8 + 7) & (~7)  注: ~取反
	八字节对齐 算法
	*/  
	static inline uint32_t word_align(uint32_t x) {
    	return (x + WORD_MASK) & ~WORD_MASK;
	}
    
    // 十六字节对齐 算法
    static inline size_t align16(size_t x) {
        return (x + size_t(15)) & ~size_t(15);
    }

2. 相关结论:
2.1 为什么分配的是8字节对齐与16字节对齐?
16字节对齐,是防止通过8字节对齐的时候,位置不够产生错误, 访问错误, 溢出。 所以使用16字节对齐,可以防止出错的概率。
通过固定8字节去读取内存,是为了提高CPU的读写效率,以空间换区时间。还有一个原因是OC里面属性最大需要的字节为8字节。
2.2 当存放一个char类型与一个Int类型的时候,难道要占用16字节来存储吗?
可以不需要16个字节来存储,可通过字节对齐进行优化
2.3 lldb相关操作
x 对象 , 当前的isa地址 ,注意 ios端为小端模式,从右往左读地址。
po 上面的地址 & 0x00007ffffffffff8ULL (x86_64真机环境下固定偏移)
比如:在这里插入图片描述
给对象增加点属性,在通过lldb 来读取下。
通过:lldb 终端输入 x/4gx p,打印出内存地址
首地址都是isa地址。
然后通过po分别打印内存地址,看看能发现什么,操作如下:
在这里插入图片描述

上述命令相对应的解释如下图:
在这里插入图片描述
3. 内存对齐

内存对齐原则:

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

看个例子:
在这里插入图片描述
字节对齐总结:

  1. 堆对象的内存为 16字节对齐
  2. 成员变量 8字节对齐,结构内部
  3. 对象 对象 16字节,前面有说为什么使用十六字节对齐。

以上是关于iOS开发底层之内存对齐详解-03的主要内容,如果未能解决你的问题,请参考以下文章

Go语言内存对齐详解

内存对齐详解 (C++代码)

iOS之深入解析内存管理Tagged Pointer的底层原理

驱动开发函数详解之Wdm

iOS开发底层之对象的本质-04

Linux内存管理之mmap详解 (可用于android底层内存调试)