我们通常会通过alloc创建OC对象,那么alloc都做了哪些事情呢,如何查看alloc底层呢,带着这些问题,开始今天的探索
1.首先我们先看一下 alloc&init
我们先看下面的代码
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@"%@---%p---%p",p1,p1,&p1);
NSLog(@"%@---%p---%p",p2,p2,&p2);
NSLog(@"%@---%p---%p",p3,p3,&p3);
我们创建了p1、p2、p3三个对象,这三个对象有什么关系呢,我们打印一下这三个对象、对象地址以及对象指针的地址
<Person: 0x600001a41ce0>---0x600001a41ce0---0x7ffedfe681c8
<Person: 0x600001a41ce0>---0x600001a41ce0---0x7ffedfe681c0
<Person: 0x600001a41ce0>---0x600001a41ce0---0x7ffedfe681b8
通过打印我们发现 p1、p2、p3这三个对象、对象的地址是一样的,但是指针的地址不一样。 我们还能看出这三个指针地址是连续的栈地址。也就是说,三个连续的栈地址空间指向的是同一个内存空间。
这是怎么做到的呢,init又做了什么,我们带着问题继续探索。
2.怎么找到alloc的实现呢
我们可以通过三种方式寻找
一、先下断点跟一下代码
1.在alloc打一个断点
2.当断点来到alloc时,按住control键点击step into
3.我们发现,
alloc走到了 objc_alloc方法中
4.找到
objc_alloc之后,我们添加一个符号断点
5.会找到
libobjc.A.dylib```objc_alloc
二、使用汇编
1.在alloc打一个断点
2.当断点来到alloc时,我们选择Debug->Debug Workflow->Always Show Disassembly
3.此时可以看到以汇编的模式看到调用堆栈信息,也可以看到调用
objc_alloc
三、直接使用alloc符号断点
1.在alloc打一个断点
2.当断点来到alloc时,我们添加一个alloc的符号断点
3.我们发现来到了
libobjc.A.dylib``[NSObject alloc]方法中
3.源码编译
4.alloc流程图
oc对象的创建流程
从图中我们可以看出,OC对象的创建主要是三个函数
cls->instanceSize()计算内存大小calloc(1, size)开辟内存空间- obj->initInstanceIsa()将内存和对象关联起来
cls->instanceSize()计算内存大小
inline 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;
}
fastInstanceSize()计算内存大小 有缓存的情况下fastInstanceSize()是怎么计算内存大小的
size_t fastInstanceSize(size_t extra) const
{
ASSERT(hasFastInstanceSize(extra));
if (__builtin_constant_p(extra) && extra == 0) {
return _flags & FAST_CACHE_ALLOC_MASK16;
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
}
}
align16()计算内存大小
按为运算16字节对齐(为了有一定的容错性)
static inline size_t align16(size_t x) {
return (x + size_t(15)) & ~size_t(15);
}
2.alignedInstanceSize()计算内存大小
无缓存情况下通过alignedInstanceSize()计算内存大小 8字节对齐(空间换时间)
uint32_t alignedInstanceSize() const {
return word_align(unalignedInstanceSize());
}
unalignedInstanceSize()计算内存大小
没有对齐的内存空间,依赖于成员变量的大小
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}
字节对齐 8字节对齐 (x + WORD_MASK) & ~WORD_MASK;/// (x+7) & ~7
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
为什么是16字节对齐,怎么容错
如果是8字节对齐的话, 对象之间 isa 都是紧邻的, 没有一点空隙, 如果内存访问出现了一点错误或者偏移, 就会访问到其他对象, 就会出现一些野指针,内存访问错误之类, 发生了混乱, 所以为了使得对象之间的访问更加的安全, 就需要给对象之前预留一部分的空间, 预留多少合适呢, 毫无疑问, 当然是8字节, 为什么不是1,2,4字节呢? 一方面16字节是可以占用最小空间的合理内存空间, 即8的倍数, 另一方面也更加的安全, 各个对象的内存都是16字节, 偏移和访问时, 可以很好地进行对齐.
为什么以8字节对齐,而不是16或者32
CUP每次读取都是固定8字节读取,以空间换取时间。但是有时候好几个char 1字节,没必要每个char补齐8字节,可以拼凑在一起。
calloc(1, size) 开辟内存空间
通过alloc开辟的内存空间返回的值并没有执行所定义的类。
obj->initInstanceIsa()将内存和对象关联起来
创建isa指针,并与对象关联
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
总结:alloc 的核心作用就是开辟内存,通过 isa 指针与对象进行关联。
完整流程图
这里为什么创建一个对象的时候,要走两次
callAlloc方法? 通过llvm分析,苹果做了插桩处理:
第一次:执行
alloc时,会通过方法映射,调用objc_alloc,此时做了 插桩 操作(做标记receiver),接下来就是第一次调用callAlloc->objc_msgSend(alloc)
第二次:再次执行
alloc,再次执行objc_alloc,发现有标记存在,所以不再执行objc_alloc方法,而是调用本身的alloc,进而执行_objc_rootAlloc->callAlloc->objc_msgSend(allocWithZone)
补充
init
- (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;
}
看起来init什么都没做,你给它一个对象,它给你返回一个对象,那有什么用呢?工厂设计模式,构造函数,用来给子类重写。一句话就是提供接口,便于扩展。
new
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
等同于alloc init