OC对象原理探究(上)

576.jpg

前言:作为一名已经工作5年iOS开发人员,突然发现自己在底层方面的知识是如此的薄弱,甚至对一个APP的启动细节的认识都不清晰。在经过一系列的学习之后,了解到APP在启动的时候,其实是经历一系列的函数调用和相关支持库加载的,具体的内容下面会逐步展开去讲。

一、探究OC对象原理的主线思维

1.1、程序的启动过程分析
  • 首先,APP在启动时,首先会调用系统的dyld链接器,去调用相关的系统库
  • 然后根据需要去调用一些镜像文件
  • 然后进行加载gcd和runtime环境支持相关的操作,为启动程序做准备。

    在这里插入图片描述

以下内容就是对上图程序启动过程的简单概述:

0               _dyld_start                 //开始加载动态链接器
        ···
m               dyld...                     //动态链接器的一系列操作过程
n               imageLoader                 //加载镜像文件(主要是动态库、共享内存、函数析构)等过程
        ···
w               libSystem_initializer       //相关系统运行库的准备
x               libdispatch_init            //GCD环境准备等操作
y               lib_object_init             //加载runtime库
z               _objc_init                  //执行runtime的相关操作过程
1.2 引出本篇主题——对象alloc的底层本质探究
  • 根据以往对alloc的认知,就是开辟一片内存空间。这里我们举个例子,通过对一个对象进行alloc操作,查看它的内存的变化
比如: LGPerson *p1 = [LGPerson alloc];

接下来就开始探究这一部分的内容:《 alloc对象的指针地址和内存探究 》。

二、alloc对象的指针地址和内存

1、开始探究alloc对内存和指针的影响

说干就干,下面我们就实现下demo,探究下alloc开辟内存的猜想是否正确:

在这里插入图片描述
在这里插入图片描述
<center>demo中内存和指针地址示意图</center>

对比上述内存地址情况,可以看出一些规律:

(1)通过alloc创建的地址 0x6000019fc5e0 存放在堆空间。
(2)p1 p2 p3 的内存地址逐渐抬高,每个之间相隔8字节,是栈里面连续存放的指针。
(3)p1 p2 p3 指针都指向同一片内存空间。

2、结论与疑惑跟进:
通过观察上面的规律,很自然的会想到指针和内存与alloc和init的关系,于是,我们就可能存在以下几个疑惑:
  • (1)对象通过alloc 之后是不是已经有了内存地址、指针指向?
  • (2)调用init之后, p2和p3内存地址是不是不一样了!
  • (3)alloc怎么做到开辟空间的? init 又有何作用?

很显然,仅仅根据以上的结论,并不能证明alloc开辟内存空间的作用,并不能让我们对内存开辟过程有个清晰的认知,那么就让我们带着这些疑问,进入接下来的《对象alloc底层探究阶段》。

三、底层探索的三种方法

在程序员发展之路上,随着工作年限的增长和自身知识的不断积累,我们会不断的去深入底层去探索一些原理性的东西。在很多时候,我们不仅要去弄懂知识和问题本身,更重要的要理解分析思维探索的角度,不断的往深入往底层去走。
接下来,就让我们先去了解下,底层探索常用的三种方法吧!

1、添加符号断点,单步调试程序
在这里插入图片描述

方法使用说明:
(1) 调试代码的位置打断点,单步调试。比如我们要调试alloc,就在alloc使用的这一行,手动断点,进行调试。
(2)可以结合手动添加符号断点,进行调试
比如: libobjc.A.dylib`objc_alloc:的获取

2、通过跟踪汇编代码的方式
在这里插入图片描述

使用方法说明:
(1)位置在Debug —— Debug Workflow —— Always Show Disassembly。
(2)通过断点,然后打开(1)的功能,查看汇编代码,通过函数跟踪执行流程,寻找符号代码。
比如: objc_alloc的获取

3、通过已知函数名称,并手动插入符号断点,确定位置
在这里插入图片描述

使用方法说明:
(1)首先关闭Debug —— Debug Workflow —— Always Show Disassembly = NO;
(2)知道要跟踪的方法,如alloc;然后手动插入符号,比如插入alloc,进行单步调试。
比如:通过插入 'alloc' 符号断点,可以直接查找到:alloc : libobjc.A.dylib`+[NSObject alloc]:

4、更多探究方式
  • 除了以上的三种方式,我们还可能通过反汇编、LLDB工具、堆栈等方式,进行底层原理的探究。

目前,我们的探究方法都已经掌握和了解,下面就开始接下来的实战过程吧!

四、汇编结合源码调试分析 - alloc源码分析实战

通过分析runtime源码运行流程,可以让我们更深刻的理解alloc内部的机制,首先我们要获取到runtime源码,然后对源码进行alloc部分的分析。

1、源码下载参考地址

1 ) 苹果开源源码汇总: https://opensource.apple.com

苹果开源源码

2 ) opensource地址: https://opensource.apple.com/tarballs/

opensource地址

3 ) Cooci大神Github地址:https://github.com/LGCooci/objc4_debug (已编译)

Cooci大神Github地址
2、源码分析
  • 编译objc4-818版本,可以查看alloc的函数执行流程,如图:

    alloc的主线流程

