欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 新车 > 【iOS】OC源码阅读——alloc源码分析

【iOS】OC源码阅读——alloc源码分析

2025/4/29 9:47:06 来源:https://blog.csdn.net/2301_80467753/article/details/147429578  浏览:    关键词:【iOS】OC源码阅读——alloc源码分析

文章目录

  • 前言
  • 基本调试方法
  • alloc
    • alloc函数逐步调试
      • 空类检查
      • 快速路径:默认内存分配
      • 慢速路径:自定义内存分配
      • slowpath和fastpath
    • alloc核心操作
      • cls->instanceSize:计算所需内存大小
        • 内存字节对齐原则
        • 为什么要16字节对齐
        • 内存字节对齐总结
    • 其他
  • 总结

前言

  之前笔者简单学习了Objective-c,这段时间在阅读OC源码,通过阅读obj838来深入理解代码的运行机制、设计思想及实现细节。本篇博客主要分享一下我通过阅读和调试alloc部分的源码所学到的东西和遇到的问题。(笔者这里调试的源码为906,点击下载)
  首先,阅读源码时我们发现,虽然我们编程使用的是OC语言,但是底层源码大部分使用的是C语言或者C++。

基本调试方法

  首先打开我们随机一个项目,在左侧找到断点图标。
在这里插入图片描述
  找到断点图标后,点击下方添加按钮选择“Symbolic Breakpoint”进行断点添加。
在这里插入图片描述
  在“Symbol”里输入想要添加断点的地方,然后点击下面的“Add Action”。
在这里插入图片描述
在对alloc方法进行断点测试的时候,一个项目中可能涉及多个alloc,我们可以在具体需要测试的位置上进行添加断点,运行程序时在走到这部分断点的时候,再打开上面新增的alloc符号断点,因为alloc的调用有很多,如果开启了就不能准确的定位到我们设定的断点处。
请添加图片描述
断点打开状态:
在这里插入图片描述
断点关闭状态:
请添加图片描述
  然后看情况选择点击Continue、Step Over或Step In进行逐步调试。
请添加图片描述
xcode调试按钮从左到右依次是:
断点符:可以用来整体关闭或打开断点。
继续运行(Continue):恢复程序执行,直到遇到下一个断点或程序结束。
步过 (Step Over)​​:逐行执行代码,不进入函数/方法内部。
​​步进进入 (Step In)​​:进入当前行调用的函数/方法内部(逐行调试)。
​​​​步出 (Step Out)​​:执行完当前函数剩余代码,直接跳出到调用位置。

alloc

  在OC语言中,alloc方法是创建对象实例的核心入口,其底层实现涉及内存分配、类结构管理和运行时机制。
请添加图片描述
上述代码依次输出GGObject对象的内容、内存地址、指针地址。
在这里插入图片描述
首先,我们通过运行这部分代码可以发现obj1、obj2、obj3指向的是同一个内存空间,因为其内容、内存地址是相同的,但是他们的指针地址是不同的。下面我们来探索alloc在这中间到底做了什么。

alloc函数逐步调试

我们添加断点,运行代码,打开alloc断点,然后根据不同情况进行逐步调试。
请添加图片描述
第一步,点击Step In进入alloc的源码,我们可以看到:
请添加图片描述
第二步,继续Step In就可以进入_objc_rootAlloc的源码实现:
请添加图片描述
第三步,接着Step In进入callAlloc的源码:
请添加图片描述
这里是用于对象内存分配的核心函数,其作用是根据类的内存分配策略(默认或自定义)选择合适的分配方法。
首先,函数签名是:

static ALWAYS_INLINE id //ALWAYS_INLINE​​:强制内联展开,避免函数调用开销(性能敏感场景)
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
//参数一:目标类(Class 类型) 参数二:是否检查类是否为 nil 参数三:是否调用 allocWithZone: 方法(默认调用 alloc)

空类检查

