【iOS】alloc & init & new 源码学习

【iOS】alloc & init & new 源码学习

前言

我这里的objc4源码参考的是objc4-940.4版本的代码。

alloc

alloc源码探索

用三个变量作为示例看一下:

objc 复制代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        LYDPerson *p1 = [LYDPerson alloc];
        LYDPerson *p2 = [p1 init];
        LYDPerson *p3 = [p1 init];
    }
    return 0;
}

我们逐步跳转alloc函数的每一个步骤:

objc 复制代码
+ (id)alloc {
    return _objc_rootAlloc(self);
}
objc 复制代码
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
objc 复制代码
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;
    // 判断一个类是否有自定义的 +allocWithZone: 实现
    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));
}

在callAlloc方法中,我们接下来不确定走哪步了,可以通过断点测试判断。

这里先认识几个简单的汇编指令:

  • bl:调用函数
  • mov:赋值
  • tbz / tbnz:if判断跳转
  • ldr / str:从内存取值

可以看出这里是执行到_objc_rootAllocWithZone了:

这里补充说明两个内容:

  • slowpath和fastpath:

这两个都是objc源码中的宏,定义如下:

cpp 复制代码
// x值很可能为真,fastpath简称为真值判断
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x值很可能为假,slowpath简称为假值判断
#define slowpath(x) (__builtin_expect(bool(x), 0))

__builtin_expect(bool(EXP), N):表示EXP==N的概率很大。

其中__builtin_expect指令是由GCC引入的,其目的是使得编译器可以对代码进行优化,以减少指令跳转带来的性能下降,也就是性能优化;其作用是允许程序员将最有可能执行的分支告诉编译器。

fastpath定义表示x的值为真的可能性更大,即执行if里面语句可能性更大。

slowpath定义表示x的值为假的可能性更大,即执行else里面语句可能性更大。

  • cls->ISA()->hasCustomAWZ():

这里是用来判断一个类是否有自定义的 +allocWithZone: 实现。通过断点调试,发现没有自定义的实现,因此执行if语句里面的代码,这样下一步走到了_objc_rootAllocWithZone

objc 复制代码
id
_objc_rootAllocWithZone(Class cls, objc_zone_t)
{
    // allocWithZone under __OBJC2__ ignores the zone parameter
    // zone参数不再使用,类创建实例内存空间
    // 确保当前类已经完成realize
    cls->realizeIfNeeded();
    // 真正进入创建实例对象
    return _class_createInstance_realized(cls, 0, OBJECT_CONSTRUCT_CALL_BADALLOC);
}

接下来到了alloc源码的核心操作部分:

objc 复制代码
static ALWAYS_INLINE id
_class_createInstance_realized(Class cls, size_t extraBytes,
                               int construct_flags = OBJECT_CONSTRUCT_NONE,
                               bool cxxConstruct = true,
                               size_t *outAllocatedSize = nil)
{
    ASSERT(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    size_t size;

    // 计算对象需要的内存大小
    size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;

    // 申请内存空间
    id obj = objc::malloc_instance(size, cls);
    if (slowpath(!obj)) {
        if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
            return _objc_callBadAllocHandler(cls);
        }
        return nil;
    }

    // 把对象和类关联起来
    if (fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        // Use raw pointer isa on the assumption that they might be
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (fastpath(!hasCxxCtor)) {
        return obj;
    }

    construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
    return object_cxxConstructFromClass(obj, cls, construct_flags);
}

这里objc4的每个版本可能有些出入,但是本质其实是一致的。

alloc核心操作

从源码我们可以看出,alloc本质做了三件事:

  • 计算需要的内存大小
  • 申请内存
  • 绑定isa,让它变成对象
  1. instanceSize:计算所需内存大小
objc 复制代码
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.
  // size最小取16
  if (size < 16) size = 16;
  return size;
}

此时我们通过断点调试,发现下一步执行到cache.fastInstanceSize方法:

