OC对象的原理(上) -- alloc的本质

前因

新建一个 iOS App 项目工程,然后先来看段代码

MYObject *obj1 = [MYObject alloc];
MYObject *obj2 = [obj1 init];
MYObject *obj3 = [obj1 init];

NSLog(@"%@-%p-%p", obj1, obj1, &obj1);
NSLog(@"%@-%p-%p", obj2, obj2, &obj2);
NSLog(@"%@-%p-%p", obj3, obj3, &obj3);

结果输出

<MYObject: 0x10060e110>-0x10060e110-0x7ffeefbff470
<MYObject: 0x10060e110>-0x10060e110-0x7ffeefbff460
<MYObject: 0x10060e110>-0x10060e110-0x7ffeefbff468

通过分析上面的结果,我们发现,obj1、obj2、obj3 这三个变量都指向了同一个对象 0x10060e110

为什么?
为什么?我明明调用了 init 方法啊
为什么?难道 init 并没有创建对象给我吗

alloc 到底干了什么

带着前面的疑问,我们一起走进 alloc 方法,看看它到底干了什么事情

定位 alloc

想要探究 alloc 方法的实现,那么我们必须先要定位到 alloc 的源码所在位置;有 3 中方式可以定位:

1、符号断点

  • 1.1 MYObject *obj1 = [MYObject alloc]; 该行代码先打个断点
  • 1.2 下一个 alloc 符号断点(Symbolic Breakpoint…)
  • 1.3 先禁用该符号断点
  • 1.4 执行程序,当进入 1.1 的断点时启用 1.2 的符号断点
  • 过掉当前断点,即可得到结果

2、control + step into

3、汇编查看跟流程

当进入断点后,点击 Debug –> Debug Workflow –> Always Show Disassembly

上面的 2 和 3 方法都只能找到 alloc 最终调用了 objc_alloc,但是这个方法又是在哪定义的呢?我们现在也不知道,所以需要结合 1 方法来进行定位。

通过上面的定位,我们知道了 alloc 方法位于 libobjc.A.dylib 动态库中,也就是 objc 库。

分析 alloc

知道了 alloc 的源码所在,接下来我们就开始编译调试 objc 源码,有关编译过程请参考我的这篇文章 objc4-818.2 源码编译调试,这里不再赘述。

LLVM 优化

我们明明调用的是 alloc 方法,但是通过上面的跟踪定位,你会发现最终调用的都是 objc_alloc 函数,这意味着 @selector(alloc) 所对应的 imp 被做了手脚并指向了 objc_alloc 的地址。相关代码大家可以查阅下 fixupMessageRef(message_ref_t *msg) 这个函数,下面是该函数的部分代码:

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

通过该函数我们发现,苹果不仅仅是 Hook 了 alloc 方法,也 Hook 了其他的方法。

alloc 分析

类首次调用 alloc 流程会有点区别

MYObject *obj1 = [MYObject alloc];
MYObject *obj2 = [MYObject alloc];
  • MYObject *obj1 = [MYObject alloc]; 的执行流程

  • MYObject *obj2 = [MYObject alloc]; 的执行流程

非首次调用的时候,少了 alloc 这一层的调用,原因在于 objc_alloc -> callAlloc 的时候,走了 _objc_rootAllocWithZone 这个分支,并未走 objc_msgSend 流程,所以最终并未调用 [NSObject alloc] 方法,感兴趣的话自己可以深入研究研究。

alloc 流程图

不太会画图, 后面再补上吧

new 又做了什么?

以下是 new 方法的具体实现:

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

通过上面对 alloc 的分析我们已经很清楚知道,alloc 最终也是调用 callAlloc 方法,所以可以得出结论 [NSObject new] 实际等同于 [[NSObject alloc] init]

既然 new 这么好用,不仅能节省代码量还更贴近现代语言语义,为什么大家都不用呢?

上面 new 的源码已经十分清楚地告诉你了,new 内部只能调用 init 方法来进行初始化,并不适用于带参初始化的场景。

比如:

@interface MYObject : NSObject

@property (nonatomic, copy) NSString *name;

- (instancetype)initWithName:(NSString *)name;

@end

针对 initWithName: 这种带参初始化,new 显然做不到,所以有它自身的局限性,并不是不推荐使用,而是实在是有点鸡肋。

后果

因为对象内存是在 alloc 方法中进行开辟的,init 方法默认不做任何处理直接返回对象本身,所以 obj1、obj2、obj3 会同时指向同一个对象(内存地址)。