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,这是为什么?->编译器优化部分

推荐阅读更多精彩内容

  • 1)了解OC运行底层入口 通常是直接进入main函数,通过插入断点,在工作台运行bt命令,可以得知线程调用状态,如...
    渊鸿shine阅读 203评论 4 0
  • 1. alloc方法的作用   首先我们先来探索最基础的alloc与init方法,创建一个项目工程,创建一个LGP...
    _从今以后_阅读 199评论 3 4
  • 前言 作为一个在iOS领域5年以开发经验的我,只会面向搜索引擎编程,control + C与 contro...
    Alex1989阅读 299评论 0 2
  • lldb命令:bt : 打印当前堆栈信息register read xxx :读寄存器的信息x objc2 <=...
    凯歌948阅读 80评论 0 0
  • 今天主要学习了flex布局,学习笔记如下: 1.指定flex布局: display:flex(任意容器)...
    riku_lu阅读 1,717评论 2 3