objc 复制代码
size_t fastInstanceSize(size_t extra) const
{
  ASSERT(hasFastInstanceSize(extra));

  if (__builtin_constant_p(extra) && extra == 0) {
      return _flags & FAST_CACHE_ALLOC_MASK16;
  } else {
      size_t size = _flags & FAST_CACHE_ALLOC_MASK;
      // remove the FAST_CACHE_ALLOC_DELTA16 that was added
      // by setFastInstanceSize
      return align16(size + extra - FAST_CACHE_ALLOC_DELTA16);
  }
}
objc 复制代码
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}

这个方法就是16字节对齐算法。

这里补充一下内存字节对齐原则:

首先了解一下内存字节对齐原则有以下三点:

  • 数据成员对齐原则:struct或者union的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(比如结构体等)的整数倍开始。(eg:int在32位机中是4字节,则要从4的整数倍地址开始存储)
  • 数据成员位结构体:如果一个结构体里有某些结构体成员,则该结构体成员要从其内部最大元素大小的整数倍地址开始存储。(eg:struct a里面存有struct b,b里面有char、int 、double等元素,则b应从8的整数倍开始存储)
  • 结构体的整数对齐规则:结构体的总大小(即sizeof的结果)必须是其内部最大成员的整数倍,不足的要对齐。

其次需要16字节对齐的原因有以下几点:

  • CPU按块读取数据,内存对齐可以减少访问次数,提高性能。
  • 在iOS64位系统中,malloc的最小分配粒度通常是16字节,因此即使对象本身只需要8字节(isa),系统也会按16字节进行分配。这有助于提高内存分配效率,减少内存碎片。

总结一下:

  • 字节对齐的本质是针对内存中的数据结构(如struct)进行的,而对象在底层是一个struct objc_object,因此同样遵循结构体的内存对齐规则。
  • 结构体在内存中是连续存储的,各成员会按照自身大小进行对齐,同时整体大小也会按照最大对齐单位进行补齐。
  • 16字节对齐是结构体对齐和内存分配策略共同作用的结果。

这里再看一下内存对齐常用的方法:

内存对齐通常通过位运算实现,例如:(size + 15) & ~15 表示将 size 向上对齐到 16 的倍数。其中 +15 用于保证进位,而 ~15(取反)用于将低 4 位清零,从而实现对齐。

  1. calloc:申请内存,返回地址指针

通过instanceSize计算内存大小size,向内存中申请大小为size的内存并赋值给obj。因此obj是指向内存地址的指针。

我们通过在calloc这里打断点发现:

此时我们看到的是一个十六进制地址,而不是类似<ClassName: 地址> 的对象描述,这说明当前拿到的只是已分配的内存地址,即开辟了内存,还没有完成isa绑定,因此还不是一个完整的OC对象。

  1. initInstanceIsa:类与isa关联

经过calloc,内存已经申请好,类也已经传入进来,接下来就是需要类与指针(即isa指针)进行关联。

objc 复制代码
inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

其关联流程图如下所示:

总结一下:开辟内存的核心步骤就是3步:计算->申请->关联

alloc的完整流程图如下:

NSObject与自定义类的alloc

LYDPerson的alloc

OC中自定义类的alloc方法调用流程涉及双重callAlloc的机制,其根源在于类初始化延迟和运行时多态设计。具体学习一下:

两次进入的流程

通过断点调试,我们发现:

  1. 第一次进入callAlloc:
  • 参数:checkNil == true, allocWithZone == false
  • 逻辑:若重写了allocWithZone:,动态派发,再次调用alloc,然后再次进入callAlloc。
objc 复制代码
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
  1. 第二次进入callAlloc:
  • 参数:checkNil == false, allocWithZone == true
  • 逻辑:跳过安全检查,直接调用 _objc_rootAllocWithZone,然后就是alloc后续关键逻辑部分,即完成内存分配和isa绑定。
两次进入的区别
objc 复制代码
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
objc 复制代码
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
    return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}

从整个逻辑可以明显地看出,两次进入callAlloc的区别就在于传入的两个参数,即判断是否需要进行安全检查

两次进入的原因

首先将callAlloc认识清楚:

objc 复制代码
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;
    // 判断一个类是否有自定义的 +allocWithZone: 实现
    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));
}

这个函数主要职责是决定这次alloc到底是直接分配,还是先走消息发送

三个参数分别是:

