三顾 alloc 之旅

1. 写在前面

首先看个 🌰objective-c

对象地址打印

从图中咱们能够看出,三个对象的地址都不相同,由于这是声明的三个不一样的对象,可是值都是指向的同一个地址,而是因为他们都是经过 obj1 这个对象赋值,指向的都是同一个对象,而这个对象是经过 alloc 方法建立的,并且 obj2 调用了 init方法以后仍是相同的值,说明这个对象就是被alloc方法建立而后返回的。算法

NSObject 对于 iOSer 来讲或许已经熟悉得不能再熟悉,开发中大部分的对象都是继承子它,建立的对象都是基于 NSObject 类,同时也提供了不少方法;万物皆有出处,深刻思考后咱们可能会疑惑到底这个对象是怎么生成的,底层作了什么处理,有哪些不同的设计思想和实现方式;所以让咱们一块儿开启三顾 alloc 旅程编程

2. 探索姿式

要探险首先也得作好攻略,准备生存装备和技能;那么探索源码,也须要正确的知(姿)识(势),才不会致使摸不到方向,无功而返,这里介绍三种姿式供你们入门 ^_^ 。api

普通断点设置 + 跟踪调试

普通断点

按住control + in 进入调试查看汇编调用信息;这里没有觉得是模拟器环境,因此没有跟踪到objc_alloc的相关信息;使用真机状况下能看到定位到 libobjc.A.dylib objc_alloc,因此是要探究的方法在 libobjc.A.dylib 这个库中。sass

符号断点设置

符号断点

要探究alloc方法,因此咱们添加一个alloc的符号断点,这时候会显示断到不少类的alloc方法,可是没有关系,由于咱们断点了代码所在对象,因此咱们直接调试就行;断点执行下一步,就能看到 libobjc.A.dylib + [NSObject alloc],说明是调用了libobjc.A.dylib库中的NSObjectalloc 类方法。安全

汇编跟踪

开启汇跟踪设置
Debug => Debug Workflow => Always Show Disassembly 开启汇跟踪设置。

汇编信息
可查看到代码 [MRObject alloc]的汇编代码下一步是调用 objc_alloc; 因此咱们须要研究 objc_alloc方法即为 alloc的底层实现方法!

objc 源码

苹果开源源码地址 => opensource.apple.com/tarballs/多线程

搜索 objc 便可看到 objc 源码地址 当前 objc4-756.2 为最新版本!架构

objc源码版本列表

ps: 在objc源码中咱们看到不少 oldnew 的文件命名标识,这表明objc在迭代过程当中过渡了两个版本,一个新版,一个旧版,如今咱们都是使用的新版本api,因此查看源码逻辑的时候只须要定位 objc2 或者 new标识的新版本~app

源码调试准备条件

经过查看objc源码和汇编的指令查看 objc_alloc 的流程发现很难定位跟踪,不能直观的跟踪到具体的调用方法路径;因此将objc源码集成到咱们的调试工程中,经过打断点调试跟踪才能一目了然,畅通无阻,所向披靡...以上一切都是理想YY,hahaha... ^_^;如何集成objc源码,移步 => iOS_objc4-756.2 最新源码编译调试post

流程调试

alloc => objc_rootAlloc

第一层调用

callAlloc

callAlloc方法

首先看 __OBJC2__的分支,如今是使用的新版本;而后看第一个判断fastpath(!cls->ISA()->hasCustomAWZ()),这里是判断该类是否有重写initWithZone;第二层判断 fastpath(cls->canAllocFast()),是否能够快速建立,这里会返回false,具体实现见下分析。

canAllocFast

调用 bits.canAllocFast方法

bool canAllocFast() {
        assert(!isFuture());
        return bits.canAllocFast();
    }
复制代码

跟踪 canAllocFast 实现

bits.canAllocFast

查看 FAST_ALLOC 宏定义

FAST_ALLOC

class_createInstance

class_createInstance

打印obj的值,印证流程

打印

申请内存之字节对齐

什么是字节对齐

现代计算机中,内存空间按照字节划分,理论上能够从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时常常在特定的内存地址访问,这就须要各类类型数据按照必定的规则在空间上排列,而不是顺序一个接一个地存放,这就是字节对齐。

对齐的缘由和做用

不一样硬件平台对存储空间的处理上存在很大的不一样。某些平台对特定类型的数据只能从特定地址开始存取,而不容许其在内存中任意存放。例如Motorola 68000 处理器不容许16位的字存放在奇地址,不然会触发异常,所以在这种架构下编程必须保证字节对齐。

但最多见的状况是,若是不按照平台要求对数据存放进行对齐,会带来存取效率上的损失。好比32位的Intel处理器经过总线访问(包括读和写)内存数据。每一个总线周期从偶地址开始访问32位内存数据,内存数据以字节为单位存放。若是一个32位的数据没有存放在4字节整除的内存地址处,那么处理器就须要2个总线周期对其进行访问,显然访问效率降低不少。

