阅读 193

iOS 底层原理01: NSObject, alloc, init, new源码分析

一. OC对象的实质是什么

我们可以通过 clang -rewrite-objc main.c -o main.cpp 将OC代码编译成C\C++代码来查看:

// 定义一个Person类
@interface Person : NSObject
{
    int _age;
    int _height;
    NSString *_name;
}
@end
复制代码

编译过后的核心代码如下:

/// NSObject
struct NSObject_IMPL {
    Class isa; // 8字节
};

/// Person
struct Person_IMPL {
    struct NSObject_IMPL NSObject_IVARS; // 8字节
    int _age;  // 4字节
    int _height; // 4字节
    NSString *_name; // 8字节
};
复制代码

可以看到, NSObject的类都被编译成了结构体. 并且 NSObject结构体还多了一个isa指针

二. OC类占用多大内存空间

我们可以通过runtime和malloc的库函数打印一下

// 查看类在内存中占用空间
#import <objc/runtime.h>
class_getInstanceSize(Class  _Nullable cls)

// 查看对象实际分配的空间
#import <malloc/malloc.h>
malloc_size(const void *ptr)

// 输出结果
NSObject:class_getInstanceSize: 8
NSObject: malloc_size: 16
Person: class_getInstanceSize: 16
Person: malloc_size: 16
复制代码

首先可以看到, NSObject类 占用空间 8个字节, 通过编译以后的结构体我们也可以知道 一个 isa指针占用8个字节, 同理, Person类 占用 8+4+4+8 = 24个字节可以理解.

那么为什么, 实际分配的地址分别是 16, 32 呢? 我们下边就通过源码查看一下, 为什么

三. alloc 的源码探索

  1. alloc 函数
//alloc函数
+ (id)alloc {
    return _objc_rootAlloc(self);
}
复制代码
  1. _objc_rootAlloc 函数
id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
复制代码
  1. callAlloc函数
static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;  // 判断参数, Cls为当前Class checkNil传入的是false
    if (fastpath(!cls->ISA()->hasCustomAWZ())) { // 判断是否自定义+allocWithZone实现
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif
    // 判断allocWithZone参数, 上文传入的是 true
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
复制代码

fastpath 和 slowpath 是2个宏定义, 具体如下:

#define fastpath(x) (__builtin_expect(bool(x), 1))(__builtin_expect(bool(x), 1)) 表示x很可能为真, fastpath 可以简称为 真值判断, 表示if里边的代码执行概率很大

#define slowpath(x) (__builtin_expect(bool(x), 0))(__builtin_expect(bool(x), 0)) 表示x很可能为假, slowpath 可以简称为 假值判断 表示if里边的代码执行概率很小

  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)
{
    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;
    // 计算需要的空间, extraBytes上一步传入的为0
    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) {
        // 将cls 与 obj 指针关联
        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的核心操作, 主要实现了3个任务:

  1. cls->instanceSize(extraBytes): 计算需要开辟的空间大小
  2. (id)calloc(1, size): 申请内存, 返回指针地址
  3. initInstanceIsa(cls, hasCxxDtor): 将类与isa指针关联
  • 1.1 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.
        // 最小16字节
        if (size < 16) size = 16;
        return size;
}
复制代码
  • 1.2 fastInstanceSize和align16, 计算空间大小, 进行16字节对齐
// 计算空间大小
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);
        }
}

// 16字节对齐算法
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
复制代码

为什么要内存字节对齐:

  1. CPU在存取数据时, 是以块为单位存取, 可以通过字节对齐, 减少存取次数, 减少CPU的开销
  2. OC对象的isa指针占用8个字节, 如果没有其他属性时, 使用8字节对齐, 会导致这个对象在内存中与其他对象的isa指针紧挨着, 容易造成访问混乱, 空出8字节可以更安全的访问

字节对齐的规则:

  1. 每个成员变量起始地址offset要是当前成员所占大小的整数倍
  2. 如果某个成员变量是结构体, 结构体成员的起始地址offset要以子结构体大小的整数倍
  3. 结构体的总大小, 必须是内部最大成员的整数倍, 不足的要补齐
  • 2 根据计算过后的内存大小, 向内存申请空间,赋值给obj, 此时obj指向该内存地址
  • 3 根据申请好的内存地址与当前传入的cls, 将指针和isa 进行关联

