美文网首页iOS底层探索
iOS底层探索之alloc

iOS底层探索之alloc

作者: loongod | 来源:发表于2020-09-05 15:03 被阅读0次

OC 是面向对象的语言,开发中一切的基础,首先需要一个对象,😆,没有的话,可以 alloc 一个。这篇文章就记录一下探索alloc 内部流程和实现。

objc源码官方地址,本次下载的是objc4-787.1版本。

一、 外层

从工程中 alloc 点击 jump to definition 之后,发现 alloc.h声明,再点击就进不去了,这时可以发现是在objc的代码下的。如下图所见:

xcode-alloc声明.png

可以知道 objc 的代码是开源的,可以在苹果的开发者网站去下载(找不到的话,可以搜索)。下载完成后,我一打开,懵逼了啊,那么多代码,我是完全找不到alloc的实现啊。o(╥﹏╥)o

还回到原来的工程中,找一个类的 alloc 方法,打个 alloc 的符号断点,如下图:

xcode-alloc符号断点.png

在类的的实例化的时候也打个普通断点,这样让代码先运行到alloc的地方再进入符号断点,具体流程:

  1. disabled掉 alloc 符号断点
  2. 运行程序,代码会停到类的 alloc 地方 eg: GLPerson *p = [GLPerson alloc];
  3. enable alloc 符号断点
  4. 运行,停到 alloc 的符号断点
  5. step into instruction,每次点击可以看到在 libobjc 中调用的方法名称
alloc断点-1.png alloc断点-2.png alloc断点-3.png alloc断点-4.png

第一次点击 step into instruction 会看到[NSObject alloc]
第二次点击 step into instruction 会看到_objc_rootAlloc, 下面又有个jmp,对应_objc_rootAllocWithZone
持续点击 step into instruction 可以继续查看...

现在可以根据 _objc_rootAlloc_objc_rootAllocWithZone 去探索一下objc的源码工程,看能不能找到alloc的实现信息

二、源码层

直接搜索 _objc_rootAlloc 看看有什么

_objc_rootAlloc查找流程.png

搜索结果里面有个NSObject.mm文件--- _objc_rootAlloc(Class cls)方法的实现,
里面直接返回 callAlloc 调用。

2.1 _objc_rootAlloc -> callAlloc

然后咱们分析如下:

callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
// 会进入 __OBJC2__  因为现在OC版本就是2.0
#if __OBJC2__    
    // slowpath和fastpath可以理解为语气助词,对理解代码没有关系
    // checkNil传过来的是false(其实不用管这个),只要cls是有值的,就不会返回nil
    if (slowpath(checkNil && !cls)) return nil;
    // 该类是否实现了allocWithZone方法,否,会调用_objc_rootAllocWithZone
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        return _objc_rootAllocWithZone(cls, nil);
    }
#endif

    // No shortcuts available.
    if (allocWithZone) { // allocWithZone为nil,所以不会走这里
        return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    }
    // 如果已经开辟过内存了,就会来到这里
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}

最后分析结果会走进 _objc_rootAllocWithZone方法

2.2 _objc_rootAllocWithZone

先搜索下找到函数实现,结果如下

_objc_rootAllocWithZone搜索.png

发现有2个实现,一个在 objc-class-old.mm 中,一个在 objc-runtime-new.mm 中,看新的吧,因为咱们用的是OC2.0版本,跟着苹果的步伐走。从上图中注释也能看出来

// allocWithZone under __OBJC2__ ignores the zone parameter

2.3 _class_createInstanceFromZone

_class_createInstanceFromZone 方法

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(); // 判断当前class或者superclass 是否有.cxx_construct构造方法的实现
    bool hasCxxDtor = cls->hasCxxDtor(); // 判断当前class或者superclass 是否有.cxx_destruct析构方法的实现
    bool fast = cls->canAllocNonpointer(); // 是对 isa 的类型的区分,如果一个类和它父类的实例不能使用isa_t 类型的 isa 的话,fast 就为 false,但是在 Objective-C 2.0 中,大部分类都是支持的

    size_t size; // 声明内存空间大小
  
    size = cls->instanceSize(extraBytes); // 获取内存大小
    if (outAllocatedSize) *outAllocatedSize = size; // outAllocatedSize没传默认是nil

    id obj;
    if (zone) { // zone是nil
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
        obj = (id)calloc(1, size); // 分配内存空间,calloc( )函数会默认的把申请出来的空间初始化为0或者nil
    }
    if (slowpath(!obj)) { // 判断是否分配成功
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) { // zone是nil,fast是true
        obj->initInstanceIsa(cls, hasCxxDtor); // 初始化实例的isa指针为cls类对象
    } 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);
}

