当前位置: 首页 > news >正文

「OC」源码学习——alloc与init的实现

「OC」源码学习——alloc与init的实现

前言

费劲千辛万苦终于项目给写完了,进入下一个阶段,源码的学习

alloc的调用顺序

我们在main函数之中打上断点,先运行

image-20250422120724912

再在alloc之中的各个函数之中打上断点,在关键步骤上打上断点,我们可以,很容易总结出我们alloc源码的编译流程。

先说结论,编译的具体流程如下

1487527-bc778871d88fdd22

流程探究

接下来我们在按照这个顺序进行断点调试,我们现在allocobjc_alloc处打一个断点,运行程序,我们会发现我们的断电先停在的是objc_alloc内,查看网上的资料,这是因为LLVM 在编译阶段会将 alloc 符号替换为 objc_alloc 入口点。

image-20250423114756417

接着继续步入,断点情况如下

image-20250423115300414

进入alloc流程之中的核心callAlloc,对两个return打上断点,我们可以看到,自定义类在第一次进入callAlloc的时候,是会使用消息转发的方法,再去调用真正的alloc函数

image-20250423120018851

至于为何需要走两次callAlloc的原因我们先按下不表,接着程序再次调用callAlloc,这次我们会发现断点打在了_objc_rootAllocWithZone之中

请添加图片描述

进入函数看看
image-20250423194828460

这个函数内容过于复杂,等到下一篇文章再详细阐述,我们只要知道,这个程序是动态创建类实例的核心函数,用于分配内存并初始化实例的 isa 指针。

Objective-C 中自定义类的 alloc 方法调用流程涉及双重 callAlloc 的机制,其根源在于 类初始化延迟运行时多态性设计。以下是该流程的核心原因及分步解析:

1. 首次调用 objc_alloc
  • 入口id objc_alloc(Class cls) 是运行时提供的全局分配函数。
  • 目的
    • 检查类是否已初始化(cls->isRealized())。
    • 若未初始化,触发 class_initialize() 完成类加载。
2. 进入 callAlloc
  • 参数checkNil=trueallocWithZone=false
  • 逻辑
    • 检查类是否存在(slowpath(checkNil && !cls))。
    • 若类支持优化路径(未重写 allocWithZone:),直接调用 _objc_rootAllocWithZone
3. 动态派发 objc_msgSend
  • 触发条件:若类重写了 allocWithZone:,需通过消息发送调用自定义逻辑。
  • 结果:进入类的 +alloc 方法,最终再次调用 callAlloc
4. 第二次 callAlloc
  • 参数checkNil=falseallocWithZone=true
  • 逻辑
    • 直接调用 _objc_rootAllocWithZone,跳过安全检查。
    • 最终通过 _class_createInstanceFromZone 完成内存分配和 isa 绑定。

两次进入的区别

我们现在在来看看两次进入callAlloc究竟有什么区别

第一次调用 callAlloc 时,参数 checkNil=true,在程序之中需要验证类是否存在。

image-20250424121010777

第二次调用 callAlloc 时,参数 checkNil=false,跳过安全检查。

image-20250424121031770

其实差别就在于,后面传进的两个布尔值,其实就是是否需要进行安全检查。在第一次进入callAlloc会做一次检查,确保对应的类不为空。

为何自定义类需要进入两次callAlloc

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{if (slowpath(checkNil && !cls)) return nil;if (fastpath(!cls->ISA()->hasCustomAWZ())) {return _objc_rootAllocWithZone(cls, nil);}// 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));
}

系统类(如 NSObject):

因为NSObject在编译时已完成isa指针的初始化。所以不需要经过两次callAlloc

只需要直接进入objc_alloc->callAlloc->_objc_rootAllocWithZone直接申请空间即可