通过源码, 我们就知道了为什么 NSObject的实际内存大小是16字节(最小16字节), 以及为什么Person为什么是32字节(16字节对齐)

四. init 源码分析

- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}
复制代码

init 只是将当前self返回到外面

五. new 源码分析

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
复制代码

可以看到, new 方法直接调用了callAlloc() 方法以后 调用 init, 所以会有 new= alloc + init 的说法

六. calloc源码分析

在objc4的calloc中, 我们就无法在继续深入看到源码, 因为calloc在libmalloc源码中, 下边我们就继续探索源码

  1. calloc 函数
void * calloc(size_t num_items, size_t size)
{
	void *retval;
	retval = malloc_zone_calloc(default_zone, num_items, size);
	if (retval == NULL) {
		errno = ENOMEM;
	}
	return retval;
}
复制代码
  1. malloc_zone_calloc 函数
void * malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
	MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
	void *ptr;
	if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
		internal_check();
	}
        // 在这里设置断点才可以继续调试calloc
	ptr = zone->calloc(zone, num_items, size);
	if (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);
	return ptr;
}
复制代码
  1. 通过断点, 我们找到calloc实际调用的是default_zone_calloc
static void * default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
	zone = runtime_default_zone(); // 创建zone
        // 第二个断点
	return zone->calloc(zone, num_items, size); // 用zone calloc
}
复制代码
  1. runtime_default_zone函数
static inline malloc_zone_t * runtime_default_zone() {
	return (lite_zone) ? lite_zone : inline_malloc_default_zone();
}
复制代码
  1. inline_malloc_default_zone 函数
static inline malloc_zone_t * inline_malloc_default_zone(void)
{
	_malloc_initialize_once();
	// malloc_report(ASL_LEVEL_INFO, "In inline_malloc_default_zone with %d %d\n", malloc_num_zones, malloc_has_debug_zone);
	return malloc_zones[0];
}
复制代码
  1. 回到default_zone_calloc 的第二个断点, 发现实际调用的是nano_calloc函数
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
	size_t total_bytes;

	if (calloc_get_size(num_items, size, 0, &total_bytes)) {
		return NULL;
	}

	if (total_bytes <= NANO_MAX_SIZE) {
                // 核心方法
		void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
		if (p) {
			return p;
		} else {
			/* FALLTHROUGH to helper zone */
		}
	}
	malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
	return zone->calloc(zone, 1, total_bytes);
}
复制代码
  1. _nano_malloc_check_clear函数简版代码
static void * _nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
        /// ...
        // 16位对齐
	size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
	mag_index_t mag_index = nano_mag_index(nanozone);
	nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
	ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
	if (ptr) {
        /// ....
	} else {
                // 获取内存指针
		ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
	}

	if (cleared_requested && ptr) {
		memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
	}
	return ptr;
}
复制代码

_nano_malloc_check_clear 核心有2个操作

  1. segregated_size_to_fit(args...):16位对齐
  2. segregated_next_block(args...): 获取内存指针
    1. segregated_size_to_fit, 通过位运算进行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) { // 16倍数对齐
		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;		
        // k =  (size + 15) >> 4 << 4		 									// Zero-based!
	return slot_bytes;
}
复制代码
    1. segregated_next_block 简版代码
static MALLOC_INLINE void *
segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
     while (1) {
        // 当前可用内存的结束地址  
	uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
	// 偏移到下个地址
        uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
	// 减去偏移量
        b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.

	if (b < theLimit) {   // Did we stay within the bound of the present slot allocation?
	    // 如果当前地址还在范围内, 直接返回
            return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
	} else {
	    if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
		pMeta->slot_bump_addr = theLimit;
		return 0;				 // We're toast
	    } else {
		// One thread will grow the heap, others will see its been grown and retry allocation
		_malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
		// re-check state now that we've taken the lock
		if (false //...)) {
			/// ...
                } else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
			_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
			continue; // ... the slot has been successfully grown by us. Now try again.
			} else {
			/// ...
			}
		}
	 }
     }
}
复制代码
    1. segregated_band_grow函数, 开辟新band
static boolean_t
segregated_band_grow(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
        /// ...
	return TRUE;
}
复制代码

这部分代码还不是十分理解, 需要进一步解读

文章分类
iOS
文章标签