前言
单独的去了解不同块的知识点并不能帮助我们深入理解和记忆 , 也并不能把知识点串联达到融会贯通 .
从实际场景选择一条主线 , 去了解和学习这条主线所碰到的知识点 , 才能更好的把不同块的只是串联 , 加深理解 .
从对象的创建 , 去探究对象的本质 , 创建的流程 , 一路上会遇到 isa
, 对象 -> 类 -> 元类
, cache_t
, 内存对齐 , 分类 , taggedPoint
, 方法缓存 , 方法查找 , 消息转发 , 内存管理等内容.
这样探索下来 , 我们不仅会熟练掌握这些知识点 , 更能对其融会贯通 , 得到苹果为什么会这么设计的根本原因 .
本篇文章从对象的创建出发 , 梳理对象创建流程 , 探索每一个遇到的知识点 .
资料准备 :
OC 对象的创建探索
对象的创建方式 , 最常见的 alloc
init
, 或者 new
.
新建工程准备代码 :
NSObject * obj = [NSObject alloc];
添加好断点 , 运行工程 点击 step into
.


objc
中的 objc_alloc
函数 . ( 笔者使用的是 Xcode 11 , 使用Xcode 10 会进入 alloc 方法 , 下面会讲解这个问题 ) .
实际在 objc 756.2
运行案例 , 在 alloc
和 objc_alloc
分别添加断点 , 你会发现先走的是 objc_alloc
.

1、objc_alloc 与 alloc
但是查阅源码我们看到 NSObject
是有 alloc
类方法的 . 那么我们外部所写的 [NSObject alloc]
为什么不调用 alloc
类方法 , 反而来到了 objc_alloc
中呢 ?
这部分笔者通过一部分源码结合 MachO
文件查看推测如下 :
Xcode 10
会直接进入alloc
,Xcode 11
会直接进入objc_alloc
是因为在Xcode11
编译后alloc
对应符号会被设置为objc_alloc
.- 而
Xcode 10
并没有 . 我们可以使用MachOView
分别查看这两种环境下编译的项目Mach-O
, 在__DATA
段 ,__la_symbol_ptr
节中 .
以下为笔者测试结果 .


