本文为L_Ares个人写作,包括图片皆为个人亲自操作,以任何形式转载请表明原文出处。
用OC开发已经4年了,虽然现在swift的确很好用,但是因为很多的项目还是以OC为基本来搭建架构的,而且swift也是在OC的思想基础上优化的,所以对OC的学习总结还是很有必要的。
想了一下,决定从alloc
函数开始探究OC的思想,因为最开始的面相对象,都会用到[[Class alloc] init]
。那么一个实例的出现到底经历了什么,这就需要去探索一下。
本文可能会用到一些汇编语言,具体的汇编语言可以自行某歌或某度。
一、alloc的作用
首先,先创建好一个project
,并且创建一个继承于NSObject
的类,用这个类的初始化来观察一下,alloc
到底是什么作用,例如创建一个JDPerson
类,并在controller里面初始化。
- (void)viewDidLoad {
[super viewDidLoad];
JDPerson *p = [JDPerson alloc];
JDPerson *p1 = [p init];
JDPerson *p2 = [p init];
NSLog(@"p的内存地址 : %@ \n p的指针地址 : %p",p,&p);
NSLog(@"p1的内存地址 : %@ \n p1的指针地址 : %p",p1,&p1);
NSLog(@"p2的内存地址 : %@ \n p2的指针地址 : %p",p2,&p2);
// Do any additional setup after loading the view.
}
执行结果如下图1.1所示 :
可以明显的发现,三个JDPerson
对象的内存地址是一样的,而指针地址是不一样的,那么这也就代表了alloc
只申请了一块内存空间,init
分配了三个指针空间,但是三个指针全部都指向了alloc
申请的内存空间。
如图1.2所示:
也就是说,如果这个JDPerson
类含有属性的话,以这种方式初始化出来的对象,设置任意一个对象,都会让其他对象的属性发生变化,因为他们全部都指向同一块内存地址。
所以,JDPerson
的对象实际上是在alloc
的时候创建出来的。那么为什么又要一个init
呢?原因很简单啊,init
可以让我们diy
啊,也就是可以重载重写override
。
但是本节不以init
为重点,所以继续探究这个alloc
是如何创建出来了一个对象的呢?
二、alloc如何创建出来的对象
需要一下alloc
的源码。这是我上一节扩展配置好的,有需要的可以直接下载使用。
那现在来看一下,alloc
到底是如何实现的,它在OC中的目的到底是什么。
首先随意的创建一个NSObject
的子类。
JDPerson *p = [JDPerson alloc];
我们commond
进去,可以看到
+ (id)alloc {
return _objc_rootAlloc(self);
}
但是实际上第一步并不是直接调用了这个,而是调用了objc_alloc
,这个会在后面说明。
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
继续下探,
比较明显的是C的代码,调用了callAlloc
,传入了一个cls
对象。继续进入
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));
}
这里就很明显了,有三个判断,首先搞清楚slowpath
是什么fastpath
又是什么。
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
两个宏,全都是参数传入了__builtin_expect(a,b)
函数,这个函数可以自行查找,意义是a == b
的概率更大。所以,slowpath
基本上是走不成了,因为checkNil
传进来就是false
。
那么看fastpath
。fastpath
检查了cls
这个类或者它的父类是否有自定义alloc
或者allocWithZone
的实现,这个AWZ
就是Alloc With Zone
的首字母缩写。如果有的话,那么就将进入_objc_rootAllocWithZone
。
如果allocWithZone
,也就是callAlloc
传进来的第三个参数是true
,并且,这个类没有自定义,或者它的父类也没有自定义实现allocWithZone
,那么就会调用objc_msgSend
发送cls
这个类需要实现allocWithZone
。
最后无论如何,前面的条件全都不符合了,那么就会直接objc_msgSend
消息,直接调用alloc
。
那么这段代码的整体意思,也就大概是这样子。
我们需要从中挑出来,更核心的代码,一个是_objc_rootAllocWithZone
,一个是objc_msgSend
。
那么找那个可以继续跟入实现的函数_objc_rootAllocWithZone
进去看。
三、内部的实现
从_objc_rootAllocWithZone
跳转进来,找到的函数。
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
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);
}
这里发现了,出现了一个obj
对象。而且发现返回的函数object_cxxConstructFromClass
有使用这个obj
,那么给它上个断点,来看一下,它的变化。
下面这张图,是我给obj
下了断点后,向下step
了几步之后,obj
出现了值。
这时候确定这是一个NSObject
,但是,它是不是我的JDPerson
,我还不知道,因为它只是开辟了一个内存空间,这也只是内存空间的地址。
但是,在这里,我们可以看到一个isa
。如果想要内存地址和你的类关联,那么其中就需要存在着isa
指针,来表达这次的指针的指示关系。
到这里,已经可以确认,alloc
申请了内存,具体给了谁,现在还不知晓,但是这个isa
表明了它有指示对象。
但是,alloc
申请了内存空间,这是确认的了。而且,如果自己实践一下会发现,obj
是在你step进入到calloc
以后才有了内存地址的。而isa
是你在step进入到initInstanceIsa
的时候出现的。
所以,可以确定的是,alloc
是利用了calloc
分配到了内存,利用initInstanceIsa
创造出来的isa
指针与这块内存发生了联系,确认了类的详细归属。
四、alloc申请的内存
上面说了,obj
利用calloc
申请了内存,但是这个内存申请多少,是谁告诉它的?这就需要继续根据刚才的代码来看,代码我就不重复贴一大段了,贴需要看的地方。
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);
}
这里calloc
动态申请了1个长度为size
的内存空间,这个size
在开始就定义了size_t size
,并且通过cls->instanceSize(extraBytes)
,获得了数值。这里也可以直接挂上断点来看,的确是拿到了需要的内存大小。
那么也就是说,alloc
能拿到多少的内存空间,看的是instanceSize
给它分配了多少的内存空间,所以我们就需要看一下instanceSize
的源码。
size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
源码中的注释明确的表明了一点,CoreFoundation
的所有的对象,至少都要给16字节大小的空间。
从头来看,思路很简单,内存里面能拿到需要申请的这么大的内存的话,那就直接返回内存里面有的。
如果没有的话,出现了一个函数alignedInstanceSize(unalignedInstanceSize())
,实例对齐。
将没有对齐的实例传进去,这个函数会帮我们对齐。
没有对齐的实例的源码:
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
这里不多说这个,知道ro
是当前这个类的对象里面,所有的ivarList
,methodList
,PropertyList
等等这些list
编译进去的属性的大小就行了。
那为什么要对齐,可以直接看这一篇扩展1
内存和isa的绑定
obj->initInstanceIsa(cls, hasCxxDtor);
这一行代码将cls
和刚才的isa
做到了绑定,从而obj
开始知道了它是JDPerson
类。
你可以在第一次运行之后再打一个断点到我打断点的位置,应该走的就是从缓存区里面拿到你的size
,走的就是alloc
进来的时候的_objc_rootAlloc
,而不是objc_alloc
了,因为你可以看到
不再进行第二次的c++的绑定了。
那这个时候,已经是开始为你创建的对象分配内存,并且将isa
和分配的内存进行绑定了。
那第二次再进来的时候,你的obj
已经是JDPerson
或者JDMan
(我自己新建的,和JDPerson
一样的,只是改了个名字)。
最后上一下alloc
的实际流程图
图片引自。他写的更为详细。
五、关于init
其实init
的源码就很简单了。
+ (id)init {
return (id)self;
}
- (id)init {
return _objc_rootInit(self);
}
类方法直接返回了自己,而实力方法则是调用了_objc_rootInit
那我们继续看_objc_rootInit
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
的意义何在?其实很简单,就是为了让你自己可以对他进行自定义的操作,也就是我们经常说的,重写。