OC底层探索 - alloc & init & new

136 阅读5分钟

本文章使用环境:

Xcode: 13.3
源码 objc4-838.1
测试设备: iPnone7 15.3.1

alloc , init方法,作为开发者相信都已经敲过无数遍了。

如果问:alloc, init分别有什么作用,相信大家都能回答出来分配内存和初始化。

如果再问:alloc, init具体是怎样实现的呢?这时候就需要通过苹果底层源码来看了。

init

我们先看一个例子:

@interface TestClass : NSObject
@property (nonatomic, copy) NSString *name;
@end


TestClass *test = [TestClass alloc];
TestClass *a = test;
TestClass *b = test;

test.name = @"test_name";

NSLog(@"name test: %@, a: %@, b: %@", test.name, a.name, b.name);
NSLog(@"地址 test: %p, a: %p, b: %p", test, a, b);

大家可能会说,代码有问题,实例创建没有进行init,但是这个代码是可以正常运行的,输出如下:

image.png

根据输出信息,我们可以看出,alloc之后就有分配一块完整可用的内存了,实例已经可以正常使用了。咦?那init做了什么,这不是不需要init了吗。

对此,我的理解是,init是苹果给提供的一个工厂方法,系统原来的init什么也没做。现在到源码中来验证一下。因为OC中所有类都继承自NSObject, 所以我们找到源码的NSObject.mm文件,从中可以找到:

- (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方法的确什么都没有做,就是一个工厂方法。

我们还可以使用汇编来证明这一点。本次需要的汇编相关知识,请查看文章底部 汇编相关 部分。 首先修改代码

TestClass *test = [TestClass alloc];
TestClass *a = [test init];

在项目中加一个 [NSObject init] 的符号断点,当运行到 TestClass *a = [test init];, 时打开符号断点。

image.png 可见,init方法中就只有一个返回,没有其他的操作。我们可以打印一下寄存器的值,看到正是当前测试类的实例。 image.png

alloc

把上面示例中的类拿到源码项目中

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        TestClass *test = [TestClass alloc];
        NSLog(@"");
    }
    return 0;
}

通过断点来查看,方法的调用顺序是:

callAlloc -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone

奇怪了,明明是调用的alloc方法,为什么前面会多出一个callAlloc方法。

我们离开源码项目,到示例项目中,打上断点,当运行到 alloc 方法时,显示汇编。

image.png 从注释可以看出,这是调用了 objc_alloc, 可是我们明明调用的是 alloc方法。从源码中可以找到答案。

objc-runtime-new.mm 中,可以找到如下代码:

static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == @selector(retain)) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == @selector(release)) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == @selector(autorelease)) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
        }
    } 
    else if (msg->imp == &objc_msgSendSuper2_fixup) { 
        msg->imp = &objc_msgSendSuper2_fixedup;
    } 
    else if (msg->imp == &objc_msgSend_stret_fixup) { 
        msg->imp = &objc_msgSend_stret_fixedup;
    } 
    else if (msg->imp == &objc_msgSendSuper2_stret_fixup) { 
        msg->imp = &objc_msgSendSuper2_stret_fixedup;
    } 
#if defined(__i386__)  ||  defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fpret_fixup) { 
        msg->imp = &objc_msgSend_fpret_fixedup;
    } 
#endif
#if defined(__x86_64__)
    else if (msg->imp == &objc_msgSend_fp2ret_fixup) { 
        msg->imp = &objc_msgSend_fp2ret_fixedup;
    } 
#endif
}

从中可以看到,方法名的大概意思是修正消息,而当消息的 selalloc 时,让 imp 指向 objc_alloc 。所以当我们调用 alloc 时,实际上调用的是 objc_alloc 的方法实现。

我们在 objc_alloc 方法打上断点,果然,调用 alloc 方法,先会走到 objc_alloc, 然后, objc_alloc 的方法中,调用了 callAlloc

所以上面的调用流程是不完整的,完整的调用流程应该是:

objc_alloc -> callAlloc -> alloc -> _objc_rootAlloc -> callAlloc -> _objc_rootAllocWithZone -> _class_createInstanceFromZone

我们到示例代码中,通过汇编和符号断点验证一下。

打一个 objc_alloc 的符号断点

image.png

有2个调用,通过逐步运行调试,发现走的是第2个方法调用

image.png

这里可以通过寄存器数据读取( register read )来确定相关信息, 寄存器中 x0 , x1, x2 ... 按顺序存储参数,而且 objc_msgSend 前2个默认参数 id self, sel _cmd, 所以我们可以打印出来,当前的类和方法。

image.png 再加一个 alloc 符号断点

image.png

再加一个 _objc_rootAlloc 符号断点

image.png

image.png

再加一个 _objc_rootAllocWithZone 符号断点

image.pngret 处加个断点,打印

image.png

可见 _objc_rootAllocWithZone 方法创建了实例。

汇编看到的流程跟源码中的类似,但是我们没有看到 callAlloc 方法和 _class_createInstanceFromZone 方法。这是因为有进行编译器优化的原因。

编译器优化

我们到 Build Settings 中搜索 Optimization Level , 可见有好多种优化等级

image.png image.png

虽然我们 Debug 模式下是 None,但是并不意味着没有进行编译器优化,这只是可选的最低等级的优化。我们可以通过修改优化等级来证明。例如,把Debug 模式下指定为与 Release 相同的 Fastest, Smallest 模式。

None 模式下汇编

image.png

Fastest, Smallest 模式下汇编

image.png

可见优化等级越高,汇编中的指令越少。少的部分就是编译器优化掉的部分。

相关方法源码

objc_alloc

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

callAlloc

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

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

alloc

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

_objc_rootAlloc

id
_objc_rootAlloc(Class cls)
{
    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);
}

_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;

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

new

TestClass *test = [TestClass new];

image.png 到源码中查看

// Calls [cls new]
id
objc_opt_new(Class cls)
{
#if __OBJC2__
    if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
        return [callAlloc(cls, false/*checkNil*/) init];
    }
#endif
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}

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

可见 new 方法 就等于 alloc + init

汇编相关

开/关汇编显示

Xcode中 Debug -> Debug Workflow -> Always Show Disassembly

汇编指令

bbl : 跳转指令, 可以理解为函数调用

ret : return的意思,可以理解为函数的返回

;# : 代表注释