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

八、对象的内存空间

待补充更新....

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