if (slowpath(checkNil && !cls)) return nil;//slowpath​​:宏定义,表示“慢速路径”(通常用于条件分支预测失败的场景)

主要是为了防止向空类发送消息,避免程序崩溃。(如果 checkNil 为 true 且类指针 cls 为 nil,直接返回 nil)

快速路径:默认内存分配

if (fastpath(!cls->ISA()->hasCustomAWZ())) {return _objc_rootAllocWithZone(cls, nil);
}
//fastpath:宏定义,表示“快速路径”(条件分支预测成功的场景)
//cls->ISA()->hasCustomAWZ()​​:检查类或其元类是否实现了自定义的 alloc/allocWithZone: 方法
//(​​hasCustomAWZ​​:全称 hasCustomAllocationWithZone;若返回 false,表示使用默认内存分配逻辑)
//_objc_rootAllocWithZone​​:默认内存分配函数,直接调用 malloc 或内存池分配内存,​​不调用 alloc 方法​​

慢速路径:自定义内存分配

if (allocWithZone) {return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
} else {return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
//objc_msgSend​​:Objective-C 运行时核心函数,用于向对象发送消息(调用方法)
//​​类型转换​​:将objc_msgSend强制转换为特定签名的函数指针,确保参数和返回值正确
//​​参数​​:allocWithZone::传递 nil 作为 _NSZone 参数

如果类实现了自定义的 alloc/allocWithZone: 方法,则通过消息发送机制调用。若 allocWithZone 为 true,调用 allocWithZone:;
否则,调用 alloc。

slowpath和fastpath

  首先,这里的slowpath和fastpath是Objective-C运行时中用于分支预测优化的宏。这里的fastpath用于提示编译器某个条件大概率成立,而slowpath则相反,提示条件大概率不成立。
  这些宏通常使用__builtin_expect来实现,帮助编译器优化代码布局,提高执行效率。这里我们通过在代码中搜索#define fastpath可以找到这个宏定义的位置文件。
请添加图片描述
  在C/C++中,__builtin_expect是GCC的一个内置函数,用于给编译器提示条件分支的预期走向,这样编译器可以进行优化,比如调整指令顺序,让经常执行的分支更靠近处理器的前端,减少分支预测失败的开销。
  这里,通过 __builtin_expect 提示编译器条件分支的预期走向,优化指令布局,减少分支预测失败的开销,分离高频路径(fastpath)和低频路径(slowpath),提升代码执行效率。
  fastpath的参数x被期望为真(即1),而slowpath的参数x被期望为假(0),将 x 为真的分支代码紧凑排列,减少跳转;将 x 为假的分支代码放在较远位置,减少对主流程的影响。
请添加图片描述

了解完上述代码中涉及的内容后,我们进行Step Over,此时来到fastpath。
请添加图片描述

关于这里的!cls->ISA()->hasCustomAWZ():
cls​​——目标类对象(Class 类型)。
​​cls->ISA()​​——获取类的元类(Meta Class)。
​​hasCustomAWZ()​​——检查类或其元类是否实现了自定义的 alloc 或 allocWithZone: 方法。
​​!​​——逻辑取反,表示“​​没有自定义分配方法​​”。

这里主要用于判断是否使用​​默认内存分配路径​​(快速路径)或​​自定义分配路径​​(慢速路径)。
条件为真时(!hasCustomAWZ())​​:类或其元类未实现自定义的 alloc/allocWithZone:,使用默认分配逻辑(_objc_rootAllocWithZone),即进入这里:
请添加图片描述
条件为假时​​:存在自定义分配方法,需通过消息发送(objc_msgSend)调用,即进入这里:
请添加图片描述
在这个过程中:
cls->ISA() 返回类的元类指针(例如:NSObject 的元类是 NSObject 自身(根元类))。

元类(Meta Class)​​:
在 Objective-C 中,每个类都有一个元类,用于存储类方法(如 alloc、allocWithZone:)。
实例方法的查找路径:实例 -> 类 -> 父类。
类方法的查找路径:类 -> 元类 -> 根元类。

我们Step Over到fastpath的返回语句里,Step In就可以看到_objc_rootAllocWithZone的实现源码:
在这里插入图片描述
_objc_rootAllocWithZone 是 Objective-C 运行时中用于​​创建对象实例的底层函数​​,主要负责分配内存并初始化对象头信息(如 isa 指针)。它是 alloc 和 allocWithZone: 方法的最终实现,直接与内存分配器交互。
参数一(cls)​​——目标类。
参数二(​​flags(第二个参数 0))​​——控制分配行为的标志位。常见值:
OBJECT_CONSTRUCT_CALL_BADALLOC:在内存分配失败时抛出异常。
其他标志可能控制是否跳过初始化(如 FAST_ALLOC)。
参数三(​​OBJECT_CONSTRUCT_CALL_BADALLOC)​​——宏定义,表示分配失败时调用 objc_abort 或抛出异常。

随后,我们继续进入到_class_createInstance源码(这里是alloc实现的核心源码),为了方便做注释,笔者将这部分代码copy了过来:

static ALWAYS_INLINE id
_class_createInstance(Class cls, //目标类,必须已加载(isRealized())size_t extraBytes,//额外内存(通常为0),用于子类扩展int construct_flags = OBJECT_CONSTRUCT_NONE,//构造标志bool cxxConstruct = true,//是否调用C++构造函数size_t *outAllocatedSize = nil)//返回实际分配的内存大小
{ASSERT(cls->isRealized());//(isRealized())确保类已加载到运行时;未实现时,触发断言,防止未初始化类被使用// Read class's info bits all at once for performancebool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();//类是否有C++构造函数bool hasCxxDtor = cls->hasCxxDtor();//类是否有C++析构函数bool fast = cls->canAllocNonpointer();//是否使用非指针isa(优化引用计数存储)size_t size;size = cls->instanceSize(extraBytes);//返回类实例的总内存大小(包括 isa、引用计数、成员变量)if (outAllocatedSize) *outAllocatedSize = size;id obj = objc::malloc_instance(size, cls);//底层内存分配函数,可能调用 malloc 或内存池。if (slowpath(!obj)) {if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {return _objc_callBadAllocHandler(cls);}return nil;}//初始化isa指针if (fast) {obj->initInstanceIsa(cls, hasCxxDtor);//使用非指针isa} else {// Use raw pointer isa on the assumption that they might be// doing something weird with the zone or RR.obj->initIsa(cls);//使用原始指针isa}if (fastpath(!hasCxxCtor)) {return obj;//无C++构造函数}//有C++构造函数construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;return object_cxxConstructFromClass(obj, cls, construct_flags);
}

在obj838中,这部分源码如下:

请添加图片描述

_class_createInstance 函数是对象实例化的核心,主要完成以下任务:
​​内存分配​​:调用 malloc 或类似函数分配内存。
​​对象头初始化​​:设置 isa 指针(指向类的元类)。
​​引用计数初始化​​:初始化 retainCount 等字段。
​​构造函数调用​​:若类实现了 +alloc 或 +allocWithZone: 的自定义逻辑,可能在这里调用。

由此,我们可以大致了解_class_createInstance函数的实现流程(与_class_createInstanceFromZone同理):
请添加图片描述

alloc核心操作

核心操作都位于calloc方法中

cls->instanceSize:计算所需内存大小

计算需要开辟内存的大小的执行流程如下所示:
在这里插入图片描述

  1. 进入instanceSize的源码实现
    请添加图片描述

  2. 进入fastInstanceSize(快速计算内存大小)的源码实现
    请添加图片描述

  3. 跳转至align16(16字节对齐算法)的源码实现
    请添加图片描述

内存字节对齐原则
  1. ​​对齐值​​

数据类型的对齐值是其大小和硬件架构共同决定的。
​​常见对齐值​​:
请添加图片描述

  1. ​​起始地址必须是整数倍​​

数据的内存起始地址必须是对齐值的整数倍。
​​示例​​:
一个 int(4字节)的起始地址应为 0x1000, 0x1004, 0x1008 等。
一个 double(8字节)的起始地址应为 0x2000, 0x2008 等。

  1. 结构体/类的对齐​​

结构体或类的对齐值由其成员的最大对齐值决定。
示例:

struct Example {char c;    // 1字节(对齐值1)int i;     // 4字节(对齐值4)
};

字符 c 后需插入 3 字节填充,使 i 从 4 字节地址开始。
结构体总大小为 1 + 3 (填充) + 4 = 8 字节,对齐值为 4 字节。

为什么要16字节对齐
  • ​​对齐访问​​允许 CPU 通过单次内存操作读取或写入数据:

通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销。

eg:例如:64 位 CPU 一次可读取 8 字节数据。若数据对齐到 8 字节边界,只需一次访问;若未对齐,可能需要两次访问。

  • 16字节对齐有利于性能优化​​,提高​​内存访问效率​​:

16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱。

内存字节对齐总结

在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体。
结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转。
苹果早期是8字节对齐,现在是16字节对齐。

请添加图片描述
算法原理
目标​​:将原始内存大小 x 向上取整到最近的 ​​16字节对齐​​ 的边界。
公式​​:对齐后的大小=(x+15) & ( 15)
​​步骤​​:

  1. ​​加15​​:将原始大小 x 加上 15,确保结果覆盖所有可能的余数。
  2. ​​取反15​​:~15 的二进制为 1111 1111 1111 0000(以32位为例),其作用是保留高位,清零低4位。
  3. ​按位与操作​​:通过 (x + 15) & (~15),将低4位清零,得到16的整数倍。

图例解析
以 x = 8 为例:

  1. 原始大小​​:8 字节。
  2. ​​加15​​:8 + 15 = 23(二进制 0000 0000 0001 0111)。
  3. 取反15​​:~15 = 1111 1111 1111 0000。
  4. 按位与​​:
    请添加图片描述

为什么要加15
确保任何余数(1~15)都能通过进位达到下一个16的倍数。例如:
若 x = 1 → 1 + 15 = 16 → 直接对齐。
若 x = 17 → 17 + 15 = 32 → 对齐到32字节。

​​为什么用按位与
通过清零低4位(~15 的低4位为0),强制将结果对齐到16字节边界。

由上述我们可以得出alloc方法的源码大概实现流程:
请添加图片描述

其他

请添加图片描述
这段代码是C++模板类Zone的一个特化版本,专为布尔模板参数true设计,提供基于C标准库的简单内存管理。其中的alloc() 方法​​是通过 ::calloc 分配一块大小为 sizeof(T) 的内存,并初始化为零值。

calloc 的特点​​:分配内存并清零(适合需要初始化的场景)。其返回 void*,需强制转换为 T*。
​​reinterpret_cast 的作用​​:将 void* 转换为 T*,直接返回原始内存地址。

从这里我们可以知道alloc方法只适用于T类型(平凡类型),如int、struct{int x;},而且比较适用于需要快速分配且零初始化的内存(如高频创建的临时对象)。

注意:

若T是非平凡类型(含构造函数/析构函数),跳过构造函数会导致未定义行为(UB)​​
若T对齐要求高于16字节,calloc可能无法满足(但Darwin系统默认对齐到16 字节)

未定义行为:是指程序的行为不可预测,可能表现为崩溃、数据损坏、安全漏洞等。编译器无需对 UB 情况做任何处理,甚至可能优化掉相关代码。

关于构造函数与析构函数,笔者之前发过:【C++】构造函数与析构函数浅知

总结

  笔者初次进行源码阅读,学习重点总是捉摸不清,读起来比较费时间,也比较难读懂,本篇文章好多地方还没完善,若有错误,还请斧正,后续笔者将会继续对obj838源码进行阅读解析。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词