001-OC对象原理探究

alloc探索

通过这篇文章可以知道什么:

  • alloc方法是如何开辟内存的,开辟了多少内存?
  • 在alloc过程中内存、指针有什么关系?
  • alloc是如何开辟内存空间的?
  • 如何探索底层源码?
  • 底层源码怎么获取,例如(Objc4/)
  • alloc源码的详细分析
  • alloc加载流程图
  • 不同模式下的编译器优化,在汇编层面上是怎样的?
  • 什么是字节对齐?字节对齐的好处?
从启动流程开始搞起:
启动流程.jpg

加载过程

绿色部分为程序启动部分,由_dyld_start(dyld开始加载)开始到dyld::main再到dyld_initialzeMainExecutableImageLoader::*等等,代表着主程序由_dyld_start开始,到main等为启动做准备,包括加载动态库,共享内存,全局C++函数的析构,还有一系列的初始化,注册回调函数都在此步骤内完成。这里并不是此篇文章的详细说明,只做引入功能。
红色部分为对象加载过程的开始,通过App启动一系列函数之后会进入到libSystem_initializer -> libdispatch_init -> GCD环境的准备 -> _objc_init

OC对象的初始化

1、oc对象是如何开辟的?
2、alloc、init、new是如何操作的?
3、在此过程中内存、指针有什么关系?


p1与p2.jpg

p1与p2的打印结果为什么一样?

LGPerson *p1 = [LGPerson alloc];

得出结论:

  • p1此刻拥有了内存
  • p1拥有了指针的指向
LGPerson *p2 = [p1 init];
LGPerson *p3 = [p1 init];

由于打印对象p2=p3,得出结论:

  • p2、p3所指向的内存地址是一样的
  • init未对指针进行任何操作
&p3.jpg

通过alloc之后开辟了一块内存空间,*p1 *p2 *p3代表3个指针地址,并且同时指向了同一块内存空间,由上图内存地址0x7ffeede340a8 0x7ffeede340a0 0x7ffeede34098得出结论:

  • *p1*p2*p3属于栈上内存地址
  • *p1*p2*p3是连续的地址空间,每个相隔8字节(解释:0x98+0x8=0xa0、0xa0+0x8=0xa8)

图形详解

关键点:连续开辟,指向同一块空间

对象内存开辟与指向.png

alloc是如何做到的?
init是真的什么都不做吗?

如何探索源码:

方式一:

  • 真机模式

第一步:在工程的LGPerson *p1 = [LGPerson alloc];处设置断点

真机+源码探索00.jpg

第二步:将工程运行,停在断点处之后,按住control + Step into进入到汇编代码

真机+源码探索01.jpg

真机+源码探索02.jpg

这里发现了objc_alloc方法,看到了熟悉的代码,变得很兴奋,再次按住control + Step into

真机+源码探索03.jpg

结果是无法再看到有效的信息了,原因是真机模式下Apple做了限制

  • 模拟器模式

第一步:在工程的LGPerson *p1 = [LGPerson alloc];处设置断点,
第二步:将工程运行,停在断点处之后,按住control + Step into进入到汇编代码

模拟器+源码探索00.jpg

模拟器+源码探索01.jpg

第三步:将看到的objc_alloc添加符号断点,具体步骤如下:

模拟器+源码探索02.jpg

第四步:继续按住control + Step into向下走

模拟器+源码探索03.jpg

这里看到了libobjc.A.dylib objc_alloc,看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend,这里豁然开朗,终于找到了objc_alloc底层的源码,来自于哪个动态库,为向下探索提供了更多的线索!

方式二:
通过汇编流程的方式去查看:
第一步,设置工程的模式,选择菜单栏Debug->Debug wrokflow->Always Show Disassembly,将工程运行

汇编+源码探索00.jpg
汇编+源码探索01.jpg

第二步:此时断点断在了LGPerson处,按住control + Step into,去找到objc_alloc

汇编+源码探索02.jpg

第三步:设置符号断点:


模拟器+源码探索02.jpg

第四步:再次按住control + Step into调试objc_alloc

汇编+源码探索03.jpg

这里看到了libobjc.A.dylib objc_alloc,看到了接下来会调用的方法_objc_rootAllocWithZone,objc_msgSend,这里豁然开朗,终于找到了objc_alloc底层的源码,来自于哪个动态库,为向下探索提供了更多的线索!

第三种:
直接通过已知符号断点设定,直接进入,通常配合第二种使用

底层源码在哪里?

Apple开元源码汇总:https://opensource.apple.com/

Apple开源源码汇总.jpg

