OC 中创建对象是基础的操作,但我们是否有了解过OC对象是怎么创建的,这篇文章就是从最基础的对象创建起,看一下OC创建对象的过程中到底都做了什么。
对象的创建方式
我们通常创建对象有两种方式
LGPerson *p1 = [[LGPerson alloc] init];
LGPerson *p2 = [LGPerson new];
一. alloc和init
我们先看第一种,第一种创建对象的方式分为了两步,alloc和init, 我分别打印出来这两步的返回值,看看alloc和init分别做了什么,对象是在哪一步创建成功的。
LGPerson *p1 = [LGPerson alloc];
LGPerson *p2 = [p1 init];
LGPerson *p3 = [p1 init];
LGNSLog(@"%@ - %p - %p",p1,p1,&p1);
LGNSLog(@"%@ - %p - %p",p2,p2,&p2);
LGNSLog(@"%@ - %p - %p",p3,p3,&p3);
<LGPerson: 0x600003dc4210> - 0x600003dc4210 - 0x7ffeea1f81d8
<LGPerson: 0x600003dc4210> - 0x600003dc4210 - 0x7ffeea1f81d0
<LGPerson: 0x600003dc4210> - 0x600003dc4210 - 0x7ffeea1f81c8
我们通过打印的日志信息可以发现,在alloc后,p1进行多次init后,p1的值并没有和只是alloc后返回的指针有什么不同,说明在alloc后,对象其实已经创建成功了,所以分析系统怎么创建对象,我们可以分析alloc的流程就知道对象是怎么创建的
注:&p1,&p2,&p3的值不同,只是代表存储p1,p2,p3这三个变量是在不同的地址,但是它们所指向的地址是同一个区域,是同一个变量。
1. alloc 调试
我们想要分析alloc做了什么,就要去对alloc进行断点调试,但是正常的代码调试并不能跑进到alloc里面,所以需要一些特殊的调试方式去查看alloc的底层函数,有三种方式去调试alloc函数。
1)control+stepInto
在断点调试到alloc方法后,按住control健,stepInto的按钮会变化,此时点击进入,就可以看到alloc进入的函数。


2)符号断点
在断点调试到alloc方法后,添加alloc方法的符号断点,进行调试,就会看到alloc进入的函数(一定要在断点到了alloc函数的断点后再放开alloc方法的符号断点,因为在我们alloc之前,会有其他类的alloc。

3)查看汇编
在断点调试到alloc方法后,Debug->Debug WorkFlow->Always Show Disassembly,会显示出当前的汇编信息,可以通过汇编信息查看当前alloc执行进入的方法。

当然之前的方法只能进入到一层,再底层无法在看到,但是可以看到alloc进入的函数在那个lib中,苹果对底层的一些库是开源的,我们可在苹果开源库的网站中找到对应的代码。可以参考Cooci 老师的源码调试
objc4-779.1源码编译调试
2. alloc 流程分析
拿到源码后,我们就可以查看源码,并对源码进行调试,首先是NSObject的alloc
+ (id)alloc {
return _objc_rootAlloc(self);
}
然后进入到_objc_rootAlloc函数
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
再是callAlloc 函数
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
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.
printf("objc_msgSend\r\n");
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
callAlloc 函数有两个流程_objc_rootAllocWithZone 和 objc_msgSend
callAlloc里面有个宏定义
//是1的几率更大一点
#define fastpath(x) (__builtin_expect(bool(x), 1))
//是0的几率更大一点
#define slowpath(x) (__builtin_expect(bool(x), 0))
这两个宏定义的作用只是为了告诉编译器进行优化,因为编译读取指令的时候是一次读多条,而读取指令是非常耗时的,如果没有这两个宏的话,编译器读取指令的时候就会直接读入,而如果编译器知道了这段代码的执行几率的话,读取这段指令的时候就会进行优化。
如果进行objc4源码调试的时候会发现,如果类第一次alloc,会直接走进callAlloc类,然后走到objc_msgSend,然后再重新从NSObject alloc函数进入,一直走到这个callAlloc函数中,再走进_objc_rootAllocWithZone流程中,后面如果类如果再次进行alloc操作的话直接进入callAlloc,走_objc_rootAllocWithZone流程,也就是说类第一次alloc走了两次callAlloc函数。
_objc_rootAllocWithZone 函数
NEVER_INLINE
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
printf("_objc_rootAllocWithZone\r\n");
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
再进入_class_createInstanceFromZone函数,可以发现类的创建主要就是在此函数中创建
/***********************************************************************
* class_createInstance
* fixme
* Locking: none
*
* Note: this function has been carefully written so that the fastpath
* takes no branch.
**********************************************************************/
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 {
// alloc 开辟内存的地方
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);
}
查看_class_createInstanceFromZone函数,发现创建函数主要有三步:
计算类需要开辟的空间.
size = cls->instanceSize(extraBytes);
开辟内存空间
obj = (id)calloc(1, size);
把开辟的空间和类关联
obj->initInstanceIsa(cls, hasCxxDtor);
因此,可以可以知道了从alloc函数进入后的主要流程

2. init 做了什么
从上面的分析可以看出,在alloc了以后,其实对象都已经创建成功了,我们已经可以对对象进行操作了
LGPerson *p1 = [LGPerson alloc];
p1.name = @"sam";
NSLog(@"%@",p1.name);
2020-09-10 22:45:10.903283+0800 001-alloc&init探索[71840:1374047] sam
那么我们为什么还要进行init的操作了,我们查看源码发现init函数只是返回了对象本身,其实init函数只是给我们提供了一个类的入口函数,类似C++里面的一些指针,让我们可以进行一些初始化操作。
- (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;
}
二. new函数
使用new函数也可以创建对象,new函数是通过callAlloc+init函数实现的。
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
我们上面分析了alloc,alloc函数也是通过调用了callAlloc函数实现的,因此new其实就相当于alloc+init的创建对象方式。
网友评论