图片文件地址:https://www.processon.com/view/link/60bc8cc65653bb7a322c37a1

分析过程:
(1)可以采用方法三:通过已知函数名称,并手动插入符号断点,确定位置。
(2)根据已知的流程方法,将上述alloc涉及到的函数当做符号断点,进行单步跟踪调试。可以了解alloc源码的执行过程。

五、编译器优化

1、Objective-C程序到源程序过程

Objective-C程序在运行过程中,会经过Clang编译器的优化,生成汇编代码,然后生成可以由机器识别的二进制文件(MachO文件)。

在这里插入图片描述
2、编译器优化策略
  • 从汇编看编译器优化过程,通过下面的函数分析编译器的优化过程:

    在这里插入图片描述

汇编分析:
(1) Xcode内部如果开启了编译器优化,上述代码中的 c = lgSum(a + b) 等价于 c = a + b;
(2) Xcode内部为我们内置了编译器优化的一些策略,总体来说,是根据空间和时间的算法规则去进行相关的处理。
(3)如果采用了编译器优化,则一些简单的函数操作,可能会被内联,我们在进行代码跟踪的时候,一般选择关闭此选项。在进行真机包打包的时候,Xcode会默认选择开启 Fastest Smallest [-Os]。

Xcode编译器优化策略

六、alloc的主线流程

1、alloc的流程分析图
在这里插入图片描述
2、源码流程分析

有了上面跟踪定位源码的方法和经验,我们对alloc的相关过程进行一下符号断点调试。记住目前研究的内容主线—— alloc的流程底层实现和对象开辟空间和内存与alloc的关系,要牢牢把握住这条主线进行探究!!!
由于时间问题,这里我们就直接采用第三种源码方式:直接使用Cooci大神已经编译好的objc库。
好了,我们开始!以下就是对源码的分析:

  • 2.1 从之前的内存和指针分析,LGPerson创建的指针p指向一片由alloc开辟的空间
LGPerson *p = [LGPerson alloc] ;
  • 2.2 在NSObject.mm 中,拿到alloc的方法,然后进行跟踪
+ (id)alloc {
    return _objc_rootAlloc(self);
}


id  _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
  • 2.3 这里到了alloc核心部分,callAlloc的实现,接下来我们对源码中的细节进行分析:
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));
}

分析上述:这里根据传入的cls、checkNil、allocWithZone的值决定程序后续的走向。
1、 关键点:hasCustomAWZ 中定义了一个获取对象缓存的方法,上述意思为,如果对象中如果存在缓存内容,就执行_objc_rootAllocWithZone方法,否则跳出判断执行后面的逻辑。
2、跟踪objc_msgSend方法,发现后续步骤实现文件是通过汇编方式实现,这里暂停跟踪。重点跟踪_objc_rootAllocWithZone方法。

⑤定义了_objc_rootAllocWithZone和_class_createInstanceFromZone方法的实现。

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



#pragma - MARK:_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);
}
在这里插入图片描述

分析malloc_zone_calloc方法,探究alloc的核心实现
1、通过断点调试,我们发现传入的cls正是LGPerson对象,通过方法体的返回值obj,可以确定这个方法的作用就是要创建一个LGPerson对象的实例。
2、再看obj对象的上下文,判断zone的值,如果zone为空,就执行calloc方法开辟内存空间,这里的内存地址目前属于脏内存地址,因为未绑定相应的isa指针;如果存在zone,就执行malloc_zone_calloc。
3、然后判断zone和fast的条件,如果满足存在zone和支持快速查找,接下来进行initInstanceIsa,进行isa和zone地址的绑定操作;如果不满足,就执行 initIsa ,进行内存开辟的操作。
其中,initInstanceIsa 和 initIsa是alloc开辟内存和绑定的核心方法。
最后,返回一个有isa指针和内存指向的对象类型。

七、字节对齐及原理

了解了alloc的内存开辟和指针的绑定流程之后,我们来看下,内存的空间大小是如何确定的,如图所示:

在这里插入图片描述

通过断点调试,发现cls就是我们初始化使用的LGPerson类对象,这里的操作是取出LGPerson对象占用的空间大小。
这里我们就接着看一下,instanceSize的具体实现代码,如下所示:

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;
}
static inline size_t word_align(size_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
1、内存对齐:
  • 上述instanceSize代码块中,描述了objc对对象内存大小的对齐规则 ”CF requires all objects be at least 16 bytes“,对象必须至少以16字节对齐。
2、字节对齐
  • 通过alignedInstanceSize ——> word_align 我们可以获取到字节对齐的规则

在word_align中规定了不同环境条件下的字节对齐规则:WORD_MASK有所区别
计算方式为:(x + WORD_MASK) & ~WORD_MASK
其中x是传入的对象大小,通过函数:unalignedInstanceSize ——>data()->ro()->instanceSize 获取对象的大小。
比如LGPerson对象大小为 isa 的大小:8字节
计算大小:(0x00001000 + 0x00001000)& ~0x00001000 = 0x00001000 = 8

八、对象的内存空间

待补充更新....

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yongtao_vip

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值