[iOS] OC底层探索——对象原理之alloc init new

390 阅读6分钟

前言

本文使用objc源码版本为:objc4-818.2

常见的对象初始化如下:

    Foo *Obj0 = [[Foo alloc] init];
    Foo *Obj1 = [Foo new];

其中,alloc init new三个关键字的意义可以理解为:

  • alloc : 创建对象并分配内存
  • init : 对象初始化
  • new : 理解为alloc + init,创建并初始化了对象

init

示例代码如下

    Foo *obj = [Foo alloc];
    Foo *obj1 = [obj init];
    Foo *obj2 = [obj init];

调试分析

上文讲到init函数执行了对象初始化,这意味着init函数并不进行对象的创建工作。

运行示例代码,可以发现obj1obj2的对象地址与obj相同,即init并没有实际创建对象。

image.png

汇编分析

断点在init语句汇编代码如下,可以发现init是通过objc_msgSend(消息发送)机制被执行的。 image.png

源码分析

- (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实际上什么都没有做就直接返回了,结合源码注释可以猜测,实际开发中[super init]是不能保证被调用到的,所以init中不敢放有意义的操作。

new

示例代码

    Foo *obj0 = [Foo new];
    Foo *obj1 = [Foo new];

调试分析

通过new创建出的是两个不同的对象,并且都完成了初始化。

image.png

汇编分析

通过汇编代码可发现,[Foo new]具体执行时,是通过函数地址调用了objc_opt_new函数,而不是通过消息发送向Foo发送new函数,这里是因为LLVM针对new, alloc等函数做了一层拦截优化。 image.png 在源码中找到objc_opt_new的定义如下:

// Calls [cls new]

id
objc_opt_new(Class cls)
{
    #if __OBJC2__
    // 当该类有默认的 new/self/class/respondsToSelector/isKindOfClass方法时 
    // 直接返回[[class alloc] init]
    if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
        // 相当于[[class alloc] init]
        return [callAlloc(cls, false/*checkNil*/) init];
    }
    #endif
    // 通过消息发送执行new函数
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}

源码分析

new的源码定义如下,上文objc_opt_new中通过消息发送执行new即调用这段代码中。

+ (id)new {
    // 该函数不必展开 只需理解为[[class init] init]即可
    return [callAlloc(self, false/*checkNil*/) init];
}

alloc

示例代码

Foo *obj0 = [Foo alloc];
Foo *obj1 = [Foo allocWithZone:nil];

调试分析

两次alloc创建出了两个不同的对象,且他们的age属性并没有被初始化为18。

alloc和allocWithZone的区分 allocWithZone:可以理解为历史包袱,下文中不进行单独分析。

image.png

汇编分析

通过汇编代码发现通过地址直接调用了objc_alloc函数(LLVM做了优化)。

image.png 查看objc_alloc函数定义如下,callAlloc函数在下文中进行分析

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

// Calls [cls allocWithZone:nil].
id
objc_allocWithZone(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}

源码分析

callAlloc定义如下:

// 名词解释
// slowPath:告诉编译器该处条件判断更可能为false
// fastPath:告诉编译器该处条件判断更可能为true
// hasCustomAWZ: 是否有自定义的alloc实现(如果没有默认alloc实现则为false)

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    #if __OBJC2__
    // 检查当前创建的Class是否为nil
    if (slowpath(checkNil && !cls)) return nil;
    // 当前Class是否有自定义的alloc方法(是否有默认alloc方法)
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // 重点!! 创建对象&&分配内存的具体函数
        return _objc_rootAllocWithZone(cls, nil);
    }
    #endif
    // No shortcuts available.
    // allocWithZone的逻辑 不必关注
    if (allocWithZone) {
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    // 当前存在自定义alloc方法 通过消息发送机制调用自定义alloc方法
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

通过消息发送调用的alloc函数如下:

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

id _objc_rootAlloc(Class cls)
{
    // 执行回callAlloc函数 不过此时不再检查Class是否为空
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

执行对象创建和分配内存的函数_objc_rootAllocWithZone:

NEVER_INLINE
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);
}

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
    // class or superclass has .cxx_construct/.cxx_destruct implementation
    // 类是否有c++的构造/析构函数
    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 {
        // !!通过calloc函数分配内存空间!!
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        // 分配内存失败 调用对应的Handler函数
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }
    // !!初始化ISA!!
    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);
    }
    // 没有c++构造函数则直接返回创建好的obj
    if (fastpath(!hasCxxCtor)) {
        return obj;
    }
    // 返回执行c++构造函数后的obj
    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

函数内我们最关注的主要有三个步骤:

  1. 计算创建对象所需的大小: size = cls->instanceSize(extraBytes);
  2. 申请内存: obj = (id)calloc(1, size);
  3. 初始化 ISA: obj->initInstanceIsa(cls, hasCxxDtor);

流程分析

alloc的流程相对较长 需要借助流程图进行梳理

(修正:_objc_rootAlloc(self)->objc_alloc改正为_objc_rootAlloc(self)->callAlloc) 流程图.jpg

本地调试时发现,即使没有自定义alloc,第一次进入callAlloc时,hasCustomAWZ()判断也为true;第二次进入callAlloc时,hasCustomAWZ()才返回false。这里结合源码分析该现象:

// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define FAST_CACHE_HAS_DEFAULT_AWZ    (1<<14)

bool hasCustomAWZ() const {
    return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
void setHasDefaultAWZ() {
    cache.setBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}
void setHasCustomAWZ() {
    cache.clearBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}

根据源码可发现该函数实际判断位信息FAST_CACHE_HAS_DEFAULT_AWZ,即是否有默认的alloc/allocWithZone实现。 而第一次进入callAlloc时,该位信息为0,表示没有默认的alloc实现,第二次进入callAlloc时,该位信息已经变成了1。 在setHasDefaultAWZ()处断点,发现两次进入callAlloc期间该函数确实被执行了,栈信息如下:

image.png 两处关键信息:

1.第一次进入callAlloc后,因为hasCustomAWZ()true,走了objc_msgSend(alloc)的分支。

2.向Class发送消息时,如果该类还没有实现or初始化,则会进行实现&初始化,也就是在这时候,我们的类的FAST_CACHE_HAS_DEFAULT_AWZ位信息被设为了1。

综上,第一次进入callAllochasCustomAWZ()返回true是因为我们的类这时候还未接收过任何消息,并没有初始化过,所以并没有默认的alloc实现。而此时通过消息发送机制调用该类的alloc,则会触发类的初始化,所以在第二次进入callAlloc时,hasCustomAWZ()返回了false

我们可以通过在第一次调用[Foo alloc]前先向Foo类发送一条消息(执行一次函数调用)进行验证。

image.png 通过上图可证明,在第一次向Foo类发送消息时,确实触发执行了setHasDefaultAWZ(),此时再执行callAlloc时,hasCustomAWZ()将返回false,直接执行_objc_rootAllocWithZone