iOS进阶学习

OC对象原理(一)— alloc流程探索

2019-12-24  本文已影响0人  小满豆

研究OC底层原理,就应该从最基本和最熟悉的开始,那就是对象的创建alloc底层实现。本文就我自己探索和学习到的alloc实现进行总结,有问题请指出,大家一起交流探索。

几种源码探索方法(建议真机环境)

以上方法前三种方法可以探索出简单的流程方法顺序,只有下载源码才能看到流程方法的具体实现。探索起来虽说比较枯燥无味,但是只要坚持下去,对于自己技术帮助还是很大的,当你探索出结果也会有很大的成就感。

alloc流程源码探索

alloc打断点

流程

第一步
+ (id)alloc {
    return _objc_rootAlloc(self);
}
第二步
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
第三步
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    //cls为空
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    //看下边官方注释 -->判断是否自定义alloc/allocWithZone,并且不是继承于NSObject/NSProxy
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        
        //canAllocFast通过点进去可以得出在arm64系统下,canAllocFast一直返回false
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            //第三步之后,进入这里
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            //此处打断点,输出一下obj,发现是LGTeacher
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}
第四步
id 
class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}
第五步
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    //cls为空
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    //判断class or superclass 是否有 .cxx_construct 方法实现
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    //判断是否需要优化的isa
    bool fast = cls->canAllocNonpointer();
    //instanceSize计算需要为对象开辟的空间
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (!zone  &&  fast) {
        //第四步之后,进入这里
       //calloc是开辟内存空间
        obj = (id)calloc(1, size);
        if (!obj) return nil;
       //isa初始化
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        //isa初始化
        obj->initIsa(cls);
    }

    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}
size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        // CF requires all objects be at least 16 bytes.
        //对象内存地址至少分配16字节
//一个对象内存都会分配16字节,实际在64位系统下,只使用了8字节;32位系统下,只使用了4字节
        if (size < 16) size = 16;
        return size;
    }
    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
    static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
    }
    uint32_t unalignedInstanceSize() {
        assert(isRealized());
        return data()->ro->instanceSize;
    }

x = 10 ; x + WORD_MASK = 17
0000 0111 //WORD_MASK二进制(4 + 2 + 1)

1111 1000 //~WORD_MASK二进制
0001 0001 //17的二进制:(16 + 1)
0001 0000 //(x + WORD_MASK) & ~WORD_MASK 二进制(16)
所以return 16; 8的倍数

calloc开辟内存空间

这里探索需要用到源码libmalloc源码,打开源码直接调用calloc方法,得出如下结果,接下来就一步一步探索下去,看看如何得出此结果。

calloc开辟空间结果 calloc方法
void *
calloc(size_t num_items, size_t size)
{
    void *retval;
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
        errno = ENOMEM;
    }
    return retval;
} 

进入malloc_zone_calloc方法

void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    //malloc追踪,DBG_FUNC_START  开始
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    //malloc检查
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
        internal_check();
    }
    //调用calloc
    ptr = zone->calloc(zone, num_items, size);
    
    //malloc 记录器  三种类型
    if (malloc_logger) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone, (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }
    //malloc追踪,DBG_FUNC_END  结束
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    return ptr;
}

通过这段代码可以看出系统在这里开启了malloc追踪、malloc监测、malloc记录(看我注释)。真正执行开辟内存的是ptr = zone->calloc(zone, num_items, size);这句代码。zone通过->调用calloc,那么和属性一样我们可以打印zone->calloc,结果如下:

default_zone_calloc 我的探索过程就是全局搜索default_zone_calloc,找到default_zone_calloc的实现,如下:
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    return zone->calloc(zone, num_items, size);
}

又碰到了zone->calloc(头大),打断点正好还能走到这里,说明探索的过程是正确的。没办法接着打印zone->calloc,得到nano_calloc,搜到nano_calloc的实现如下:

static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;
    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        //异常情况
        return NULL;
    }
    if (total_bytes <= NANO_MAX_SIZE) {
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
            //异常情况-->失败会走helper zone
        }
    }
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}

可以看出没有异常情况,会进入_nano_malloc_check_clear方法,打断点进入。