另外在 objc
源码中查找到部分代码如下 :
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == SEL_alloc) {
msg->imp = (IMP)&objc_alloc; // 这里就是符号绑定后对应所做的一些处理了.
} else if (msg->sel == SEL_allocWithZone) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == SEL_retain) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == SEL_release) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == SEL_autorelease) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
}
}
/*...*/
}
( 关于符号绑定 , 可以阅读一下 Hook / fishHook 原理与符号表 、 从头梳理 dyld 加载流程 这两篇文章 , 本文就不在多阐述了 ) .
◈ alloc
类方法源码如下 :
+ (id)alloc {
return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
◈ objc_alloc
函数如下 :
id objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
我们可以看到不管是 alloc
还是 objc_alloc
, 都会进入 callAlloc
这个函数 , 只是最后两个参数传入的不同 . 那么我们就继续往下看 .
◈ --> 提示 :
至于在
Xcode 11
调用[NSObject alloc]
会来到objc_alloc
, 而内部在callAlloc
函数中[cls alloc]
则会直接进入alloc
, 笔者还没有查找到确切资料来证实 , 猜测符号绑定和fixup
部分有没有完全开源的代码对此作了相应操作 . 如有知晓 , 欢迎交流 .
2、 callAlloc
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
if (fastpath(cls->canAllocFast())) {
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
}
else {
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
首先我们注意到了两个宏定义的函数 : fastpath
与 slowpath
.
// x 很可能不为 0,希望编译器进行优化
#define fastpath(x) (__builtin_expect(bool(x), 1))
// x 很可能为 0,希望编译器进行优化
#define slowpath(x) (__builtin_expect(bool(x), 0))
那么我们就来顺便提一下这个知识点 .
2.1、fastpath 与 slowpath
其实这两个其实将 fastpath
和 slowpath
去掉是完全不影响任何功能的。之所以将 fastpath
和 slowpath
放到 if
语句中,是为了告诉编译器 :
if
中的条件是大概率 ( fastpath ) 还是小概率 ( slowpath ) 事件
从而让编译器对代码进行优化。
那么如何告诉编译器 , 或者说编译器如何针对处理和优化的呢 ?
举个例子 🌰 :
if (x)
return 2;
else
return 1;
解读 :
-
1️⃣ : 由于计算机并非一次只读取一条指令,而是读取多条指令,所以在读到
if
语句时也会把return 2
读取进来。如果x
为0
,那么会重新读取return 1
,重读指令相对来说比较耗时。 -
2️⃣ : 如果
x
有非常大的概率是0
,那么return 2
这条指令每次不可避免的会被读取,并且实际上几乎没有机会执行,造成了不必要的指令重读。 -
3️⃣ : 因此,在苹果定义的两个宏中,
fastpath(x)
依然返回x
,只是告诉编译器x
的值一般不为0
,从而编译可以进行优化。同理,slowpath(x)
表示x
的值很可能为0
,希望编译器进行优化。
这个例子的讲解来自 bestsswifter 的 深入理解GCD,大家感兴趣可以看看。
因此 我们 callAlloc
中 , 第一步
if (slowpath(checkNil && !cls)) return nil;
其实就是告诉编译器 , cls
大概率是有值的 , 编译器对应处理就好 .
那么接下来就来到了 cls->ISA()->hasCustomAWZ()
.
2.2、hasCustomAWZ
字面意思看来 , 是判断有没有自己实现 AllocWithZone
方法 . 这个是通过 类的结构体 objc_class
中的 hasCustomAWZ
方法判断的 .
bool hasCustomAWZ() {
return ! bits.hasDefaultAWZ();
}
hasDefaultAWZ
实现如下 :
bool hasDefaultAWZ() {
return data()->flags & RW_HAS_DEFAULT_AWZ;
}
void setHasDefaultAWZ() {
data()->setFlags(RW_HAS_DEFAULT_AWZ);
}
void setHasCustomAWZ() {
data()->clearFlags(RW_HAS_DEFAULT_AWZ);
}
其实是在 RW
中所做标记来标识用户有没有自己实现 allocWithZone
.
由于类是有懒加载概念的 , 当第一次给该类发消息之前 , 类并没有被加载 , 因此 , 当类第一次接受到 alloc
, 进入到 hasCustomAWZ
时 , 并没有 DefaultAWZ
, 所以 hasCustomAWZ
则为 true
, 因此会直接进入 [cls alloc];
我们可以做一下测试 , 代码如下 :
LBPerson *objc = [[LBPerson alloc] init];
LBPerson *objc1 = [[LBPerson alloc] init];
当 objc
进入到 callAlloc
时 , 会进入下面的 [cls alloc]
, 而当 objc1
进入时 , 会直接进入 if (fastpath(!cls->ISA()->hasCustomAWZ())) {
内部 .
提示 :
- 1️⃣ : 我们所熟知的
initialize
, 也是在类接收到第一次消息时 , 在objc_msgSend
流程被触发调用的 .- 2️⃣ : 上述结果为
Xcode 11
环境下 ,Xcode 10
环境 直接进入alloc
即是objc_msgSend
, 因此会直接进入if
成立流程 .- 3️⃣ : 关于
allocWithZone
, 我们暂且需要知道的是它是对象开辟的另一种方法 , 如果重写了 , 在 alloc 时 , 则会进入用户自定义的allocWithZone
流程 . 这也是我们在写单例时 , 也要处理allocWithZone
的原因 .
lookUpImpOrForward
中针对 initialize
所做处理 .
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
/*...*/
if (initialize && !cls->isInitialized()) {
runtimeLock.unlock();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.lock();
}
/*...*/
}
关于类的结构 以及 isa
的具体内容 , 由于内容较多 , 我会另起两篇文章专门讲述 , 先放一张图 , 方便有个大概理解 .

当第一次进入 [cls alloc];
, 我们来看下源码实现 :
+ (id)alloc {
return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
再次来到 callAlloc
中 , 由于 [cls alloc];
触发的是消息发送机制 , DefaultAWZ
为 true
, 那么 hasCustomAWZ
则为 false
, 因此进入到下个流程 .
2.3、canAllocFast
源码如下 :
bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
#if !__LP64__
/**/
#elif 1
#else
#define FAST_ALLOC (1UL<<2)
#if FAST_ALLOC
#else
bool canAllocFast() {
return false;
}
#endif
可以很清楚的看到 返回 false
. 因此 callAlloc
则来到了
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
至于为什么要这么做 , 其实是因为在 32 位系统下 , 有额外的流程 , 而 64 位系统不再使用 , 因此使用宏定义来处理兼容 .
2.4、class_createInstance
id class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
static __attribute__((always_inline))
id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
if (!cls) return nil;
assert(cls->isRealized());
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
看函数名称和返回值 , 我们知道来到了重点 , 在这里就开始创建对象 , 分配内存空间了 .
首先是 hasCxxCtor
与 hasCxxDtor
. 我们来提一句 .
参考自 :
2.4.1、 hasCxxCtor 与 hasCxxDtor
先来看下 对象的释放流程 .
- 1️⃣ : 在对象
dealloc
时 , 会判断是否可以被释放,判断的依据主要有 5 个:NONPointer_ISA // 是否是非指针类型 isa weakly_reference // 是否有若引用 has_assoc // 是否有关联对象 has_cxx_dtor // 是否有 c++ 相关内容 has_sidetable_rc // 是否使用到 sidetable
- 2️⃣ : 如果没有之前 5 种情况的任意一种,则可以执行释放操作,C 函数的
free()
, 执行完毕 , 否则会进入object_dispose
- 3️⃣ :
object_dispose
- 直接调用
objc_destructInstance()
. - 之后调用 C 函数的
free()
.
- 直接调用
- 4️⃣ :
objc_destructInstance
- 先判断
hasCxxDtor
,如果有c++
相关内容,要调用object_cxxDestruct()
,销毁 c++ 相关内容 . - 再判断
hasAssociatedObjects
,如果有关联对象,要调用object_remove_associations()
,销毁关联对象的一系列操作 . - 然后调用
clearDeallocating()
. - 执行完毕 .
- 先判断
- 5️⃣ :
clearDeallocating()
调用流程- 先执行
sideTable_clearDeallocating()
. - 再执行
waek_clear_no_lock
,将指向该对象的弱引用指针置为nil
. - 接下来执行
table.refcnts.eraser()
,从引用计数表中擦除该对象的引用计数 . - 至此为此,
dealloc
的执行流程结束 .
- 先执行
这两个其实一开始是 objc++
中用来处理 c++
成员变量的构造和析构的,后来 .cxx_destruct
也用来处理 ARC 下的内存释放。
-
在使用
MRC
时,开发人员必须手动编写dealloc
以确保释放对其保留的所有对象的所有引用。这是手动操作,容易出错。 -
引入
ARC
时,执行与这些手动发行版等效的任务的代码必须在每个具有除简单属性之外的所有对象的对象中实现。依靠开发人员手动实现dealloc
例程将无法解决这一问题。 -
因此使用了
objective-c ++
的预先存在的机制,即一个被称为隐藏选择器,该选择器 (.cxx_destruct
) 在对象被释放之前自动被Objective C
运行时调用 , 它们由编译器自动生成 。
因此 hasCxxCtor
与 hasCxxDtor
, 就是为了标记是否有这两个选择器 .
可能有同学注意过 , 在我们获取类的方法列表时就有 .cxx_destruct
.
测试 :
void testObjc_copyMethodList(Class pClass){
unsigned int count = 0;
Method *methods = class_copyMethodList(pClass, &count);
for (unsigned int i=0; i < count; i++) {
Method const method = methods[i];
//获取方法名
NSString *key = NSStringFromSelector(method_getName(method));
NSLog(@"Method, name: %@", key);
}
free(methods);
}
打印如下 :

因此 , .cxx_destruct
也被常称为 隐藏选择器 .
回到 class_createInstance
中来 , 下一步 , canAllocNonpointer
, 这里在 isa
中会详细讲述 . 接下来来到 size_t size = cls->instanceSize(extraBytes);
2.4.2、instanceSize
到这里就开始计算所需开辟内存空间了 , 也就涉及到了经常被提及的 内存对齐 .
关于开辟内存 , OC对象占用内存原理 这篇文章中也有详细讲述 .
先来看源码 :
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF 要求 all objects 需要最少为 16 bytes.
if (size < 16) size = 16;
return size;
}
// Class's ivar size 四舍五入 to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
#ifdef __LP64__
# define WORD_SHIFT 3UL
# define WORD_MASK 7UL
# define WORD_BITS 64
#else
# define WORD_SHIFT 2UL
# define WORD_MASK 3UL
# define WORD_BITS 32
#endif
instanceSize
传入参数 extraBytes
为 0 , 从上面源码我们首先可以看到 , 属性64 位下满足 8 字节对齐 , 32 位下满足 4 字节对齐 .
使用的是 (x + WORD_MASK) & ~WORD_MASK ;
. 跟位运算左移三位右移三位是同样的效果 , 类结构体 RO
中的信息在编译期就已经确定了 ( data()->ro->instanceSize
, 也就是 unalignedInstanceSize
) .
同时 , 满足最小 16
字节 ( if (size < 16) size = 16
) .
那么接下来 , 由于传入 zone
为 NULL
, 并且是支持 Nonpointer isa
的 . 因此来到 if
满足语句中 .
id obj;
if (!zone && fast) {
obj = (id)calloc(1, size);
if (!obj) return nil;
obj->initInstanceIsa(cls, hasCxxDtor);
}
2.4.3、calloc
点击进去发现 calloc
源码在 malloc 中 .
void * calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
小提示 : 在跟不进源码时可以按照以下方式

最后跟到这里 :
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
mag_index_t mag_index = nano_mag_index(nanozone);
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
/**省略*/
} else {
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
其中 segregated_size_to_fit
如下 :
static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
size_t k, slot_bytes;
if (0 == size) {
size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
}
k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
slot_bytes = k << SHIFT_NANO_QUANTUM;// multiply by power of two quanta size
*pKey = k - 1;// Zero-based!
return slot_bytes;
}
#define SHIFT_NANO_QUANTUM 4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) // 16
可以看出 slot_bytes
相当于 (size + (16-1) ) >> 4 << 4
,也就是 16 字节对齐,因此 calloc()
分配的对象内存是按 16 字节对齐标准的 .
那么 calloc
开辟了内存空间 , 并返回一个指向该内存地址的指针 . 回到 libobjc
, _class_createInstanceFromZone
接下来 .
obj->initInstanceIsa(cls, hasCxxDtor);
2.4.4、initInstanceIsa
inline void
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
assert(!cls->instancesRequireRawIsa());
assert(hasCxxDtor == cls->hasCxxDtor());
initIsa(cls, true, hasCxxDtor);
}
inline void
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)
{
assert(!isTaggedPointer());
if (!nonpointer) {
isa.cls = cls;
} else {
assert(!DisableNonpointerIsa);
assert(!cls->instancesRequireRawIsa());
isa_t newisa(0);
#if SUPPORT_INDEXED_ISA
/*arm64 不走这里*/
#else
newisa.bits = ISA_MAGIC_VALUE;
newisa.has_cxx_dtor = hasCxxDtor;
newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
isa = newisa;
}
}
这里就是初始化 isa
, 并绑定指向 cls
:
newisa.shiftcls = (uintptr_t)cls >> 3;
后续 isa
文章会详细讲述 .
至此 , 对象的创建已经探索完毕了 . 释放过程我们也稍微讲述了一下 .
3、init
来看下 init
- (id)init {
return _objc_rootInit(self);
}
id _objc_rootInit(id obj)
{
return obj;
}
可以看到 init
默认返回方法调用者 . 这个设计其实是为了方便工程设计 , 以便于在初始化对象时做一些初始化或者赋值操作 .
4、new
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
new
相当于 alloc
+ init
. 但是使用 new
并不能调用我们所重写的各种 init
工厂方法 .
有小道消息说是为了 java
等语言的开发者的习惯问题加入的 , 听一听就得了 , 当不得真 .
分享
最后分享一下 , sunnyxx
在线下的一次分享会上给了 4 道题目。 大家可以查看并探讨一下 , 说一说你的答案 , 如有必要分享一篇解析文章 .
