iOS 底层探索01——alloc初探

494 阅读6分钟

这是我参与更文挑战的第1天,活动详情查看: 更文挑战

引言

我们每天都在和不同的OC对象打交道,虽然我们知道对象只是内存上的一块区域上存放的数据,但通过alloc创建对象的过程中到底发生了什么我们并不清楚,今天一起来探究下;

首先一起看如下代码

    NSObject *person = [GCPerson alloc];
    NSObject *person1 = [person init];
    NSObject *person2 = [person init];
    NSLog(@"对象地址%@  %@   %@",person, person1,person2);
    NSLog(@"指针指向的地址 %p  %p   %p",person, person1,person2);
    NSLog(@"指针自身的地址 %p  %p   %p",&person, &person1,&person2);

打印结果为

对象地址<GCPerson: 0x600003d64060>  <GCPerson: 0x600003d64060>   <GCPerson: 0x600003d64060>
指针指向的地址 0x600003d64060  0x600003d64060   0x600003d64060
指针自身的地址 0x7ffeed1d5568  0x7ffeed1d5560   0x7ffeed1d5558
  1. 第一行打印的是person、person1、person2三个对象的内存地址。
  2. 第二行指针指向的地址person、person1、person2三个指针对象指向的内存地址;
  3. 第三行打印的是&person、&person1、&person2 三个指针对象自身的地址;由于person、person1、person2 对象是位于ViewDidLoad函数开辟的栈帧上的三个对象,所以他们的地址值是存放在栈空间上的从高地址到低地址的三个连续地址;
  4. 前两行打印的是存放在堆区上的一块内存区域,存放着通过alloc创建的对象,但为什么alloc后的对象和init后对象的地址值是一样的?我们猜测是可能是因为对象的内存地址分配是alloc执行的,init并不操作对象的内存地址; 如图所示

堆栈内存占用.jpg 带着上述疑惑,我们尝试了找到alloc方法调用的相关信息

底层探索方式:汇编和源码配合分析找到相应方法

1. 直接断点找到符号;

  1. 进入断点后,配合control + step into查看调用信息

2.3.jpg

1.100.jpg

  1. 找到对应符号信息 1.111.jpg

  2. 设置符号断点查看信息 1.3.jpg

1.4.jpg

  1. 最终找到对应的方法 1.5.jpg

2. 通过查看汇编代码找到对应符号查看调用;

  1. 在我们开发中某些时候想进入汇编模式下查看当前代码在汇编层面是如何执行的,可以通过以下设置进入汇编调试模式:Xcode -> Debug -> Debug Workflow -> Always Show Disassembly

截屏2021-06-07 23.54.28.png

  1. 进入汇编模式下查看执行信息

2.3.jpg

2.4.jpg 3. 这里由于我们使用的是模拟器,看到的是X86汇编,当我们使用arm架构的手机进行汇编调试时会看到不同的汇编指令;例如X86汇编的callq指令在arm汇编下可能会变成bl指令,但不会影响实际的调用结果不用太在意;

3. 直接添加alloc符号断点;

  1. 我们已经知道要查找的是alloc方法,那么可以直接给alloc方法添加符号断点,但需要注意的是alloc方法的调用频率太高,为了避免产生其他影响干扰调试,我们需要让断点在NSObject *person = [GCPerson alloc];这一行处再生效;

3.1.jpg

3.2.jpg

3.3.jpg

3.4.jpg

探索objc源码

上述三种方法虽然可以找到相关的调用信息进行调试,但由于过于复杂效率太低;既然objc源码已经开源,我们为什么不直接在源码中一探究竟呢?下面就一起探索一番吧!

准备工作

  1. Apple Open Source Source Browser进行objc源码下载;
  2. 常用计算机基础知识:大端小端模式
  3. LLDB调试常用命令内存打印

调试过程

通过追踪我们发现alloc依次调用了以下几个函数

+ (id)alloc {
    return _objc_rootAlloc(self);
}
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
  1. 核心方法
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));
}
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);
}

  1. 当前方法为alloc创建对象的方法,核心代码有几处: size = cls->instanceSize(extraBytes);//计算编译期确定的成员变量class_ro_t;

obj = (id)calloc(1, size);//开辟内存空间创建对象的核心方法

obj->initInstanceIsa(cls, hasCxxDtor);//绑定对象的isa,只有绑定后才能确定对象的isa从而确定class类型

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);//计算编译期确定的成员变量class_ro_t;
    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);//绑定对象的isa,绑定后才能确定对象的isa从而确定class
    } 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);
}
  1. 当前方法为alloc计算分配内存地址的核心方法
inline size_t instanceSize(size_t extraBytes) const {
        if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
            return cache.fastInstanceSize(extraBytes);//有缓存时对齐规则16字节对齐
        }

        size_t size = alignedInstanceSize() + extraBytes;//无缓存时对齐规则8字节对齐
        // CF requires all objects be at least 16 bytes.
        if (size < 16) size = 16;  
        return size;
    }
  1. 绑定对象isa
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);

    if (!nonpointer) {
        newisa.setClass(cls, this);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA
        ASSERT(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

流程总结

至此,整个alloc流程已经分析完了,主要包含计算size、申请内存、和绑定isa等几处核心逻辑; 由于整体流程图如下

alloc流程.png

整个过程比较难理解的是计算size的过程我们这里着重探究下;

instanceSize 探究

instanceSize流程.png

instanceSize 总结
  1. 是否hasFastInstanceSize 决定size计算是内存对齐还是字节对齐;
  2. 内存对齐是对象之间的,内存对齐原则是16字节对齐,计算size时如果有缓存会直接进行内存对齐;
  3. 字节对齐是对象内部属性之间的,字节对齐是8字节对齐,计算size的时候如果没有缓存会计算字节对齐;
  4. 字节对齐和内存对齐都是向上取整;
对齐算法
  1. 计算向上取整的算法(x+mask)&~mask 其中 mask可以是2的任意次方-1,对于字节对齐mask为8-1、内存对齐mask为16-1;
  2. (x+mask)>> 位数 << 位数这种算法和第1种效果一致,具体做法先左移2的次方数个位,再右移2的次方数个位;

其他

  1. 编译优化影响,本次探索过程中偶尔出现了一些“奇怪”情况,其实是编译优化的结果,具体原因看这里——编译优化

结语

以上就是初步探究alloc执行过程的部分内容,更多内容我们后续探究之后再来补充;