OC底层原理三:探索alloc (你好,alloc大佬 )

OC底层原理 学习大纲

OC对象的始源 - alloc

前言

我们都知道,创建OC对象的2种方式: [[ClassName alloc]init][ClassName new]

当被问起他们的作用时,可能你的回答是: alloc + init给对象开辟内存空间并完成对象初始化,new是类方法,实现的功能一样。

这个描述没有错,但请详细描述下他们的作用。

可能你一脸懵逼。 心里已经开骂: 你丫有病吧,我已经说完了呀!!

今天,我将帮你扯开alloc、init、new的这块遮羞布。 让你深入了解alloc、init、new。

前期准备

课前问题

打开objc4-781包,在HTTest文件夹中,创建HTPerson测试文件(继承自NSObject),切换项目target为HTTest,在main.m文件中加入测试代码。

image.png

%@ 打印对象 %p 打印地址 &p 指针地址

问题:

  1. p1、p2p3对象和地址打印都一致, 为何&p打印不一致
  2. p4的地址为什么和p1、p2p3都不一样。

学完本章,你就彻底懂了

本节内容:

  1. alloc流程
  2. alloc核心函数
  3. alloc的地位(init、new)

1. alloc流程

打开源码工程,跟随alloc函数,一步步深入。流程如下:

alloc流程.png

当出现分支时,我们可以添加断点,辅助查看主流程是进入哪个分支。

不知道打断点,可参考OC底层原理一:定位源码(欢迎来到底层世界)内的三种断点技巧。

callAlloc处出现了分支。断点后发现程序走向_objc_rootAllocWithZone分支,继而进入_class_createInstanceFromZone函数。

image.png

关于fastpathslowpath的作用,请移步OC底层原理四: 编译器优化

allocwithZone: 和alloc一样,为对象分配足够的内存, cocoa 会遍历该对象所有的成员变量,通过成员变量的类型来计算所需占用的内存。从iOS8以后,Zone外层API已被废弃,仅底层源码做兼容处理。

_class_createInstanceFromZone是最底层的包工头。?? 终于找到真正干活的人了。它实现三大核心方法,然后将成品obj返回给外层。

_class_createInstanceFromZone核心方法.png

2.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;

    // 1. 计算开辟的内存大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    id obj;
    if (zone) {
        obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
    } else {
      // 2. 申请内存空间
        obj = (id)calloc(1, size);
    }
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    if (!zone && fast) {
        // 初始化isa并与objc关联
        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);
}
  • hasCxxCtor、hasCxxDtor、fast等 后续剖析isa会详细讲解
1. 计算内存大小: instanceSize
    size_t instanceSize(size_t extraBytes) const {
        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;
    }

if (size < 16) size = 16 :做了小于16字节的判断。
跟断点,发现主流程进入cache.fastInstanceSize(extraBytes)

 size_t fastInstanceSize(size_t extra) const
    {
        ASSERT(hasFastInstanceSize(extra));

        if (__builtin_constant_p(extra) && extra == 0) {
            return _flags & FAST_CACHE_ALLOC_MASK16;
        } else {
            size_t size = _flags & FAST_CACHE_ALLOC_MASK;
            // remove the FAST_CACHE_ALLOC_DELTA16 that was added
            // by setFastInstanceSize
            return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
        }
    }

继续跟断点,进入align16

static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

align16的实现,就是使用位运算算法完成16字节对齐

算法(x + size_t(15)) & ~size_t(15)
x=8为例,计算过程如下:

  • 8 + size(15) = 23 二进制-> 0000 0000 0001 0111
  • size_t(15) 二进制-> 0000 0000 0000 1111
  • 取反~size_t(15) 二进制-> 1111 1111 1111 0000
  • 求交 & :
    0000 0000 0001 0111 & 1111 1111 1111 0000 = 0000 0000 0001 0000
  • 结果表示为十进制: 16

目的:

  • 提高性能,加快存储速度
    通常内存是由一个个字节组成,cpu在存储数据时,是以固定字节块为单位进行存取的。这是一个空间换时间的优化方式,这样不用考虑字节未对齐的数据,极大节省了计算资源,提升了存取速度。
  • 更安全
    在一个对象中,isa占8字节,对象属性也占8字节。苹果公司现在采用16字节对齐,当对象无属性是,会预留8字节,即16字节对齐。 如果不预留,CPU存取时以16字节为单位长度去访问,会访问到相邻对象,造成访问混乱。

执行完后,回到上层函数size = cls->instanceSize(extraBytes)可打印size值。
此时已完成内存大小计算。

image.png
2. 分配内存 calloc

根据size 大小进行内存分配

image.png
  • 执行前打印obj只有cls类名,执行后打印,已成功申请内存首地址。

  • 但并不是我们想象中的格式<HTPerson: 0x10069eff0>,这是因为这一步只是单纯的完成内存申请,返回首地址。

  • 类和地址的绑定是下一步initInstanceIsa的工作

3. initInstanceIsa

初始化isa,完成与类的绑定

obj->initInstanceIsa(cls, hasCxxDtor);
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

具体的isa结构和绑定关系,后续会作为单独章节进行讲解

isainit之后加断点,打印obj,此时发现地址与类完成绑定

image.png

总结: 至此,我们已对alloc有了完整的认知

3. alloc的地位(init、new)

可能你有疑问,alloc把活都干完了,init和new干啥?

init

进入init

+ (id)init {
    return (id)self;
}

- (id)init {
    return _objc_rootInit(self);
}

进入_objc_rootInit

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的类方法和对象方法返回的都是id对象本身。
  • 不同的是类方法返回了一个id类型的self,这是为了可以给开发者提供自定义构造方法的入口,通过id强转类型实现工厂设计,返回我们定义的类型。

new

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

实际上就是完成调用了callAlloc,走的时alloc流程。

唯一区别:

  • alloc + init允许对init进行重写,可自定制init完成工厂设计
  • new是完整封装,无法在初始化这一步加入自定制需求

答案

image.png
  • 问题1: p1、p2、p3对象和地址打印都一致, 为何&p打印不一致

alloc是真正开辟内存和绑定对象的,p1、p2、p3共用1个alloc,所以他们都是指向同一目的地址。但是他们本身也是对象,在init时传入他们自身id,&p打印的是他们自身的地址。

通俗的说:
我有一个房子出售,A、B、C三个都是我员工,他们都领着客户来看我这套房子。但是他们三个虽然都是我公司员工,但工号(id)不一样。
如果客户问他们房子在哪(等同于%@%p打印),他们都会告诉我房子的具体位置(三人说的一定相同)。
如果顾客问他们是谁(等同于打印&p),他们就会各自回答A、B、C。

  • 问题2. p4的地址为什么和p1、p2、p3都不一样。

因为p1、p2、p3是同一个alloc打印的,而p4是new出来的,new会单独调用alloc。 所以他们打印肯定不一样

下一节: OC底层原理五: NSObject的alloc分析

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