OC对象原理之内存对齐

329 阅读3分钟

上一篇文章(OC对象原理之alloc流程分析)重点分析了OC创建对象的方法alloc的调用流程及每个方法的作用,这篇文章继续探索OC创建对象的过程中是如何计算所占空间的大小的?是如何开辟内存的?苹果底层做了哪些优化处理? ​

alloc流程最终调用到objc源码中的_class_createInstanceFromZone函数,具体实现如下:

/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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);
}
  • 这里的重点方法是:
    • size = cls->instanceSize(extraBytes); 计算占用内存大小
    • obj = (id)calloc(1, size); 开辟内存空间
    • obj->initInstanceIsa(cls, hasCxxDtor); 初始化isa,关联类
    • return obj; 返回对象地址

如何计算占用内存大小的?

  • instanceSize的具体实现
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.
    if (size < 16) size = 16;  
    return size;
}
  • 通过断点调试,定位到实际调用的是return cache.fastInstanceSize(extraBytes);fastInstanceSize的具体实现:
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);
    }
}

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
  • __builtin_constant_p:是LLVM内置的函数,用来判断某个表达式是否是一个常量(参考:LLVM编译器中的内置(built-in)函数
  • 通过断点调试,很容易定位到,这里执行了else中的代码,可以看出:
    • 调用了align16,即进行16字节对齐
    • 这里可能会有两个疑问:
      • 为什么会执行cache.fastInstanceSize
      • _flags是什么时候赋值的?
      • 带着这两个问题,我们继续探索

为什么会执行cache.fastInstanceSize?

  • hasFastInstanceSize实现:
#define FAST_CACHE_ALLOC_MASK         0x1ff8

bool hasFastInstanceSize(size_t extra) const
{
    if (__builtin_constant_p(extra) && extra == 0) {
        return _flags & FAST_CACHE_ALLOC_MASK16;
    }
    return _flags & FAST_CACHE_ALLOC_MASK;
}
  • LLDB查看FAST_CACHE_ALLOC_MASK的二进制值:(p/t为以二进制形式输出)

image.png

  • 可以看出_flags & FAST_CACHE_ALLOC_MASK即为保留_flags的第3到12位的值(最后一位为第0位),其他位清零

_flags是什么时候赋值的?

  • LGPerson的定义

image.png

  • 在当前文件中全局搜索_flags =,定位到_flags赋值的位置
  • 通过断点动态调试

image.png image.png image.png image.png image.png

  • 通过断点调试可以发现,_flags赋值会调用两次,第二次传入的值即为LGPerson的实际大小

image.png

  • 查看此时的函数调用栈,可以看到_flags赋值时的调用流程
    • 在通过[LGPerson alloc]创建对象的时候,先调用了objc_alloc
    • 然后调用的callAlloc
    • 然后调用objc_msgSend
    • 此时会去缓存中查找,如果没有找到就会将其先加载到缓存中,再调用alloc
    • 所以在instanceSize执行中都会走缓存中的方法

image.png

  • 到这里可能会有一个疑问,为什么LGPerson占用的内存是24呢?这是因为OC的类在底层实际上是一个结构体,接下来看下结构体的内存对齐原则。(备注:这个还有另一个疑问,_flags的作用是什么?这里先记录一下,保留这个疑问,后面的文章会继续探索...)

补充:结构体内存对齐原则

不同类型数据占内存大小

  • 首先需要了解下不同数据类型占用内存空间大小(iOS开发中现在使用的都是64位架构)

image.png

结构体内存对齐原则:

  • 数据成员对齐规则:
    • 结构体(struct)或联合体(union)的数据成员,第一个成员放在offset为0的位置
    • 以后每个数据成员存储的起始位置要从该成员大小或成员的子成员大小(只要该成员有子成员,比如数组或结构体)的整数倍开始存储
  • 结构体作为成员:
    • 如果一个结构体里有其他结构体成员,则结构体成员要从其内部最大元素大小的整数倍位置开始存储
  • 收尾工作:
    • 结构体的总大小(sizeof计算大小),必须是其内部最大成员的整数倍,不足要补齐

练习:

  • 练习一:下面两个结构体占用内存大小?
struct LGStruct1 {
    double a;      
    char b;         
    int c;         
    short d;        
}struct1;

struct LGStruct2 {
    double a;      
    int b;          
    char c;         
    short d;        
}struct2;
  • 答案:struct1占用内存大小为24字节,struct2占用内存大小为16字节(可以使用sizeof(struct1)查看占用内存大小)
  • 解释:
    • struct1
      • double类型占8字节,第一个成员从位置0开始,即占[0 - 7]位置
      • char类型占1字节,从位置8开始,即占[8]位置
      • int类型占4字节,从位置9开始,因为不是第一个成员,所以要从该成员大小的整数倍位置开始存储,位置9不是4的整数倍,所以从位置12开始存储,即空出(9 - 11)位置,占[12 - 15]位置
      • short类型占2字节,从位置16开始,即占[16 - 17]位置
      • 所有成员所占位置为[0 - 17],共18个字节,进行收尾工作,此结构体重最大成员所占内存为double占8字节,所以该结构体占内存大小必须为8的整数倍,即最终结果为占24字节;
    • struct2
      • double类型占8字节,第一个成员从位置0开始,即占[0 - 7]位置
      • int类型占4字节,从位置8开始,即占[8 - 11]位置
      • char类型占1字节,从位置12开始,即占[12]位置
      • short类型占2字节,从位置13开始,因为不是第一个成员,所以要从该成员大小的整数倍位置开始存储,位置13不是2的整数倍,所以从位置14开始存储,即空出(13)位置,占[14 - 15]位置
      • 所有成员所占位置为[0 - 15],共16个字节,进行收尾工作,此结构体重最大成员所占内存为double占8字节,所以该结构体占内存大小必须为8的整数倍,即最终结果为占16字节;

  • 练习二:下面结构体占用内存大小?
struct LGStruct3 {
    double a;
    int b;
    char c;
    short d;
    int e;
    struct LGStruct1 str;
}struct3;
  • 答案:struct3占用内存大小为48字节
  • 解释:
    • double类型占8字节,第一个成员从位置0开始,即占[0 - 7]位置
    • int类型占4字节,从位置8开始,即占[8 - 11]位置
    • char类型占1字节,从位置12开始,即占[12]位置
    • short类型占2字节,从位置13开始,因为不是第一个成员,所以要从该成员大小的整数倍位置开始存储,位置13不是2的整数倍,所以从位置14开始存储,即空出(13)位置,占[14 - 15]位置
    • int类型占4字节,从位置16开始存储,即占[16 - 19]位置
    • struct LGStruct1类型,从上面练习中可以知道,占24字节,从位置20开始存储,结构体LGStruct1中的最大成员为double类型,占8字节,所以要从该结构体内部最大成员的整数倍位置开始存储,即空出(20 - 23)位置,占[24 - 47]位置
    • 所有成员所占位置为[0 - 47],共48个字节,进行收尾工作,此结构体重最大成员所占内存为double占8字节,所以该结构体占内存大小必须为8的整数倍,即最终结果为占48字节;

对象的内存是如何存储的?

  • 创建LGPerson
@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic) BOOL height;
@property (nonatomic) short sh;

