iOS 探索系列相关文章 :
一. 源码跟踪
一般情况下, 当我们想要去了解某个方法的实现的时候, 我们可以在工程里面 command + Ctrl 然后点击我们想要查看的方法。但是在系统方法上却不能找到我们想要的答案, 因为苹果公司并没有把所有方法的实现开源出来, 下面介绍几种寻找源码实现的方法:
方法断点:
第一种方法就是通过给当前方法下断点, 然后逐步往后运行的方法。需要注意的地方如下图:
需要按住 Ctrl 然后一步步往下执行, 才能找到我们需要的东西。还有就是需要在真机上进行调试, 模拟器会在 'pushq' 和 'jmp' 之间一直循环。
最后得到的结果如上图2, libobjc.A.dylib 就是我们所要找的东西。
符号断点:
通过在程序运行中加入 符号断点 来拦截当前正在执行的方法, 如下图:
不过需要注意的是, 在开始打符号断点之前, 首先要先等程序走到我们之前打过的方法断点。因为比如上图的 alloc 方法, 系统有太多的类会去调用该方法, 如果不先定位到方法断点, 就会一直走到符号断点, 无法确认当前 alloc 方法到底是哪个类调用的。
汇编:
使用这个方法首先需要设置 xcode->Debug->Debug workflow->勾选 always Show Disassembly , 然后重新启动程序。 在程序运行到我们的断点的时候就会直接进入汇编界面, 如下图:
找到我们想要了解的方法, 上图中红框内的 objc_alloc, 就是我想要去找的方法。然后继续断点, 按住 Ctrl , 一步步执行, 一直到如下页面:
最后的结果如上图
二. alloc & init & new 探究
准备:
通过上面的方法, 我们找到了我们的方法所在的位置。然后下一步就去展开对源码的探究吧, 具体准备如下:
objc4-750源码 + Xcode 11 + MacOS 10.15
官方的源码下载下来是不能直接编译的, 还需要做一下后面的处理, 具体处理步骤在这里:iOS_objc4-756.2 最新源码编译调试 , 当然你也可以直接下载下来。
1. alloc 原理:
环境配置好之后, 在 alloc 方法打上断点, 然后一步步往下层查看。
alloc 流程图:
废话少说, 先放上我根据调试流程画的 alloc 流程图:
图7 alloc 流程图
从流程图上可以看出, 执行顺序为 alloc -> objc_rootAlloc -> callAlloc , 在 callAlloc() 方法执行以后, 源码出现了一些判断(这里只保留了主要部分代码), 程序开始出现分叉, 下面是方法源码:
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// 没有alloc/allocWithZone实现. 直接去找 allocator.
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
// fixme store 非元类中的 hasCustomAWZ 并且 将其添加到 canAllocFast 的摘要里面去
if (fastpath(cls->canAllocFast())) {
// No ctors, raw isa, etc. Go straight to the metal.
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 {
// Has ctor or raw isa or something. Use the slower path.
// 重点在这里
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls);
return obj;
}
}
#endif
// No shortcuts available.
if (allocWithZone) return [cls allocWithZone:nil];
return [cls alloc];
}
接下来来看一下if语句中的判断条件:
// hasCustomAWZ()的实现
bool hasCustomAWZ() {
return ! bits.hasDefaultAWZ();
}
bool hasDefaultAWZ() {
return data()->flags & RW_HAS_DEFAULT_AWZ;
}
// class or superclass has default alloc/allocWithZone: implementation
// Note this is is stored in the metaclass.
#define RW_HAS_DEFAULT_AWZ (1<<16)
/................分割线..................../
// canAllocFast()的实现
bool canAllocFast() {
assert(!isFuture());
return bits.canAllocFast();
}
// bits.canAllocFast()
#if FAST_ALLOC
......
#else
size_t fastInstanceSize() {
abort();
}
void setFastInstanceSize(size_t) {
// nothing
}
bool canAllocFast() {
return false;
}
// FAST_ALLOC 的声明
#if !__LP64__
......
#elif 1
......
#else
// summary bit for fast alloc path: !hasCxxCtor and
// !instancesRequireRawIsa and instanceSize fits into shiftedSize
#define FAST_ALLOC (1UL<<2)
#endif
关于最外层的判断条件 hasCustomAWZ 可以根据其名字进行大概猜测, 就是 有没有自定义的 allocWithZone: 方法, 如果有的话就跳出判断直接走到 return 的地方. 关于这个判断涉及的知识我目前还不太确定, 只是感觉应该跟 isa 的存储信息有关, 有兴趣的可以继续去了解一下。
对于 canAllocFast() 的判断, 关键点在于 FAST_ALLOC 的宏定义声明, 可以看到在 64位 下会走到 #else 1 里面去, 但是里面根本就没有定义过 FAST_ALLOC, 所以canAllocFast()一直都会是 false。
接下来来看一下 id obj = class_createInstance(cls, 0); 方法, 从方法名字就可以得出 "创建对象":
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;
// Locking: To prevent concurrent realization, hold runtimeLock.
// 加锁, 防止该步骤的并发实现, 保持运行时锁定
assert(cls->isRealized());
// Read class's info bits all at once for performance
bool hasCxxCtor = cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer(); // 是否需要初始化 isa 指针
// 从传参可以得出此时的 extraBytes 为 0
// instanceSize 方法返回一个 size,也就是对象实际需要的 size 大小
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); // 初始化 isa
}
else {
if (zone) {
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
obj->initIsa(cls);
}
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
通过上面代码的相关调用方法名, 我们可以大概猜到 内存申请 (id)calloc(1, size); 和 初始化 isa指针 obj->initInstanceIsa(cls, hasCxxDtor); 的方法 (到这里想到一句话不知道对不对,对象的创建过程就是 isa 的初始化和对象内存开辟的过程), 在这前面还有一个方法,就是对对象开辟内存大小计算的方法:
size_t size = cls->instanceSize(extraBytes); // 获取需要开辟的内存大小
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
return size;
}
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
static inline uint32_t word_align(uint32_t x) {
// 7+8 = 15
// 0000 1111
// 0000 1000
//&
// 1111 1000 ~7
// 0000 1000 8
// 0000 0111
//
// x + 7
// 8
// 8 二阶
// (x + 7) >> 3 << 3
return (x + WORD_MASK) & ~WORD_MASK;
}
这个方法中涉及到两个内容,首先是通过 word_align 方法对属性进行的 8字节 对齐,然后又通过 if(size < 16)size = 16; 对对象大小进行了 16 字节对齐,关于这一块的内容可以看一下这里的内容 iOS 探索-- 内存对齐原理分析 - 掘金 (juejin.cn)
2. init 原理:
- (id)init {
return _objc_rootInit(self);
}
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 方法就相对简单了, 因为他什么都没有做, 只是 返回了 self。init 方法之所以这样实现其实是为了提供 工厂设计模式 下的接口方法, 给子类去自定义重写该方法。
谈到 init , 我们会想起在日常写代码中用到的写法 self = [super init] , 那么我们为什么要这样写呢 ? 结合自己的想法 和 网上找到的意见我总结出一下几点:
3. New 方法:
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
通过查看 new 方法的源码, 我们发现该方法总共做了两件事。首先通过 callAlloc 方法申请内存, 然后再去调用 init 方法, 其实就是对上面两个方法的整合。