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

OC底层原理【一】 alloc init new

OC底层原理【一】 alloc init && new

文章目录

  • OC底层原理【一】 alloc init && new
    • 前言
    • alloc
      • slowpath(checkNil && !cls)) 和 fastpath(!cls->ISA()->hasCustomAWZ())
      • !cls->ISA()->hasCustomAWZ())
    • obj->initInstanceIsa();将类与isa关联
    • NSObject的alloc
    • 内存对齐
        • sizeof
        • class_getInstanceSize
        • malloc_size
      • 内存对齐规则
        • 属性重排
      • NSObject 走objc_alloc方法

前言

这里笔者参考阅读的是objc838系列代码,但是由于笔者的Xcode版本不支持运行该版本,所以笔者采用的是运行objc906的代码,然后对照阅读的Objc838。这里感谢这位大佬的博客提供了一个可编译代码:objc906

这里先看一下alloc的一个流程图:

alloc + init 整体源码的探索流程

alloc

首先OC的对象的一个创建是从alloc开始的:

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

这时候我们逐步跳转alloc函数的每一个步骤,了解函数的一个调用顺序:

+ (id)alloc {return _objc_rootAlloc(self);
}
_objc_rootAlloc(Class cls)
{return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__if (slowpath(checkNil && !cls)) return nil; // 这里大概率为false 所以不会进入nilif (fastpath(!cls->ISA()->hasCustomAWZ())) { //判断一个类是否有自定义的 +allocWithZone 实现,没有则走到if里面的实现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));
}
bool hasDefaultAWZ( ) { //fastpath路径return data()->flags & RW_HAS_DEFAULT_AWZ; //这个值会被储存到·metaClass中}
#define RW_HAS_DEFAULT_AWZ    (1<<16)

slowpath(checkNil && !cls)) 和 fastpath(!cls->ISA()->hasCustomAWZ())

//x的值和很可能为真,真值判断
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
//x的值很可能为假,假值判断

其中的__builtin_expect指令是由gcc引入的, 1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化 2、作用:允许程序员将最有可能执行的分支告诉编译器。 3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。 4、fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大 5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大

!cls->ISA()->hasCustomAWZ())

这里是判断这个类别有没有一个 判断一个类是否有自定义的 +allocWithZone 实现

这里大概率是正确的所以会直接跳转到里面的函数:

_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused) //unused 一个 宏定义,它的作用是为 未使用的变量 打上一个标记,让编译器不要对它发出警告。
{// allocWithZone under __OBJC2__ ignores the zone parameterreturn _class_createInstanceFromZone(cls, 0, nil,OBJECT_CONSTRUCT_CALL_BADALLOC);
}

现在才算正式步入正题,进入我们的alloc的一个核心部分代码:

_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,int construct_flags = OBJECT_CONSTRUCT_NONE,bool cxxConstruct = true,size_t *outAllocatedSize = nil) // cls->instanceSize:计算需要开辟的内存空间大小calloc:申请内存,返回地址指针 obj->initInstanceIsa:将 类 与 isa 关联
{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); // 注意大小,extraBytes大小为0if (outAllocatedSize) *outAllocatedSize = size;id obj;if (zone) { //这里是一个分配内存的内容obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size); //malloc_zone_calloc 是苹果系统(macOS/iOS)特有的底层 API,用于在指定内存分配区域(malloc zone)中分配并初始化零值内存。} else {obj = (id)calloc(1, size); }if (slowpath(!obj)) {if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {return _objc_callBadAllocHandler(cls);}return nil;}if (!zone && fast) {//将 cls类 与 obj指针(即isa) 关联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);
}

这里我们先看一下的一个函数的流程图

_class_createInstanceFromZone流程

我们从这个流程图开始往下看:

