【OC底层】 alloc底层原理

562 阅读4分钟

Pre

objc4-824

提出问题

首先我们有如下代码

YKPerson *p0 = [YKPerson alloc];
YKPerson *p1 = [p0 init];
YKPerson *p2 = [p0 init];

NSLog(@"%@-%p-%p",p0,p0,&p0);
NSLog(@"%@-%p-%p",p1,p1,&p1);
NSLog(@"%@-%p-%p",p2,p2,&p2);

根据打印结果可以看出指向的内存地址空间是一样的,但指针存储在栈中,且为连续存储,每个相隔8字节。

流程分析

我们从NSObjectalloc方法入手,探索对象创建的流程。从下面的代码调用来看,会走到callAlloc方法。

+ (id)alloc {
    return _objc_rootAlloc(self);	// self -> YKPerson
}

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id
callAlloc(Class cls,	// YKPerson
          bool checkNil,	// false
          bool allocWithZone=false) 	// true
{
	···
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
	···
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
	···
}

但实际上在llvm_read_images的过程中,会调用到fixupMessageRef这个函数,在这其中则对alloc的调用进行了重定向

if (msg->sel == @selector(alloc)) {
    msg->imp = (IMP)&objc_alloc;
}

也就是说当我们调用alloc,实际调用的是objc_alloc

id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

callAlloc

上面的判断中我们可以梳理一下在callAlloc方法中的判断流程。

判断条件

fastpath(!cls->ISA()->hasCustomAWZ())

结果为true

根据代码的调用流程可以看到会走到_objc_rootAllocWithZone方法

+ (id)allocWithZone:(struct _NSZone *)zone {
    return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone);
}

结果为false

根据代码的调用流程可以看到会走到_objc_rootAllocWithZone方法

return _objc_rootAllocWithZone(cls, nil);

id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
    // 见下面的详细分析
    return _class_createInstanceFromZone(cls, 0, nil,
                                         OBJECT_CONSTRUCT_CALL_BADALLOC);
}

【核心方法】_class_createInstanceFromZone

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls,	// YKPerson
                              size_t extraBytes,	// 0
                              void *zone,	// nil
                              int construct_flags = OBJECT_CONSTRUCT_NONE,	// OBJECT_CONSTRUCT_CALL_BADALLOC
                              bool cxxConstruct = true,
                              size_t *outAllocatedSize = nil)

基本条件判断

ASSERT(cls->isRealized());
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();	// 是否有C++构造函数
bool hasCxxDtor = cls->hasCxxDtor();	// 是否有C++析构函数
bool fast = cls->canAllocNonpointer();	// 是否可以分配

获取cls的实例所需占用的内存大小

size_t size;
size = cls->instanceSize(extraBytes); // extraBytes == 0
  1. 如果之前已经获取过实例大小,从缓存中取值返回。
    if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
        return cache.fastInstanceSize(extraBytes);
    }

取值时会先判断__builtin_constant_p(extra) && extra == 0

/*
* true
*/
return _flags & FAST_CACHE_ALLOC_MASK16;

/*
* false
*/
size_t size = _flags & FAST_CACHE_ALLOC_MASK; // #define FAST_CACHE_ALLOC_MASK         0x1ff8
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16); // #define FAST_CACHE_ALLOC_DELTA16      0x0008 即align16()为16字节对齐

关于__builtin_constant_p,查过资料后知道这是编译器内置函数。它接受一个数值参数,如果已知参数是一个编译时常量,则返回 1。返回值 0 意味着编译器无法确定参数是否是编译时常量。此内置函数的典型用法是在宏中用于手动编译时优化。

  1. 如果没有,则计算出值。
else {
  size_t size = alignedInstanceSize() + extraBytes;
}
  1. 若size小于16,出于【内存补齐】的原则,补齐16个字节
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;

开辟内存空间,返回地址指针

id obj;
obj = (id)calloc(1, size);

// ->
void	*calloc(size_t __count, size_t __size) __result_use_check __alloc_size(1,2);

将内存地址与类绑定

obj->initInstanceIsa(cls, hasCxxDtor);

// ->
initIsa(cls, false, false);
inline void 
objc_object::initIsa(Class cls,	// YKPerson
                     bool nonpointer,	// false
                     bool hasCxxDtor)	// false
{
    isa_t newisa(0)
···
    newisa.setClass(cls, this);
···
    isa = newisa;
}
  1. 创建一个联合体位域结构isa_t(下面会详细说明)
  2. 绑定到类
inline void
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
···

#elif SUPPORT_INDEXED_ISA
    cls = newCls; // 将传入的newCls赋值给isa_t中的cls

#else
    shiftcls = (uintptr_t)newCls >> 3;
#endif
}

isa

内存结构

union isa_t {
    isa_t() { }  // 构造方法
    isa_t(uintptr_t value) : bits(value) { }  // 构造方法
    uintptr_t bits;

private:
    Class cls;

public:

#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif
    void setClass(Class cls, objc_object *obj);
    Class getClass(**bool** authenticated);
    Class getDecodedClass(**bool** authenticated);
};

我们以arm64真机为例看下ISA_BITFIELD里存放了什么值

define ISA_BITFIELD                
uintptr_t nonpointer        : 1// 是否对isa指针开启指针优化【0:纯isa指针,1:不只是类对象地址,isa中包含了类信息、对象的引用计数等】
uintptr_t has_assoc         : 1// 关联对象【0:没有,1:存在】
uintptr_t has_cxx_dtor      : 1// 该对象是否有C++/Objc的析构器,若有,则需要做析构逻辑,若没有,则可以更快的释放对象
uintptr_t shiftcls          : 33; // 存储类指针的值。开启指针优化的情况下,在arm64架构中有33位用来存储类指针
uintptr_t magic             : 6// 用于调试器判断当前对象是真的对象还是没有初始化的空间
uintptr_t weakly_referenced : 1// 标志对象是否【被指向】/【曾经指向】一个ARC的弱变量,没有弱引用的对象可以更快释放
uintptr_t unused            : 1uintptr_t has_sidetable_rc  : 1// 当引用计数>10时,需要借用该变量存储进位
uintptr_t extra_rc          : 19 // 引用计数

isa流程图

isa流程图.png

isMemberOfClass & isKindOfClass

+ (BOOL)isMemberOfClass:(Class)cls {
    return self->ISA() == cls;  // 比较元类与类
}

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
  for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

- (BOOL)isKindOfClass:(Class)cls {
  for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
        if (tcls == cls) return YES;
    }
    return NO;
}

补充说明

  1. __has_feature(ptrauth_calls):判断编译器是否支持指针身份验证功能,- iPhone X系列以上的设备(arm64e架构,使用Apple A12或更高版本A系列处理器的设备)支持指针身份验证