alloc 对象的指针地址和内存
在我们日常开发过程中,对象初始化应该是我们每个人都经常遇到的,但是对象初始化过程中具体做了哪些事情呢?下面我们来一起探究一下对象初始化的过程,希望能解答一二。
下面我们通过一个案例来看一下。
首先创建一个 LGPerson 对象,对 p1 做 alloc 操作, 对 p2、p3 分别做 init 操作, 然后分别打印 p1、p2、p3 及它们的地址跟指针的地址,然后我们可以看到 p1、p2、p3 的地址都一样(难道 init 真的什么都不做吗?这个问题放到后面来解答),但是它们指针的地址却不一样。这里我们可以得到一些结论:
-
LGPerson类在执行完alloc方法之后就开辟了内存空间,有了指针的指向。 -
p2、p3跟p1指向的内存相同,说明init方法没有对指针做操作。 - 在执行完
init方法后我们通过对p1、p2、p3指针取地址打印,可以看到指针地址并不相同,且是以 8 个字节的间隔连续存储在栈空间(ox7开头的是栈空间)。
现在我们想点进去看看 alloc 跟 init 方法的具体实现,我们可以看到都是只有声明没有实现。那我们有什么其他办法能看到具体实现呢?下面我们会介绍三种分析方法。
底层探索的三种方法
-
1:结合符号断点
首先我们在 alloc 方法下断点,断点来到之后按住 control 键并点击向下的箭头 step into,然后会看到底层方法的调用 objc_alloc,知道了底层调用了哪个方法,我们就可以下符号断点。符号断点之后我们可以看到是执行了 libobjc.A.dylib 库中的 objc_alloc 方法。
- 2: 汇编跟流程
这里我们对
alloc 方法打断点,通过 Debug Workflow 生成汇编代码可以看到,这里调用的是 objc_alloc 方法。接着就跟方法一一样,下符号断点。
-
3:对已知方法下符号断点
对 alloc 方法直接下符号断点。
汇编结合源码调试分析
上面我们已经定位了底层方法由 libobjc.A.dylib 库调用,那么我们就来分析下源码的执行流程。这里提供了源码下载的网址 苹果开源源码汇总。下载好源码之后,我们首先搜索 alloc方法。
现在通过运行代码,然后打断点我们一步一步的看一下源码内部都怎么执行的。
-
[LGPerson alloc]
首先第一步在起始位置打上断点。
-
[NSObject alloc]
第二步我们可以看到来到了NSObject类的alloc方法,因为所有类都是继承于NSObject类。
-
objc_alloc
这一步我们会遇到一些问题,因为第二步命名调用的是_objc_rootAlloc,但是这里却来到了objc_alloc方法里面。那么为什么会走到这里了呢?我们在如下代码中可以找到答案。
我们都知道方法编号跟
IMP 指针是一一对应的,fixupMessageRef 方法表示如果符号绑定失败了就会触发一个这样的修复操作,当 sel 等于 alloc 的时候,就把 imp 指向绑定为 &objc_alloc ; 这和平常使用的 Method Swizzling 很相似,不过这里只是临时交换了一下,而 Method Swizzling 是永久交换。
-
callAlloc
这里可以看到在 1931 行有个判断,第一次调用的时候并没有走到 1932 行,而是在 1940 行对cls也就是LGPerson类发送了alloc方法。 -
_objc_rootAlloc
这里就会先进入 alloc 方法,然后调用 _objc_rootAlloc 方法。
-
_objc_rootAllocWithZone
现在就再次进入了callAlloc方法,这也是callAlloc方法调用两次的原因。这次会走到 1932 行,调用_objc_rootAllocWithZone方法。 -
_class_createInstanceFromZone
在 _objc_rootAllocWithZone 方法之后会进入到 _class_createInstanceFromZone 方法,在这个方法里面我们着重关注几个地方,在 7977 行这个会计算出 LGPerson 类相应的内存大小;7984 行会在堆区开辟出 size 大小的内存空间;7994 会绑定 isa 与 LGPerson 类相关联, 并赋值了 hasCxxDtor c++ 相关的方法与函数;8002 行返回 obj 对象。
那么问题来了,是如何知道该开辟多大的内存空间呢?具体我们来看一下 instanceSize 方法。
inline size_t instanceSize(size_t extraBytes) const {
// 如果有缓存会返回缓存的 size 大小
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
// 如果没有缓存,会通过 alignedInstanceSize() + extraBytes 这一步来计算 size 大小
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
通过 instanceSize 方法我们可以看到,如果有缓存会返回缓存的 size 大小, 如果没有就会通过 alignedInstanceSize() + extraBytes 这一步来计算 size 大小。开辟空间的大小与类的成员变量有关。继承于 NSObject 的类都会有一个 isa 成员变量。如果当前类没有其他成员变量,返回的 size 大小就是 isa 的大小8 字节。但是因为 8 自己小于 16 字节,根据字节对齐原则,最后返回的是 16 字节。
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
Class isa OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
通过代码也可以看到,isa 是 Class 类型,objc_class 是一个结构体类型。isa 是结构体指针类型,所以是 8 个字节。objc_class 继承与最原始的类型 objc_object。
最后我们通过打印可以看到对象内存的首字节就
isa 及当前对象的内存情况。
allco 方法调用流程图
最后通过流程图来总结下对象的创建流程。
alloc 流程图










网友评论