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

研究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];
}
  • hasCustomAWZ()是用来判断是否有alloc/allocWithZoneimp,以及是否有自定义allocWithZone
  • canAllocFast()通过点进去可以得出在arm64系统下,一直返回false。
  • hasCxxDtor()是否有c++析构函数。
    if (slowpath(!obj)) return callBadAllocHandler(cls);打断点,输出obj对象,说明追踪正确,如下图:
    obj对象
第四步
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;
}
  • canAllocNonpointer()判断是否需要优化的isa。
  • instanceSize(extraBytes)计算需要开辟的空间大小,extraBytes等于0。
  • calloc->malloc_zone_calloc开辟内存空间。
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;
    }
  • if (size < 16) size = 16;对象内存地址至少分配16字节。一个对象内存都会分配16字节,实际在64位系统下,只使用了8字节;32位系统下,只使用了4字节。
  • 此处word_align()方法用到了一个内存的8字节对齐计算,保证返回的size大小是8的倍数。WORD_MASK在arm64系统下等于7。假如传入word_align()的x = 10,计算过程:

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容