@end
  • LGPerson对象p的内存存储

image.png

  • 从上图可以看到
    • 类都已一个默认的成员isa,存储在第一个位置
    • 类转成结构体会对成员进行重新排列,来优化占用的内存空间,这里将int类型的ageBOOL类型的heightshort类型的sh存储到了同一个8字节中;
    • sizeof(p):结果为8,因为p是一个指针变量,占8字节
    • class_getInstanceSize([p class]):结果为24,使用该方法需要先导入#import <objc/message.h>,作用是打印LGPerson类对应的结构体占用内存大小
    • malloc_size((__bridgevoid *)p):结果为32,使用该方法需要先导入#import <malloc/malloc.h>,作用是打印p开辟的实际内存大小
  • 测试:给LGPerson增加属性
@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic) BOOL height;
@property (nonatomic) short sh;
@property (nonatomic, copy) NSString *nickname;

@end
  • 打印结果为

image.png

  • 可以看出class_getInstanceSize([p class])的结果从24变为32,即属性对对象的大小是有影响的
  • 同理测试添加方法,可以看出,方法对对象的大小是没有影响的

如何开辟内存空间的?

  • 继续上面的探索,计算完开辟对象占用的空间大小后,会调用obj = (id)calloc(1, size);开辟内存,calloc底层是如何开辟内存空间的呢?
  • 按住command+control点击calloc,可以看到calloc的声明
void	*calloc(size_t __count, size_t __size) __result_use_check __alloc_size(1,2);
  • 再继续点击,无法看到calloc的具体实现,此时查看文件的路径可以发现,calloc的具体实现可能在libmalloc源码中

image.png

libmalloc源码分析

  • calloc方法的具体实现
void *
calloc(size_t num_items, size_t size)
{
	return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}

MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
		malloc_zone_options_t mzo)
{
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

	void *ptr;
	if (malloc_check_start) {
		internal_check();
	}
	ptr = zone->calloc(zone, num_items, size);

	if (os_unlikely(malloc_logger)) {
		malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
				(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
	}

	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
	if (os_unlikely(ptr == NULL)) {
		malloc_set_errno_fast(mzo, ENOMEM);
	}
	return ptr;
}
  • 从上面源码可以看出,calloc会调用_malloc_zone_calloc_malloc_zone_calloc方法的返回值为ptr,可以快速定位到此方法中的主要代码为:ptr = zone->calloc(zone, num_items, size);
  • 继续查看zone->calloccalloc的实现,点击进去只能看到声明如下:
void 	*(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
  • 这里再点击进入,依然无法查看到具体的实现,但是仔细观察可以发现,这里是类似函数指针的定义形式,是不是在什么地方有赋值?全局搜索一下

image.png

  • 发现确实是有赋值的地方,动态调试打印一下?

image.png

  • 打印发现这里调用的是default_zone_calloc,继续调试

image.png

  • default_zone_calloc中又调用了nano_calloc

image.png

  • 这里调试可以发现,最终会调用_nano_malloc_check_clear

image.png

  • 这里最终会调用segregated_next_block

image.png

  • 此方法会找到合适的内存地址,并返回内存地址;
    • 从这个方法中看到,计算使用到了slot_bytes
    • _nano_malloc_check_clear中计算slot_bytes是调用了segregated_size_to_fit
    • segregated_size_to_fit的具体实现:(这个函数的作用是进行16字节对齐,并且最小为16字节)
#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;
}

libmalloc中calloc调用流程

image.png

总结

  • 结构体的内存对齐最终取决于最大成员变量大小,对OC对象来说,一般为8字节(最少有一个结构体指针类型的isa)
  • 对象与对象之间的内存对齐是16字节对齐