p1:表示当前要分配哪个类的实例

p2:表示要不要先检查cls是否为空

objc 复制代码
if (slowpath(checkNil && !cls)) return nil;

p3:表示这次如果要走消息发送,是alloc还是allocWithZone:

objc 复制代码
if (allocWithZone) {
  return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}

这个函数的核心判断在于:当前这个类能否直接走默认的、快捷的分配路径。

objc 复制代码
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
  return _objc_rootAllocWithZone(cls, nil);
}

hasCustomAWZ()的含义是这个类有没有自定义alloc/allocWithZone:相关逻辑:

  • 如果没有自定义,就直接走快捷路径:

    objc 复制代码
    return _objc_rootAllocWithZone(cls, nil);
  • 如果有自定义,就不能直接分配,而是通过消息发送:

    objc 复制代码
    return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
    objc 复制代码
    return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));

再看一下hasCustomAWZ()的源码逻辑:

objc 复制代码
#if FAST_CACHE_HAS_DEFAULT_AWZ
bool hasCustomAWZ() const {
  return !cache.getBit(FAST_CACHE_HAS_DEFAULT_AWZ);
}

#   define FAST_CACHE_HAS_DEFAULT_CORE   (1<<15)
bool getBit(uint16_t flags) const {
  return _flags & flags;
}

简单来说,这里其实就是从cache的标志位里读取这个类里是否有默认allocWithZone:

综上所述,自定义类两次进入callAlloc的原因就在于:

  • 第一次callAlloc通过objc_alloc进入,目的是检查类的初始化状态,类初次创建是没有默认的alloc/allocWithZone实现,所以继续向下进入到消息发送流程,为这个类进行初始化。
  • 第二次callAlloc时,类已经初始化完成,进入allocWithZone:,继续完成分配内存等操作。

NSObject的alloc

通过两个实例分析一下源码:

objc 复制代码
NSObject *objc1 = [NSObject alloc];
LYDPerson *objc2 = [LYDPerson alloc];

通过断点调试我们发现NSObject并没有走alloc源码,而是objc_alloc

找到objc_alloc源码,打断点重新调试发现此时的cls就是NSObject。

通过LLVM源码调试会发现,其原因是alloc作为根类对象创建入口,可能经过编译器和运行时的特殊化处理,而不完全表现为普通的消息发送路径。通俗地说,NSObject作为根类,在编译器和运行时层面通常会有更直接的优化分配路径,因此调试时往往会表现为更快进入_objc_rootAllocWithZone

init

我们同样逐步跳转init函数的每个步骤:

objc 复制代码
- (id)init {
    return _objc_rootInit(self);
}
objc 复制代码
id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

init很简单,没有做什么操作,只是返回了当前对象。

new

初始化出了使用init,还可以使用new。

通过源码可以得知,new函数中直接调用了callAlloc函数,后续和alloc都一样。因此可以看出,new其实就等价于alloc init

objc 复制代码
+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

但是一般开发不常使用new,主要是因为我们经常会重写init方法来做一些自定义的操作。用new初始化可能无法走到initWithXXX这一步。

总结

下篇我会对NSObject的alloc和自定义类的alloc详细区别学习。

相关推荐
2501_915909062 小时前
苹果App Store上架全流程指南从注册到上线
android·ios·小程序·https·uni-app·iphone·webview
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(九):线程池实现(附代码示例)
linux·运维·服务器·c++·学习·架构
许彰午2 小时前
debug驱动学习——三次debug改变我的技术认知
学习
古方路杰出青年2 小时前
学习笔记1:Python FastAPI极简后端API示例解析
笔记·后端·python·学习·fastapi
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(八):<策略模式>日志系统的封装实现
linux·运维·服务器·c++·学习·策略模式
HalvmånEver2 小时前
MySQL数据库操作
linux·数据库·学习·mysql
小夏子_riotous2 小时前
Docker学习路径——4、制作/更改镜像
学习·docker
chools10 小时前
【AI超级智能体】快速搞懂工具调用Tool Calling 和 MCP协议
java·人工智能·学习·ai
自信1504130575911 小时前
重生之从0开始学习c++之模板初级
c++·学习