总结:通过上面查找分析,现在可以明白alloc的内部流程,

alloc内部调用流程.png

其实主要是 _class_createInstanceFromZone 的实现流程,可以分3步:

  1. 获取实例的内存空间大小: cls->instanceSize()
  2. 根据内存大小,分配内存空间,让实例指向内存开始地址: calloc
  3. 关联isa,实例的isa指向类: obj->initInstanceIsa(cls, hasCxxDtor)

三、_class_createInstanceFromZone内部

3.1 cls->instanceSize()

获取实例的内存大小,怎么获取呢?先找下内部调用流程

    // May be unaligned depending on class's ivars.
    uint32_t unalignedInstanceSize() const {
        ASSERT(isRealized());
        // 这些方法都在struct objc_class : objc_object 的结构体中,
        // data()和ro()的数据会在map_Images的时候完成,instanceSize会根据类有多少属性,返回相加之后的结果
        return data()->ro()->instanceSize;
    }

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        // word_align是字节对齐方法,下面有分析
        return word_align(unalignedInstanceSize());
    }

    size_t instanceSize(size_t extraBytes) const {
        // 应为在map_Images的时候会调用realizeClassWithoutSwift,这里面会调用setInstanceSize,setInstanceSize里面cache.setFastInstanceSize
        // 所以会走if, 通过cache.fastInstanceSized进行16字节对齐, extraBytes 为0
        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;
    },
    // 注意:setInstanceSize内部设置了cache.setFastInstanceSize(newSize)
    void setInstanceSize(uint32_t newSize) {
        ASSERT(isRealized());
        ASSERT(data()->flags & RW_REALIZING);
        auto ro = data()->ro();
        if (newSize != ro->instanceSize) {
            ASSERT(data()->flags & RW_COPIED_RO);
            *const_cast<uint32_t *>(&ro->instanceSize) = newSize;
        }
        cache.setFastInstanceSize(newSize);
    }

因为在 map_images 的时候会调用 realizeClassWithoutSwift 方法,该方法内部会调用 setInstanceSize,就会设置 cache.setFastInstanceSize(newSize)

    // Set fastInstanceSize if it wasn't set already.
    cls->setInstanceSize(ro->instanceSize);

所以分析出 instanceSize 里面的调用流程,instanceSizecache.fastInstanceSize(extraBytes)align16(size + extra - FAST_CACHE_ALLOC_DELTA16)

可以知道最后分配的内存都是以16字节对齐的,

可以简单验证一下:

@interface GLPerson : NSObject
@end

---
#import <objc/runtime.h> // class_getInstanceSize
#import <malloc/malloc.h> // malloc_size

int main(int argc, char * argv[]) {
        GLPerson *p = [GLPerson alloc];
        NSLog(@"%zd %zd", class_getInstanceSize([GLPerson class]), malloc_size((__bridge const void *)(p)));
}
class_getInstanceSize // 返回某个类对象至少需要多少内存空间
malloc_size // 返回实际分配的内存空间

console: 8 16

通过上面代码,GLPerson 本身不带属性,因为 NSObject 自带有默认 isa,所以一个继承于 NSObject 的类对象至少需要8个字节,但是返回的是16字节。可验证上面的16字节对齐。

现在可以知道,根据类的属性,在计算各个属性所占用的大小后相加可知总内存,总内存最后再16字节对齐,返回的就是这个类对象真正开辟的内存大小。

但是这时,有个疑问,系统是怎么计算,然后相加的,这块可能比较复杂,专门写一篇介绍下。

iOS底层探索之内存对齐和calloc

3.2 calloc

开辟给定大小的内存空间,返回首地址,calloc内部流程怎么走的 --> iOS底层探索之内存对齐和calloc

3.3 obj->initInstanceIsa
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());
    // 会继续调用 initIsa
    initIsa(cls, true, hasCxxDtor);
}

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    ASSERT(!isTaggedPointer()); 
    //  nonpointer 传过来的是true,会走else
    if (!nonpointer) {
        isa = isa_t((uintptr_t)cls);
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());
        // 这里声明并初始化一个newisa
        isa_t newisa(0);
 
#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
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3; // 这个地方把cls和newisa做了关联
#endif
        isa = newisa;
    }
}

关联isa的流程:initInstanceIsainitIsanewisa.shiftcls = (uintptr_t)cls >> 3;

详细分析isa

三、init & new

如上分析 alloc 一样,可在 objc 中找到 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 直接就是返回了 self

new的调用

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

总结new 的调用省略了alloc_objc_rootAlloc,直接调用 callAlloc ,然后initisa的后续分析

相关文章

网友评论

    本文标题:iOS底层探索之alloc

    本文链接:https://www.haomeiwen.com/subject/fevxektx.html