OC底层探索-alloc底层原理

265 阅读4分钟

我们通常会通过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

16231453279173.jpg 3.我们发现,alloc走到了 objc_alloc方法中

16231454260243.jpg 4.找到objc_alloc之后,我们添加一个符号断点

16231458906147.jpg 5.会找到 libobjc.A.dylib```objc_alloc 16231459797069.jpg

二、使用汇编

1.在alloc打一个断点 2.当断点来到alloc时,我们选择Debug->Debug Workflow->Always Show Disassembly

16231465995817.jpg 3.此时可以看到以汇编的模式看到调用堆栈信息,也可以看到调用objc_alloc 16231467701063.jpg

三、直接使用alloc符号断点

1.在alloc打一个断点 2.当断点来到alloc时,我们添加一个alloc的符号断点 16231473435877.jpg 3.我们发现来到了libobjc.A.dylib``[NSObject alloc]方法中

16231474109677.jpg

3.源码编译

iOS_objc4-818.2 源码编译调试

4.alloc流程图

16231625322218.jpg

oc对象的创建流程

从图中我们可以看出,OC对象的创建主要是三个函数

  1. cls->instanceSize()计算内存大小
  2. calloc(1, size) 开辟内存空间
  3. 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;
    }
  1. 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 指针与对象进行关联。

完整流程图

16232097577743.jpg

这里为什么创建一个对象的时候,要走两次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