文章目录
- [alloc & init & new源码阅读](#alloc & init & new源码阅读)
alloc & init & new源码阅读
本文内容是关于OC源码学习,简单梳理一下alloc&init&new的实现过程以及内存对齐原理
alloc
首先,我们从main函进入,先看alloc方法:
objc
+ (id)alloc {
return _objc_rootAlloc(self);
}
我们可以看到alloc方法中其实是调用了一个_objc_rootAlloc方法,我们看下实现:
objc
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false, true);
}
方法中又调用了另一个函数,如下:
objc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
//前两行是函数的定义部分,表明函数只能在当前文件中使用,表明函数要尽量内联,提高性能。
//三个参数:cls表示要分配内存的类,checkNil表示是否需要检查cls为nil,最后一个决定是否使用aallocWithZone方法
{
#if __OBJC2__//现代运行时优化路径
if (slowpath(checkNil && !cls)) return nil;//判断类是否为nil
if (fastpath(!cls->ISA()->hasCustomAWZ())) {//cls->ISA()用于获取类的元类信息,hasCustomAWZ()用于判断类是否重写了allocWithZone方法
return _objc_rootAllocWithZone(cls, nil);//底层C函数,用于直接操作内存管理器来分配病初始化对象头
}
#endif//退出路径,如果函数没能走上面的快速路径,就只能走慢路径,发送消息调用alloc/allocWithZone:
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @seletor(allocWithZone:), nil);
}
return ((id(*)(id,SEL))objc_msgSend)(cls, @selector(alloc));
}
这个方法是runtime中分配对象的核心方法之一,用于决定走哪一条路径调用alloc或者allocWithZone:
我们简单总结一下:
这是一个高性能的对象分配调度器,如果是现代OC运行时,先快速检查类是否为nil,迅速返回。通过快速路径检查该类是否没有自定义实现allocWithZone方法,如果没有实现,绕过所有OC消息机制,直接调用C函数
_objc_rootAllocWithZone分配内存,速度最快。如果该类重写了分配逻辑或者处于旧版本运行时,根据参数决定是通过objc_msgSend调用allocWithZone:还是alloc
这条语句就相当于给类对象cls发松消息,从而创建一个该类的实例对象
接下来跳转至:_objc_rootAllocWithZone
objc
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
//zone 参数不再使用 类创建实例内存空间
return _class_createInstanceFromZone(cls, //让函数知道目标类的大小,以及是否有特殊的内存对齐要
0,
nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
可以看见又跳转至:_class_createInstanceFromZone,是alloc源码的核心操作:
objc
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
ASSERT(cls->isRealized());//确保类已经realize完成,即运行时初始化完成,在runtime中,类的加载流程为load->initialize->realzie。其中最后一步会解析superclass、计算instance size、布局ivar、method list处理
// Read class's info bits all at once for performance
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();//是否有C++构造函数
bool hasCxxDtor = cls->hasCxxDtor();//是否有C++析构函数
bool fast = cls->canAllocNonpointer();//是否使用优化isa即nonpointer isa,不只是class指针,还存储引用计数、weak标志、deallocating、has_assoc
size_t size;
size = cls->instanceSize(extraBytes);//计算对象最终大小
if (outAllocatedSize) *outAllocatedSize = size;
id obj;//对象指针
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);//旧版本的内存管理,基本不用
} else {
obj = (id)calloc(1, size);//默认使用calloc
}
if (slowpath(!obj)) {//如果分配失败
if (construct_flags & OBJECT_CONSTRUCT_CALL_BADALLOC) {
return _objc_callBadAllocHandler(cls);
}
return nil;
}
//给对象写入isa指针,内部设计大量流程
if (!zone && 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->Class*,直接是一个指针地址
}
if (fastpath(!hasCxxCtor)) {
return obj;
}
construct_flags |= OBJECT_CONSTRUCT_FREE_ONFAILURE;
return object_cxxConstructFromClass(obj, cls, construct_flags);
}
构造函数:是在创建对象时自动调用的函数,主要作用是初始化对象的数据成员,函数名必须和类名相同,没有返回值类型,可以重载(一个类可以有多个构造函数)
objcclass Person { public: string name; int age; Person() { name = "Unknown"; age = 0; } Person(string n, int a) { name = n; age = a; } };析构函数:在对象销毁时自动调用的函数,用于释放资源,清理内存。函数名前必须加~,函数名与类名相同,没有返回值,不能带参数,一个类只能有一个析构函数。
objc#include <iostream> using namespace std; class Person { public: Person() { cout << "对象创建" << endl; } ~Person() { cout << "对象销毁" << endl; } }; int main() { Person p1; }创建对象 → 调用构造函数
程序结束 → 调用析构函数
OC中的自定义类的alloc方法调用流程涉及双重的callAlloc机制,其根源在与类初始化延迟和运行时多态性设计
- 首次调用
objc_alloc
id objc_alloc(Class lcs)提供运行时的全局分配函数。- 检查类是否已经初始化
(cls->isRealized())。- 如果未初始化触发
class_initialize()完成类加载- 进入
callAlloc
checkNil = true,allocWithZone = false- 检查类是否存在
slowpath(checkNil && !cls)- 若类支持优化路径(未重写
allocWithZone:),直接调用_objc_rootAllocWithZone。- 动态派发
objc_msgSend
- 如果类重写了
allocWithZone:需要通过消息发送调用自定义逻辑- 进入类的
+alloc方法,最终再次调用callAlloc(需要判断是否重写了allocwithzone方法,决定走快速路径还是慢速路径)- 第二次
callAlloc
checkNil = false,allocWithZone = true- 直接调用
_objc_rootAllocWithZone,跳过安全检查- 最终通过
_class_createInstanceFromZone完成内存分配和isa绑定
为什么自定义类可能需要进入两次callAlloc呢?核心就是第一次callAlloc用来处理alloc消息分发,第二次用来执行真正的root alloc创建对象
系统类NSObject在编译时已经完成了isa指针的初始化,类结构初始化完成。所以不需要经过第一次callAlloc判断,只需要直接进入alloc流程。
objc_alloc->callAlloc->_objc_rootAllocWithZone直接申请空间即可。自定义类:
第一次callAlloc目的是检查类的初始化状态,runtime在objc_msgSend机制里检查类是否已经initialize,如果没有,会调用+initialize


