对象的底层探究 (上)

探究对象个人认为需要从以下几个方面来探究下

1、创建方式
2、占用空间大小
3、如何创建

要分析以上内容,需先准备好一份源码,这里以objc4-838.1为例,另附一点汇编指令。
  • b bl 跳转指令,方法调用
  • ldr 将内存内容加载入通用寄存器
  • ret 函数的返回
  • ; 注释

日常开发中常见的创建对象方式如下:

        FMUserInfo *user1 = [FMUserInfo alloc];
        FMUserInfo *user4 = [user1 init];
        FMUserInfo *user2 = [[FMUserInfo alloc] init];
        FMUserInfo *user3 = [FMUserInfo new];

其运行结果如下:


image.png

init

在这里我们可以看到user1user2共用的同一份内存地址。通过查看源码可以看到对象的init方法底层调用了_objc_rootInit方法,并把 [FMUserInfo alloc]创建的对象作为参数传入,而_objc_rootInit则直接返回了obj。

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

既然allocinit都能同样的创建一个同地址的对象,那为什么平时开发过程中我们经常用alloc init而不单单使用init呢?

  • 在这里alloc的作用是给对象分配内存,并将地址返回,但是分配内存后这篇内存还没有正确的初始化,所以在很多类方法中我们都需要写如下代码;其作用就是调用init方法为类中属性进行初始化。
- (instancetype)init{
    self = [super init];
    if (self) {
        
    }
    return self;
}

new

在看由new创建的对象的时候,我们看源码发现,其调用了以下方法:

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

在这里我们可以看到new方法最终调用的是 callAlloc方法后最后再调用init方法
我们看下callAlloc方法定义:

static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false){......}

联想到当实例化一个对象的时候,其调用:

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

这里就不难看出,new方法与alloc init方法的区别:

  • 无论是 alloc init 方法还是new方法,都是类初始化的方法,区别在于,一个是显示调用,一个是隐式调用。大家目的都是一样的。只是在底层代码执行时会有点不同.
  • new方法虽然能一步到位,但其只能调用init,而alloc init可以派生出initwithFrame、initwithString等各种便于调用的方法。
  • 这里要补充的是,编译器对于new还做了一些优化。
    当我们打开Always Show Disassembly进行调试时,会发现它会调用一个名为objc_opt_new的方法,在该方法内部最终调用了callAlloc方法。
    image.png
// Calls [cls new]
id
objc_opt_new(Class cls)
{
#if __OBJC2__
    if (fastpath(cls && !cls->ISA()->hasCustomCore())) {
        return [callAlloc(cls, false/*checkNil*/) init];
    }
#endif
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(new));
}

objc_opt_new方法中,值得注意的是fastpath函数

#define fastpath(x) (__builtin_expect(bool(x), 1)) //x很可能为真  
#define slowpath(x) (__builtin_expect(bool(x), 0))  //x很可能为假
  • __builtin_expect这个指令是gcc引入的,作用是允许程序员将最有可能执行的分支告诉编译器。这个指令的写法为:__builtin_expect(EXP, N)。
    意思是:EXP==N的概率很大。
  • 作用:通过这种方式,编译器在编译过程中,会将可能性更大的代码紧跟着起面的代码,从而减少指令跳转带来的性能上的下降。
  • fastpath函数所起到的作用:如果我们把FMUserInfo *user3 = [FMUserInfo new];拿到第一行执行就会发现,当加断电点运行到objc_opt_new方法时,如果FMUserInfo类型的对象之前从没有创建过,那么他就会走objc_msgSend分支,如果之前曾创建过,那么就会走callAlloc分支。(编译器优化,下文 alloc中也有相关调用)

alloc

通过实际运行以及查看源码我们可以看到整个alloc的调用函数过程如下:
alloc-> objc_alloc —> callAlloc —> objc_msgSend —> alloc —> _objc_rootAlloc —> callAlloc —> _objc_rootAllocWithZone —> _class_createInstanceFromZone
对于为什么callAlloc方法执行了两次

这里LLVM对底层方法进行了拦截优化,alloc方法被hook成上面说的objc_alloc方法,这样做的目的就是标记一个receiver,在标记完这个类为receiver之后都会进入普通的消息发送判断(调用fixupMessageRef,然后第二次进入的alloc方法),这样做的目的其实就是间接符号的绑定。当然在fixupMessageRef中似乎不止alloc,还有其他的几个方法,例如releaseretain等。

/***********************************************************************
* fixupMessageRef
* Repairs an old vtable dispatch call site. 
* vtable dispatch itself is not supported.
**********************************************************************/
static void 
fixupMessageRef(message_ref_t *msg)
{    
    msg->sel = sel_registerName((const char *)msg->sel);

    if (msg->imp == &objc_msgSend_fixup) { 
        if (msg->sel == @selector(alloc)) {
            msg->imp = (IMP)&objc_alloc;
        } else if (msg->sel == @selector(allocWithZone:)) {
            msg->imp = (IMP)&objc_allocWithZone;
        } else if (msg->sel == @selector(retain)) {
            msg->imp = (IMP)&objc_retain;
        } 
    } 
......
}

