iOS底层01-对象alloc流程探究

706 阅读5分钟

前言

写代码一般都离不开创建对象,那这个过程到底是怎样的呢,今天我们就来探究一下。

alloc和init初探

我们先来看下对象alloc和init对指针的影响: 截屏2021-06-06 23.09.07.png 这里分别打印了对象p指针p指针地址p,得出如下结果:

截屏2021-06-06 23.09.40.png 可以看出来,三个对象指向的是同一个内存空间,我猜测在alloc中开辟了内存,而init操作却没有,到底是不是这样呢,接下来我们继续探究

工具准备

  1. 下载 objc4-818.2 源码,编译可以参考这个男人的文章 源码编译调试
  2. 编译好的源码 objc4_debug

alloc源码探究

alloc的整体流程图如下

截屏2021-06-07 09.26.37.png

第一步:在main中找到LGPersonalloc,点击进入alloc函数

+ (id)alloc {
    return _objc_rootAlloc(self);
}

第二步:进入_objc_rootAlloc函数

id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

第三步:进入callAlloc函数

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__ // 判断是否有可用的OC编译器
    // 传入的checkNil为nil, !cls为nil, 所以 slowpath为false
    // slowpath是假值判断,不会走到if
    if (slowpath(checkNil && !cls)) return nil;
    
    // 判断累或父类是否有alloc/allocWithZone的默认实现
    // fastpath为真值判断
    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));
}

备注一slowpathfastpath

// x很可能为真, fastpath 可以简称为 真值判断
#define slowpath(x) (__builtin_expect(bool(x), 0))

// x很可能为假,slowpath 可以简称为 假值判断
#define fastpath(x) (__builtin_expect(bool(x), 1)) 

引荐自文章 __builtin_expect说明

__builtin_expect这个指令是gcc引入的,作用是 允许程序员将最有可能执行的分支告诉编译器,这个指令的写法为 __builtin_expect(EXP, N)
意思是:EXP==N 的概率很大

  • __builtin_expect(bool(x), 0) 表示x值为假的可能性比较大
  • __builtin_expect(bool(x), 1) 表示x值为真的可能性比较大
  • slowpathfastpath的作用是对编译器进行优化,减少指令的跳转而使性能降低
  • XCode中也可以通过设置来达到性能优化的目的:Build Settings -> Optimization Level -> Debug -> Fastest, Smallest,具体变化可以通过写一个简单的代码,然后修改该设置,再在汇编代码中查看变化

备注二hasCustomAWZ()

bool hasCustomAWZ() const {
        return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}

// class or superclass has default alloc/allocWithZone: implementation (该类或者父类有没有默认的 alloc/allocWithZone:)
// Note this is is stored in the metaclass.
#define FAST_CACHE_HAS_DEFAULT_AWZ    (1<<14)
  • 表示 该类或者父类有没有默认的 alloc/allocWithZone: 实现

第四步:进入 _objc_rootAllocWithZone 函数

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);
}

第五步:进入 _class_createInstanceFromZone 函数,该步骤为alloc的核心

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);
    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);
    } 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. instanceSize:计算要开辟的内存空间大小
  2. calloc :根据内存空间向系统申请地址指针
  3. initInstanceIsa:关联类与地址

instanceSize:计算内存大小

主要流程如下图: 截屏2021-06-07 14.22.58.png

备注一align1616字节对齐算法

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
  1. 举例截屏2021-06-07 15.52.29.png
  • ~是取反,也就是 1变0,0变1, &(与)同1则为1,反之为0
  • word_alignalign16算法类似,不同点是前者 +7, 然后 ~-7
  1. 为什么要字节对齐:
  • 5s以上的手机都是64位cpu64位的cpu一次性可以处理64bit数据,而1字节=8bit,也就是64位的cpu一次性可以处理8字节数据

  • 苹果采取16字节对齐,是因为OC的对象中,第一位叫isa指针,它是必然存在的,而且它就占了8字节,就算你的对象中没有其他的属性了,也一定有一个isa,那对象就至少要占用8字节

  • 如果以8字节对齐的话,如果有连续的两块内存都是没有属性的对象,它们的内存空间就会完全的挨在一起,容易混乱。以16字节为一块,这就保证了CPU在按块读取的时候效率更高,同时还不容易混乱。

calloc :根据内存空间向系统申请地址

obj = (id)calloc(1, size);

我们可以通过断点来得出这个结论,未执行callocp obj0x00000001002e3dc5, 那么问题来了,没有创建怎么会有地址呢,这是系统分配的一个脏内存地址,执行后打印,得到一个地址:

截屏2021-06-07 18.56.35.png

通常我们打印的对象都是<LGPerson: 0x66666666>这种形式,而这里却只有地址,为什么呢?主要是因为 obj 还没有传入 进行关联

initInstanceIsa:关联类与地址指针

通过calloc我们已经得到了指针地址,接下来我们需要将类与地址进行关联

  • 进入initInstanceIsainitIsa函数,流程如下:

截屏2021-06-09 18.59.35.png

  • 执行完这个流程后,我们通过断点打印 obj

截屏2021-06-07 17.40.07.png

总结

  1. 通过阅读分析源码,我们印证了之前的猜想,alloc主要是创建对象
  2. 从三个步骤来进行创建:计算 -> 申请空间 -> 关联

init探究

类init

// Replaced by CF (throws an NSException)
+ (id)init {
    return (id)self;
}
  • 源码中的init方法是返回self

实例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;
}
  • 该方法也是返回self,是一个构造方法,可以重新该函数对进行一些拓展,也就是工厂模式

new探究

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}
  • 本质上与alloc没有什么区别, 只不过如果项目中一个类用init写了构造方法,而使用new去创建对象,则不会走这个自定义的构造,这样可能会导致一些错误产生
  • 相比init更加灵活,建议使用init,根据场景来使用new