自定义类:

  • 第一次 callAlloc 通过 objc_alloc 进入,目的是 检查类的初始化状态,类初次创建是没有默认的alloc/allocWithZone实现的所以继续向下执行进入到msgSend消息发送流程,为这个类进行初始化。

  • 第二次 callAlloc,此时类已初始化完成,进入实际内存分配逻辑。

  • callAlloc 的分支逻辑

    if (fastpath(!cls->ISA()->hasCustomAWZ())) {return _objc_rootAllocWithZone(cls, nil); // 直接分配内存
    } else {return objc_msgSend(cls, @selector(alloc)); // 通过消息发送调用自定义逻辑
    }
    
    • hasCustomAWZ():检测类是否重写了 allocWithZone: 方法。
    • 若未重写(默认情况),直接调用 _objc_rootAllocWithZone 分配内存。
    • 若重写,则通过 objc_msgSend 触发动态派发,确保调用正确的 alloc 实现。

    那么源码是如何查看缓存的呢,我们进入hasCustomAWZ()看一下

    #   define FAST_CACHE_HAS_DEFAULT_AWZ    (1<<14)
    bool hasCustomAWZ() const {return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);}bool getBit(uint16_t flags) const {return _flags & flags;}
    

    FAST_CACHE_HAS_DEFAULT_AWZ宏定义表示 是否实现alloc/allocWithZone位域标识位

    为了证明是类没有初始化的问题才导致callAlloc函数在类第一次被调用的时候被进入两次,我们可以通过在申请一次空间来尝试一下,断点和之前的一样。

    GGObject *obj = [GGObject alloc];
    GGObject *obj2 = [GGObject alloc];
    

    image-20250424141439512

    我们发现obj2的alloc调用顺序就和NSObject一样了。objc_alloc->callAlloc->_objc_rootAllocWithZone

    但紧接着问题又来了,重复进入callAlloc的原因我们找到了,那为什么要设置这个流程,这个所谓的类初始化实在哪里开始的呢?

    查看hasCustomAWZ()的调用栈可以看到,有个名字为 _objc_msgSend_uncached的方法,其实也很好理解,就是消息发送给了一个初始化信息的类之中,其实就是在消息转发这一步顺带把第一次调用的类进行初始化,然后在第二次进入hasCustomAWZ()就能进入

    _objc_rootAllocWithZone

image-20250424132737830

双重 callAlloc 流程是 Objective-C 运行时与编译器协作的产物,其核心目的是 确保类初始化完成兼容自定义内存分配逻辑。这一机制平衡了性能、灵活性与安全性,是 Objective-C 动态特性的典型体现。

init

其实点进去init, 很简单,就是把内容返回一次,如此设计的原因是为了让整个程序符合工厂模式的设计理念,我们可以通过简单的重新init的方法去实现较为复杂的其他操作。

image-20250424164807997

总结

其实我是想讲整一个alloc的流程梳理完整再将内容发出,无奈一个完完整整的流程涉及的内容实在过多,有想要了解的可以看 OC对象底层内存开辟和实现(中)了解初始化类的完整流程

参考资料

iOS-底层原理 02:alloc & init & new 源码分析

iOS-底层原理 04:NSObject的alloc 源码分析

OC对象底层内存开辟和实现(中)

http://www.xdnf.cn/news/186301.html

相关文章:

  • 【博客系统】博客系统第二弹:实现博客列表接口(在 Service 层重新封装 Mapper 层返回结果,避免实体类所有字段都向前端返回)、SimpleDateFormat 类的使用方法
  • 强化学习中关键超参数的详细说明
  • 如何把握邮件发送的黄金时间?
  • TypeScript 入门到进阶全讲解(超全版)
  • DAY8-GDB调试及打桩
  • BOPF Business Object Development
  • AiCube 试用 - ADC 水位监测系统
  • NameSilo转入转出域名
  • 大模型笔记8 文本数据清洗
  • C语言数据结构—数组(cpu内存与指针)
  • CSS选择器
  • 6.图的OJ题(1-10,未完)
  • shell命令二
  • “八股训练营”学习总结
  • Linux学习——FTP(功能实现)
  • windows anaconda 配置 ipynb 环境
  • 《电商业务分析终极框架:从数据到决策的标准化路径》
  • OpenLayers WebGL与3D渲染 (进阶一)
  • 五分钟讲清数据需求怎么梳理!
  • 数据库关系模型的总结
  • 软件功能设计视角下的能源管理系统功能清单构建与实践
  • Redis高可用架构全解析:主从复制、哨兵模式与集群实战指南
  • 2025系统架构师---黑板架构风格
  • 风控策略引擎架构设计全解析:构建智能实时决策系统
  • 探索大语言模型(LLM):自监督学习——从数据内在规律中解锁AI的“自学”密码
  • MLLM之Bench:LEGO-Puzzles的简介、安装和使用方法、案例应用之详细攻略
  • OpenSSH 漏洞 CVE-2025-26465 和 CVE-2025-26466 可引发中间人攻击和 DoS 攻击
  • 毫米波振荡器设计知识笔记
  • BeautifulSoup的详细使用说明
  • 迈锐思C1pro插件安装包【附百度网盘链接】