本文为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所示 :
1.1.png
可以明显的发现,三个JDPerson对象的内存地址是一样的,而指针地址是不一样的,那么这也就代表了alloc只申请了一块内存空间,init分配了三个指针空间,但是三个指针全部都指向了alloc申请的内存空间。
如图1.2所示:
1.2.png
也就是说,如果这个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出现了值。
3.1.png
这时候确定这是一个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
4.1.png
内存和isa的绑定
obj->initInstanceIsa(cls, hasCxxDtor);
这一行代码将cls和刚才的isa做到了绑定,从而obj开始知道了它是JDPerson类。
你可以在第一次运行之后再打一个断点到我打断点的位置,应该走的就是从缓存区里面拿到你的size,走的就是alloc进来的时候的_objc_rootAlloc,而不是objc_alloc了,因为你可以看到
图片.png
不再进行第二次的c++的绑定了。
那这个时候,已经是开始为你创建的对象分配内存,并且将isa和分配的内存进行绑定了。
那第二次再进来的时候,你的obj已经是JDPerson或者JDMan(我自己新建的,和JDPerson一样的,只是改了个名字)。
图片.png
最后上一下alloc的实际流程图
图片引自。他写的更为详细。
4.2.png
五、关于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的意义何在?其实很简单,就是为了让你自己可以对他进行自定义的操作,也就是我们经常说的,重写。












网友评论