大概流程如图所示:

alloc核心操作
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.
if (size < 16) size = 16;
return size;
}
接下来看一下fastInstanceSize中:
objc
size_t fastInstanceSize(size_t extra) const//用于快速计算对象实例的大小
{
ASSERT(hasFastInstanceSize(extra));//确保当前类支持fast instcane size计算,类的instanceSize已经缓存、extraBytes不会导致溢出、可以安全使用fast Path
//Gcc的内建函数 __builtin_constant_p 用于判断一个值是否为编译时常数,如果参数EXP 的值是常数,函数返回 1,否则返回 0
if (__builtin_constant_p(extra) && extra == 0) {//判断参数是否为编译期常量,如果extra = 0并且是编译器常量可以走最快路径(触发编译器优化)
return _flags & FAST_CACHE_ALLOC_MASK16;//直接从类缓存中读取instanceSize,其中_flags是类对象的一个位字段,内部存储了instance size、fast alloc flag、other runtime flags。后面参数作用是提取16字节内存对齐的instanceSize
} else {
size_t size = _flags & FAST_CACHE_ALLOC_MASK;//取出缓存的基础instance size
// remove the FAST_CACHE_ALLOC_DELTA16 that was added
// by setFastInstanceSize
// 删除由setFastInstanceSize添加的FAST_CACHE_ALLOC_DELTA16 8个字节(_flags中不仅存size还存flags,所以为了避免size = 0.会加一个偏移量)
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);
}
都位于calloc方法中。cls->instanceSize:计算所需要的内存大小,流程如下:

内存字节对齐原则
- 数据成员对齐规则:struct或者union的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始。
- 结构体的总大小即sizeof的结果必须是其内部最大成员大小的整数倍。
内存对齐的好处:
- 16字节对齐后,可以加快CPU的访问速度,同时访问更加安全,主要是为了保证isa指针的大小,一个对象一个isa指针
- CPU在存取数据的时候,是以块(总线宽)位为单位存储。块的大小为内存存取力度,频繁的存取未对齐数据会极大降低cpu的性能,可以通过减少存取次数来降低CPU开销
下面我们看一下16字节对齐算法的例子:

calloc:申请内存,返回地址指针
通过instanceSize计算内存大小,向内存中申请大小为size的内存,并赋值给obj,所以obj是指向内存地址的指针
objc
obj = (id)calloc(1, size);
在未执行calloc时,obj为nil,执行后,obj返回一个16进制的地址
Obj->initInstanceIsa:
类与地址指针关联。
objc
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
ASSERT(!cls->instancesRequireRawIsa());
ASSERT(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}

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

init源码
举一个例子简单学习一下:
objc
- (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方法作为一个工厂方法,方便开发者自定义进行重写
new源码
objc
- (id)new {
return [callAlloc(self, false,) init];
}
与init本质没啥区别,其中直接调用了callAlloc函数,且调用了init函数,(约等于alloc、 init)
补充

sizeof- 一个操作符
- 传入的是对象的数据类型,在编译阶段就已经确认了大小,而不是在运行时确定的
class_getInstanceSize()- runtime提供的api用于获取类的实例对象所占用的内存大小,返回一个具体的字节数,本质是获取实例对象的成员变量中的内存大小
malloc_size- 获取系统实际分配内存的大小
alloc实际流程
实际的alloc调用流程与上述有不同:
在实际运行中,NSObject或者MyClass类调用alloc通常不会进入
libobjc源码层,而是走了更加高效的底层路径,这是因为苹果对运行时做了大量的优化来避免频繁进入C层函数
对于NSObject类,objc_msgSend是汇编函数,大多时候在汇编层直接查找IMP并跳转执行,不会进入OC的runtime的C函数实现。涉及cache_t的方法缓存,后续学习。
alloc的调用流程
无论是自定义类还是NSObject类调用alloc方法最开始进入的都是objc_alloc,而不是obj_rootAlloc,这是因为消息转发时系统在底层帮我们转发到了objc_alloc。
源码实现:
objc
id
objc_alloc(Class cls) {
return callAlloc(cls, true, false);
}
可以看到和objc_rootAlloc实现是一样的。
对于NSobject类,初始化在llvm编译时就已经初始化好了,所以缓存中已经有alloc/allocWothZone方法。hasCustomAWZ()为false。!cls->ISA()->hasCustomAWZ()为true,走了快速路径。
对于自定义类,初次创建时没有默认的alloc/allocWithZone方法实现,所以继续向下执行到消息转发流程,消息转发向父类栈,最终找到NSObject的alloc方法并调用,后续就是在此进入callAlloc流程