所以,经过合理的内存对齐能够提升访问效率。为使CPU可以对数据进行快速访问,数据的起始地址应具备“对齐”特性。好比4字节数据的起始地址应位于4字节边界上,即起始地址可以被4整除。

此外,合理利用字节对齐还能够有效地节省存储空间。但要注意,在32位机中使用1字节或2字节对齐,反而会下降变量访问速度。所以须要考虑处理器类型。还应考虑编译器的类型。在VC/C++和GNU GCC中都是默认是4字节对齐。

对齐原则

1.数据类型自身的对齐值:char型数据自身对齐值为1字节,short型数据为2字节,int/float型为4字节,double型为8字节。

2.结构体或类的自身对齐值:其成员中自身对齐值最大的那个值。

3.指定对齐值:#pragma pack (value)时的指定对齐值value。

4.数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中较小者,即有效对齐值=min{自身对齐值,当前指定的pack值}。

instanceSize 获取实例对象内存大小

size_t instanceSize(size_t extraBytes) {
  	// 对齐的大小 + extraBytes(额外加的字节数)
  	size_t size = alignedInstanceSize() + extraBytes;
  	// CF requires all objects be at least 16 bytes.
  	// 这里选择最小16,由于已经至少8字节了,为了防止在读取或者存储的时候多线程越界之类的安全性;16处于一个基本的合理节约的大小,cup读取的时候比较合理
  	if (size < 16) size = 16;
  	return size;
}
复制代码

字节对齐

// alignedInstanceSize
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
  	// 调用 unalignedInstanceSize 获取内存字节大小,而后调用 word_align 方法对齐
  	return word_align(unalignedInstanceSize());
}

#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL // 7
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif

// 对齐算法 (8字节对齐)
// ps: &与运算,同为1则为1,否为0;|或运算,一个为1则为1,否为0;^异或运算,两个不相同则为1,否为0;~非运算,取反
static inline uint32_t word_align(uint32_t x) {
  	// WORD_MASK = 7,先非运算,而后与运算,二进制
    // 例如这里是 x = 8; 由于 MRObject 类结构咱们没有声明其余属性之类的,只有默认包含的 isa 属性结构,占用8字节
  
  	// x+mask = 8 + 7 = 15 => 0000 1111
  
  	// mask => 0000 0111
  	// ~7 非运算,取反
  	// a => 1111 1000
  	// & 与运算,同时为1则为1
  	// x+mask => 0000 1111
  	// return 8 => 0000 1000
  	// 本质就是 mask = 7 操做 => 获得8的倍数,8字节对齐,以8为倍数;至关于 x >> 3 << 3, 右移三位,左移三位 (枚举定义也是如此,计算快速)
    return (x + WORD_MASK) & ~WORD_MASK;
}

// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
  	assert(isRealized());
  	// 对象的实例内存字节大小,是根据类的ivarList、methodList、propertyList、protocolList 等相关的属性相加计算出来的
  	// 这些数据存储在类结构中的 data 段的 ro 结构中,这里经过 ro 获取具体须要建立的字节数大小
  	return data()->ro->instanceSize;
}
复制代码

可能会疑惑为何须要8字节对齐,(⊙o⊙)…???

cpu读取的时候不知道连续的内存开辟的大小,访问的时候可能一个1字节,4字节,8字节...,而后读取的时候须要先判断多长而后在读取,比较麻烦;所以cpu就想着若是每一段都是8字节那么读取的时候就比较方便,空间换取时间

印证字节对齐

怎么印证本身对齐呢,那么咱们经过LLDB调试打印对象内存结构,查看信息来验证。

首先类声明几个属性:

@property (nonatomic, copy) NSString *name;     // 8个字节
@property (nonatomic, assign) int age;          // 4个字节
@property (nonatomic, assign) long height;      // 8个字节
@property (nonatomic, copy) NSString *hobby;    // 8个字节
复制代码

内存打印

😤说好的能所有打出呢,嗯哼?为啥没能访问到hobby呢?这是由于咱们只打印了四段内存地址,而加上isa 一共是默认5个属性,因此hobby在第五段内存,下面是打印日志信息😺。

(lldb) x/5xg obj1
0x1010a1f40: 0x001d800100001521 0x0000000000000012
0x1010a1f50: 0x00000001000010a8 0x00000000000000b9
0x1010a1f60: 0x00000001000010c8
(lldb) po 0x00000001000010c8
money
复制代码

alloc 流程图

流程图

结束语

以上为简要分析alloc流程,有歧义欢迎指出,持续更新进阶之旅,未完待续。。。

相关文章
相关标签/搜索