单独的去了解不一样块的知识点并不能帮助咱们深刻理解和记忆 , 也并不能把知识点串联达到融会贯通 .html
从实际场景选择一条主线 , 去了解和学习这条主线所碰到的知识点 , 才能更好的把不一样块的只是串联 , 加深理解 .java
从对象的建立 , 去探究对象的本质 , 建立的流程 , 一路上会遇到 isa
, 对象 -> 类 -> 元类
, cache_t
, 内存对齐 , 分类 , taggedPoint
, 方法缓存 , 方法查找 , 消息转发 , 内存管理等内容.ios
这样探索下来 , 咱们不只会熟练掌握这些知识点 , 更能对其融会贯通 , 获得苹果为何会这么设计的根本缘由 . c++
本篇文章从对象的建立出发 , 梳理对象建立流程 , 探索每个遇到的知识点 .objective-c
资料准备 :swift
objc源码 .缓存
objc4-756.2 最新源码编译调试 .bash
对象的建立方式 , 最多见的 alloc
init
, 或者 new
.app
新建工程准备代码 :ide
NSObject * obj = [NSObject alloc];
复制代码
添加好断点 , 运行工程 点击 step into
.
objc
中的
objc_alloc
函数 .
( 笔者使用的是 Xcode 11 , 使用Xcode 10 会进入 alloc 方法 , 下面会讲解这个问题 ) .
实际在 objc 756.2
运行案例 , 在 alloc
和 objc_alloc
分别添加断点 , 你会发现先走的是 objc_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
部分有没有彻底开源的代码对此做了相应操做 . 若有知晓 , 欢迎交流 .
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))
复制代码
那么咱们就来顺便提一下这个知识点 .
其实这两个其实将 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()
.
字面意思看来 , 是判断有没有本身实现 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
, 所以进入到下个流程 .
源码以下 :
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 位系统再也不使用 , 所以使用宏定义来处理兼容 .
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
. 咱们来提一句 .
参考自 :
先来看下 对象的释放流程 .
dealloc
时 , 会判断是否能够被释放,判断的依据主要有 5 个:NONPointer_ISA // 是不是非指针类型 isa
weakly_reference // 是否有若引用
has_assoc // 是否有关联对象
has_cxx_dtor // 是否有 c++ 相关内容
has_sidetable_rc // 是否使用到 sidetable
复制代码
free()
, 执行完毕 , 不然会进入 object_dispose
object_dispose
- 直接调用
objc_destructInstance()
.- 以后调用 C 函数的
free()
.
objc_destructInstance
- 先判断
hasCxxDtor
,若是有c++
相关内容,要调用object_cxxDestruct()
,销毁 c++ 相关内容 .- 再判断
hasAssociatedObjects
,若是有关联对象,要调用object_remove_associations()
,销毁关联对象的一系列操做 .- 而后调用
clearDeallocating()
.- 执行完毕 .
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);
到这里就开始计算所需开辟内存空间了 , 也就涉及到了常常被说起的 内存对齐 .
关于开辟内存 , 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);
}
复制代码
点击进去发现 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);
复制代码
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
文章会详细讲述 .
至此 , 对象的建立已经探索完毕了 . 释放过程咱们也稍微讲述了一下 .
来看下 init
- (id)init {
return _objc_rootInit(self);
}
id _objc_rootInit(id obj)
{
return obj;
}
复制代码
能够看到 init
默认返回方法调用者 . 这个设计实际上是为了方便工程设计 , 以便于在初始化对象时作一些初始化或者赋值操做 .
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
复制代码
new
至关于 alloc
+ init
. 可是使用 new
并不能调用咱们所重写的各类 init
工厂方法 .
有小道消息说是为了 java
等语言的开发者的习惯问题加入的 , 听一听就得了 , 当不得真 .
最后分享一下 , sunnyxx
在线下的一次分享会上给了 4 道题目。 你们能够查看并探讨一下 , 说一说你的答案 , 若有必要分享一篇解析文章 .