static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
    MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

    void *ptr;
    size_t slot_key;
    //加盐处理,size在此处使用
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    mag_index_t mag_index = nano_mag_index(nanozone);

    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
    if (ptr) {
        //都是一些异常情况处理,代码没有粘贴
    } else {
                //正常情况
        ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
    }

    if (cleared_requested && ptr) {
        memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
    }
    return ptr;
}

到了此处确实不知该如何进行下去(一脸懵逼)。我们既然要探索size是如何开辟到内存的,那我们就跟踪size,进入segregated_size_to_fit方法。(我不会告诉你这都是老师教的)

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    //40 + 16 - 1  >> 4
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    // << 4
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM;
slot_bytes = k << SHIFT_NANO_QUANTUM;

这句代码翻译出来就是

k = (40 + 16 - 1)>>4 //右移4位
slot_bytes = 55 << 4 //再左移4位

我们试着计算一下。

k = 40 + 16 - 1 = 55
0011 0111 //k的二进制
右移4位,空的补0(>>4)
0000 0011
左移4位,空的补0(<<4)
0011 0000 //正好等于48

这里其实运用了16字节内存对齐,和上边的8字节对齐有着异曲同工之妙(把~WORD_MASK换成>>3、<<3也可以实现8字节对齐)。上边的8字节对齐是为了保证每个对象占用8字节内存(64位),16字节对齐则是保证了每个对象开辟了16字节的内存。关于对象占用内存和开辟内存的关系可以看下 Cocoi老师的文章

进行到这里我们大致也就验证出来了,那些没看懂可以以后慢慢探索,现在我们至少了解了对象占用内存和开辟内存苹果底层都是怎么实现的。

objc_alloc的探索

其实使用探索方法1和3的时候,我们都看到了objc_alloc这个方法,那这个方法到底是干什么的呢。
全局搜索objc_alloc,可以找到两个方法。

// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

// Calls [cls allocWithZone:nil].
id 
objc_allocWithZone(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, true/*allocWithZone*/);
}

可以看出objc_alloc也会调用callAlloc,只不过和_objc_rootAlloc调用传入的参数不同。在objc_alloc打断点调试得出objc_alloc_objc_rootAlloc之前执行,进入callAlloc执行[cls alloc],会调起alloc。然后才是上边总结的流程。

那么objc_alloc做了什么事情?全局搜索objc_alloc,可以看到一个调用objc_alloc的地方,方法如下:

static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == SEL_alloc) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == SEL_allocWithZone) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == SEL_retain) {
            msg->imp = (IMP)&objc_retain;
        } else if (msg->sel == SEL_release) {
            msg->imp = (IMP)&objc_release;
        } else if (msg->sel == SEL_autorelease) {
            msg->imp = (IMP)&objc_autorelease;
        } else {
            msg->imp = &objc_msgSend_fixedup;
            //下边代码省略

此处判断sel如果是SEL_alloc(即alloc)就把objc_alloc绑定为他的IMP。
我们接着寻找fixupMessageRef方法,结果如下:

fixupMessageRef
_read_images是系统读取镜像文件,就是dyld进行符号绑定的时候,由此我们可以猜想msg->imp = (IMP)&objc_alloc;就是一个符号绑定的过程。那我们验证一下,编译一下工程,把目录Products下的可执行文件用MachOView(链接:https://pan.baidu.com/s/10k5BFJueUpz_ccBS54DQHQ 密码:eadw)打开,可以看到下图:
符号绑定验证
至此也就验证了我们的猜想。

init&new探索

init

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源码直接返回obj,其实init方法更多的是为了开发者自定义类使用的,在实际中我们自定义类都会重写init,把自定义的内容写在init方法内,这个时候init才真正发挥了作用。创建对象时调用init,是为了防止有自定义内容。

new

new流程方法

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

new方法是调用了callAlloc&init方法,其实和alloc&init是一样的。

总结

alloc流程图 alloc流程图.jpg

努力就有收获,想要在开发的路上走远,就要不断地更新自己的知识库!

上一篇 下一篇

猜你喜欢

热点阅读