inline size_t instanceSize(size_t extraBytes) const {if (fastpath(cache.hasFastInstanceSize(extraBytes))) {return cache.fastInstanceSize(extraBytes); // 这个函数是一个快速计算高度}size_t size = alignedInstanceSize() + extraBytes; // 计算类中所有属性的大小 + 额外的字节数0// CF requires all objects be at least 16 bytes.if (size < 16) size = 16;return size;}

这里我们跳转到fastInstanceSize这个方法中进行一个学习:

size_t fastInstanceSize(size_t extra) const{ASSERT(hasFastInstanceSize(extra));//Gcc的内建函数 __builtin_constant_p 用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0if (__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// 删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节return align16(size + extra - FAST_CACHE_ALLOC_DELTA16); // 会将输入值向上舍入(round up)到最近的 16 字节对齐的地址。提高访问效率}}
//16字节内存对齐函数
static inline size_t align16(size_t x) { return (x + size_t(15)) & ~size_t(15);
}

内存字节对齐原则:

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

内存对齐的一个好处:

  • 16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况。 主要是因为isa指针占了8个字节,这样可以保证一个对象有一个

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

这里我们看一下字节内存对齐函数的操作流程:

16字节对齐算法图解

这里加15的原因是:

  • 确保 x 即使不是 16 的倍数,也能通过“向上舍入”到最近的 16 倍数。

为什么用 & ~15

  • 1111 1111 1111 0000 这里注意的点是他一定会通过15按位与,把后面4位都清0,这里可以类比掩码的处理,保证后四位一定为0,这样两种保证就可以让他肯定是16的某一个整数倍

这种算法保证了一个16位,这样就实现了一个内存对齐的效果

这里我们在calloc这个位置上打上一个断点

image-20250415171741406

这里可以看到我们打印出来的obj,还没有和类别连接起来。按照之前对象的打印逻辑来讲他应该类似于这个<LGPerson: 0x01111111f>

  • 这样说明了objc的地址还没有和穿入的cls进行一个关联
  • 同时印证了alloc的根本作用就是开辟内存

obj->initInstanceIsa();将类与isa关联

现在这一个步骤做的就是将类与地址指针即isa指针进行一个关联,流程图如下:

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{ASSERT(!cls->instancesRequireRawIsa());ASSERT(hasCxxDtor == cls->hasCxxDtor());initIsa(cls, true, hasCxxDtor);
}

image-20250415172621082

主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行关联。

在这里插入图片描述

这张图是一个完整的流程图。这里我们注意一个点,就是alloc函数其实已经完成了创建一个对象并且申请了一块不少于16字节的一个内存空间,绑定isa指针的操作。

这里alloc的核心三步是:

obj->instanceSize > calloc > obj->initInstanceIsa

第一步计算需要开辟的内存大小,第二步开辟内存,第三步将cls类和obj指针关联

那么init函数负责做什么呢?

这里我们可以看一下:

GGObject *obj = [GGObject alloc];
GGObject* obj2 = [obj init];
GGObject* obj3 = [obj init];
NSLog(@"%@, %p, %p", obj, obj, &obj);
NSLog(@"%@, %p, %p", obj3, obj3, &obj3);
NSLog(@"%@, %p, %p", obj2, obj2, &obj2);

这里我们看这个部分的代码的输出结果:

image-20250415220917263

这里说明了他们三个是同一个对象,但是他们三个指向同一块内存空间,但是他们申请的一个内存地址有不一样

这里我们认识一下init的源码:

- (id)init {return _objc_rootInit(self);
}
_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没有做什么操作,只是把当前的对象给返回了。这里主要是通过调用init方法作为一个工厂模式,方便开发着自行定义重写。

NSObject的alloc

这里简单给出一张图做一个简单的介绍,说明他和自定义类的一个区别:

NSObject alloc源码流程

自定义类 alloc源码流程

由此可以得出NSObject中的alloc会走到objc_alloc,其实这部分是由系统级别的消息处理逻辑,所以NSObject的初始化是由系统完成的,因此也不会走到alloc的源码工程中.

内存对齐

这里笔者就简单介绍一下有关OC这里的内容一个内存对齐的内容:

NSObject *objc = [[NSObject alloc] init];
NSLog(@"objc对象类型占用的内存大小%lu", sizeof(objc));
NSLog(@"objc对象实际占有的内存大小%lu", class_getInstanceSize([objc class]));
NSLog(@"objc对象实际占有的内存大小%lu", malloc_size((__bridge const void*)(objc)));

这里我们看一下他的一个打印结果:

image-20250418204312415

这里我们简单分析一下这里的一个原因:

sizeof
  • sizeof是一个操作符,并不是一个函数
  • 用siezof计算内存大小的时候,传入的对象是数据类型,这个在编译期的便宜阶段就会确定大小而不是在运行时确定
  • sizeof最后的道德几个是该数据类型占用空间的一个大小
class_getInstanceSize

这里是有runtime提供的一个api,用于获取类的实例对象所占用的一个内存大小.返回一个具体的字节数,本质是获取实力对象中成员变量的一个内存大小

malloc_size

这个反复该是获取系统实际发分配内存的大小

这里为什么会出现一个实际分配了16个字节但是却实际质用占用一个8字节,而且为什么sizeof是返回一个8字节?

  • sizeof:之所以是一个8字节是因为,我么这里对于类似于NSObject定义实例对象而言,对象类型的本质就是一个结构体指针,其实这里的就是指针,所以sizeof打印的其实是一个对象的指针大小我们知道一个指针的内存大小为8,所以打印的是8
  • Class_getInstanceSize:计算对象实际中诺安用的一个内存大小,这个需要根据类的熟悉而变化,
  • malloc_size计算对象实际分配的内存大小,这个由系统完成,可以由系统完成的,可以从上面的打印结果看出,实际分配分配和实际占用的内存大小并不相等.

内存对齐规则

数据类型对应的字节数表格

上面是我们的一个内存对齐的一个规则,我们可以用这个规则来分析下面两个结构体的一个内存大小的一个打印情况:

结构体对应的存储情况

这里就不过多赘述一个计算结构体大小的一个逻辑了,但是从上面我们可以看出来这里的代吗的一个内存上的一个优化了,明明是一样的一个属性内容,但是却有着不同的一个内存大小的情况.

属性重排

如果结构体中的数据成员是根据内存从大到小的顺序定义的吗根据内存对齐规则来计算就诶狗蹄内存大小,我们只需要补齐比较少的一个内存padding就可以满足内存的对齐的一个规则,所以我们可也对类中的属性进行一个重新排序,来达到一个优化内存的目的.

这里我们可以先看这里的一个代吗内容:

@interface CJLPerson : NSObject@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end@implementation CJLPerson@endCJLPerson *person = [CJLPerson alloc];
person.name      = @"CJL";
person.nickName  = @"C";
person.age       = 18;
person.c1        = 'a';
person.c2        = 'b';
NSLog(@"%@", person);

这时候我们开始有一个断点调试,来观察这个对象中每一个成员变量的数据的时候,我们按照我们之前的一个顺序来看,先找到name和nickname

在这里插入图片描述
在这里插入图片描述

从这两张图片我们可以看出一个内容,就是首先我们这里的一个name和nickName的地址都可以找到这个对象,但是如果我们按照之前的思路来找我们的一个age和height就会发现有问题:

image-20250418215608712

这里变成了一个乱码,这里是因为apple对于这几个属性的内存进行了一个重排,因为age类型炸按5个字节,c1,c2类型站1个字节,通过4+1+!的方式,按照8字节对齐,不足不其方式在统一快内存中.

image-20250418215942594

NSObject 走objc_alloc方法

这里我们现在先认识一下NSObject的objc_alloc方法,他和我们之前讲的alloc方法不一致

  • NSObject 是iOS中的基类,所有自定义的累都要继承自NSObject
  • LGPerson是继承于NSObject累的,重写了alloc方法。

为什么NSobject调用alloc方法,会走到objc_alloc源码

而LGPerson中的alloc会走两次?即调用了alloc,进入源码,还要走到objc_alloc

这里的原因其实是因为NSObject

相关文章:

  • java单元测试不能点击run运行测试方法
  • 【第二天】一月速通Python第二天,函数,数据容器,列表,元组,字典。
  • 论文阅读:2023 arxiv A Survey of Reinforcement Learning from Human Feedback
  • 集成运放的关键技术参数
  • 7.0/Q1,Charls最新文章解读
  • 【Oracle专栏】Oracle中的虚拟列
  • pnpm确认全局下载安装了还是显示cnpm不是内部或外部命令,也不是可运行的程序
  • 算法分析传输加密数据格式密文存储代码混淆逆向保护
  • Mac上Cursor无法安装插件解决方法
  • 【大模型】RAG(Retrieval-Augmented Generation)检索增强生成
  • 使用 NEAT 进化智能体解决 Gymnasium 强化学习环境
  • 分布类相关的可视化图像
  • 从内核到用户态:Linux信号内核结构、保存与处理全链路剖析
  • DMA映射
  • 大模型S2S应用趋势感知分析
  • SSM(SpringMVC+spring+mybatis)整合的步骤以及相关依赖
  • 计算机视觉与深度学习 | LSTM原理,公式,代码,应用
  • n8n 中文系列教程_04.半开放节点深度解析:Code与HTTP Request高阶用法指南
  • 人形机器人马拉松:北京何以孕育“领跑者”?
  • SpringBoot实战3
  • 泡泡玛特一季度整体收入同比增超1.6倍,海外收入增近5倍
  • 中汽协:杜绝虚假宣传与过度营销,确保用户清晰区别驾驶辅助与自动驾驶
  • 第六季了,姐姐们还能掀起怎样的风浪
  • 《王牌对王牌》确认回归,“奔跑吧”将有主题乐园
  • 两部门通报18个破坏耕地、毁林毁草典型问题
  • 全球建筑瞭望|与自然共呼吸的溪谷石舍与海边公共空间