[Source Browser:https://opensource.apple.com/tarballs/]

Source Browser.jpg

我这里查看的源码是objc4-818.2.tar.gz,来自于LGCocci老师,那个最靓的男人:https://github.com/LGCooci/objc4_debug,有需求的伙伴可以自行获取,素质三连

Source Browser objc4:818.jpg

alloc源码分析:

首先打开源码项目objc4-818.2,搜索alloc,查看一下alloc源码执行的详细流程:

源码alloc.jpg

1、进入_objc_rootAlloc方法

源码_objc_rootAlloc.jpg

2、进入callAlloc方法

源码callAlloc.jpg

3、这里有#if __OBJC2__判断,如何验证走哪个方法进入_objc_rootAllocWithZone

源码_objc_rootAllocWithZone.jpg

4、进入_class_createInstanceFromZone方法

源码_class_createInstanceFromZone.jpg

alloc加载流程图

alloc加载流程图.png

编译器优化

<span id="callalloc">进入到BuildSetting下,找到Optimization level(GCC_OPTIMIZATION_LEVEL),意思是指定生成的代码针对速度和二进制大小进行优化的程度</span>

设置 参数
None[-O0] 编译器不会优化代码。编译器的目标是蒋迪编译成本并使调试产生预期的结果,通常在Debug模式下使用。
Fast[-O,O1] 快速,优化编译器需要编译的时间更久,对大型函数需要更多的内存。编译器会尝试减少代码大小和执行时间,而不执行任何需要大量编译时间的优化。
Faster[-O2] 更快速,编译器执行几乎所有不涉及空间速度权衡的受支持优化。使用此设置,编译器不会执行循环展开或函数内联或寄存器命名,次设置会增加编译时间和生成代码的性能。
Fastest[-O3] 设置指定的所有优化,并打开函数内联和寄存器重命名选项,此设置可能会产更大的二进制文件
Fastest,Smallest[-Os] 最快、最小,此设置启用所有通常不会增加代码大小的更快的优化,它还会做减少代码大小的进一步优化

尝试写一个小例子,设置不同的优化方案,用来验证编译器优化情况:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

//MARK: - 测试函数
int lgSum(int a, int b){
    return a+b;
}

int main(int argc, char * argv[]) {
    int a = 10;
    int b = 20;
    int c = lgSum(a, b);
    NSLog(@"查看编译器优化情况:%d",c);
    return 0;
}
  • None[-O0]


    编译器优化.jpg
执行结果:不优化的情况下所有信息在寄存器中显示完整,我分别打印了a、b、计算钱与计算后的x0寄存器,结果如下:
编译器优化-None.jpg
  • Fastest,Smallest[-Os]


    编译器优化-Fastest,Smallest.jpg

执行结果:优化掉了a、b两个变量,甚至连lgSum函数都被优化掉了,只剩下了一个结果0x1e存在w8寄存器中了。

结论:由于选择了Fastest,Smallest[-Os]优化方案,导致lgSum函数没有了,同理callAlloc函数也是一样的。

alloc做了什么?

源码解析


源码_class_createInstanceFromZone详解.jpg

alloc内存是如何开辟的,开辟了多少内存

开辟内存是由instanceSize这个函数决定的,进入到这个函数,首先判断是否有缓存,如果有执行cache.fastInstanceSize函数直接返回,内存开辟结束,获得该对象内存大小。如果没有缓存,会执行alignedInstanceSize函数,执行word_align函数,此函数的参数是函数unalignedInstanceSize,而这个函数通过data()->ro()->instanceSize获取到对象的实例大小,也就是说,最终开辟内存空间的大小是根据对象的成员变量大小决定的。

默认情况下,不创建任何成员变量,类开辟的内存空间是8字节,因为继承NSObject造成的,NSObject内有成员变量isa,由于isa的类型是结构体指针,所以isa是8字节,所以创建一个新的对象,没有任何成员变量,默认内存大小是8字节

//对象
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

//_class_createInstanceFromZone内开辟内存的大小
size = cls->instanceSize(extraBytes);

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceStart() const {
    ASSERT(isRealized());
    return data()->ro()->instanceStart;
}

// Class's instance start rounded up to a pointer-size boundary.
// This is used for ARC layout bitmaps.
uint32_t alignedInstanceStart() const {
    return word_align(unalignedInstanceStart());
}

// 可能是不对齐的,取决于类的成员变量(ivars)
uint32_t unalignedInstanceSize() const {
    ASSERT(isRealized());
    return data()->ro()->instanceSize;
}

// 类的 ivar 大小向上舍入到指针大小边界。
uint32_t alignedInstanceSize() const {
    return word_align(unalignedInstanceSize());
}

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

字节对齐

字节对齐的优势:以空间换取时间

  • 8字节来自于NSObject对象的isa结构体指针
  • 不满16等于16
  • 如果大于16会根据对象在内存分布中的特性来决定(根据传入的x,取x的整数倍),如果传入8,最后得到的是8的倍数

#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

//字节对齐算法
//define WORD_MASK = 7
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

工程调试

1、验证代码是否执行#if __OBJC2__判断内函数

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_rootAlloccallAlloc_objc_rootAllocWithZone等方法添加符号断点,并且将项目运行起来

排查错误-断点调试00.jpg

按照想象如期的停在了_objc_rootAlloc方法处,通过register read读取寄存器,但是问题是并没有发现LGPerson这个class,原因是LGPerson还没有初始化,解决方法先将断点放过去,让系统的方法执行完,等执行到LGPerson时候再调试

排查错误-断点调试01.jpg

执行的结果是_objc_rootAllocWithZone先会被执行,然后再执行objc_msgSend,这也就证明了#if __OBJC2__判断为true,执行了内部的代码。
但是细心你会发现,当前正在被执行的这个函数是_objc_rootAlloc并不是源码中的callAlloc,这是为什么?

问题:

当前简书页面内跳转失效了上文中两个对应关系如下:

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

推荐阅读更多精彩内容