callAlloc

点击 callAlloc 后这样就进入到alloc的核心方法:

// 重磅提示 这里是核心方法
/**
 注意:
 slowpath和fastpath 这两个都是objc源码中定义的宏,其定义如下:
 //x很可能为真, fastpath 可以简称为 真值判断
 #define fastpath(x) (__builtin_expect(bool(x), 1))
 //x很可能为假,slowpath 可以简称为 假值判断
 #define slowpath(x) (__builtin_expect(bool(x), 0))
 
 其中的__builtin_expect指令是由gcc引入的,
 1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
 2、作用:允许程序员将最有可能执行的分支告诉编译器。
 3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。
 4、fastpath定义中__builtin_expect((x),1)表示x的值为真的可能性更大;即 执行if 里面语句的机会更大
 5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行else 里面语句的机会更大
 6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest

 cls->ISA()->hasCustomAWZ()

 其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone。
 */
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
    if (slowpath(checkNil && !cls)) return nil;
    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));
}

点击_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);
}

callAlloc核心操作

点击_class_createInstanceFromZone后,可以看到alloc底层的三步重要操作:

  • cls->instanceSize(extraBytes) 计算需要开辟的内存空间大小
  • obj = (id)calloc(1, size) 申请内存,返回地址指针
  • obj->initInstanceIsa(cls, hasCxxDtor) 将 类 与 isa 关联
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) {
        //将类cls和obj指针进行关联
        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);
}

字节对⻬

这里我们专门看下size = cls->instanceSize(extraBytes);计算对象所需要的空间

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

// Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }
#ifdef __LP64__
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL
#   define WORD_BITS 64
#else
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif
    static inline uint32_t word_align(uint32_t x) {
        return (x + WORD_MASK) & ~WORD_MASK;
    }

     size_t fastInstanceSize(size_t extra) cons
      {
        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);
        }
    }

在instanceSize方法中我们看到有两个分支

  • cache.fastInstanceSize()->align16()这里返回16字节;
  • alignedInstanceSize()->word_align()这里返回8字节,但是返回instanceSize后又做了16字节处理;

那么实际分配的内存大小呢?我们看下obj = (id)calloc(1, size);这个方法calloc函数的实现在libmalloc中,并且其最终会到_nano_malloc_check_clear函数中。其中计算对象开辟空间大小的关键代码为

_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
    
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
}


#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

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
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

这里NANO_REGIME_QUANTA_SIZE=16SHIFT_NANO_QUANTUM=4,将size+15,右移4位,再左移4位,将低4位变为0,就是16字节对齐。以size为2为例,如下图所示:

image.png

由此,在为对象开辟内存空间时,是以16字节对齐的方式。

为什么要字节对⻬?
字节是内存的容量单位。但是,CPU在读取内存的时候,却不是以字节为单位来读取的,⽽是以 “块”为单位读取的,所以⼤家也经常听到⼀块内存,“块”的⼤⼩也就是内存存取的⼒度。如果不 对⻬的话,在我们频繁的存取内存的时候,CPU就需要花费⼤量的精⼒去分辨你要读取多少字节, 这就会造成CPU的效率低下,如果想要CPU能够⾼效读取数据,那就需要找⼀个规范,这个规范就 是字节对⻬。
为什么对象内部的成员变量是以8字节对⻬,系统实际分配的内存以16字节对⻬?
以空间换时间。苹果采取16字节对⻬,是因为OC的对象中,第⼀位叫isa指针,它是必然存在的, ⽽且它就占了8位字节,就算对象中没有其他的属性了,也⼀定有⼀个isa,那对象就⾄少要占⽤8 位字节。如果以8位字节对⻬的话,如果连续的两块内存都是没有属性的对象,那么它们的内存空 间就会完全的挨在⼀起,是容易混乱的。以16字节为⼀块,这就保证了CPU在读取的时候,按照块 读取就可以,效率更⾼,同时还不容易混乱。

总结

由上可总结类 alloc时调用流程图


image.png

扩展:class_instanceSizemalloc_sizesizeof() 区别
1)class_getInstanceSize为获取对象实际占用的空间(要求符合内存对齐原则)
2)malloc_size为系统为OC对象分配的空间(要求≥class_getInstanceSize,且能被16整除,iOS规定一个对象至少分配16Byte空间)
3)sizeof()sizeof()实际上不是函数,()传入的是类型常量(int 等),在编译时确定缓冲区的长度,不能返回动态分配的内存空间大小 e.g. sizeof(int) 为 4

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

推荐阅读更多精彩内容