一、探索底层原理的思路
1、一切从main函数入手
我们想着手开始探索iOS的底层,但又不知道从哪里开始,怎么办?
那就从main函数入手!
我们先开启上帝视角!来观察一个粗略的加载流程。进行准备工作:
- 在main函数中直接打断点
- 下一个符号断点
_objc_init
- 关闭左侧debug栏下面的第一个按钮
运行!来看一下调用堆栈的信息:
我们可以从下到上看出一个大致的流程:
当然,这其中还进行了很多我们看不到的操作,ImageLoader中包含了我们的类、分类、属性、协议、方法、方法编号等等,我们以后再慢慢探索~ 这里想说的是:这一套下来,就是底层探索的主思路!
2、如何找到源码中具体的实现方法
2.1 引发思考
我们先看这么一段代码
Person *p1 = [Person alloc];
Person *p2 = [p1 init];
Person *p3 = [p1 init];
NSLog(@" %@ - %p",p1,&p1);
NSLog(@" %@ - %p",p2,&p2);
NSLog(@" %@ - %p",p3,&p3);
打印结果:
<Person: 0x600003b3fc30> - 0x7ffee88cc138
<Person: 0x600003b3fc30> - 0x7ffee88cc130
<Person: 0x600003b3fc30> - 0x7ffee88cc128
结论:
- 打印三个对象,结果一样,但是并不能说明他们就是同一个对象;
我们打印三个指针,得出三个指针不一样
因此说明三个不是同一个对象,但是他们的指针指向同一片内存空间 - 也就是说,alloc开辟了内存空间,但是init并没有对这份内存进行修改
于是,就产生了去看一下alloc方法是怎么实现的想法,但是直接command进去并没有实现方法啊。
那我们就要学会怎么找到源码!
2.2 源码探索方式
这里介绍三种方式:
注意一点:真机调试打断点找arm64、模拟器找的是x86
- 直接断点:
ctrl+in(↓)
跳到汇编,找到libobjc.A.dylib
动态库里的源码
- 下符号断点:先断点(防止跳到同名方法,先准确断点比较安全),然后下符号断点。就是上面提到过的(不再截图,把
_objc_init
换成alloc
即可)找到Symbolic Breakpoint
选项,在Symbol中输入要断点的内容,然后运行,就会跳到汇编里,找到libobjc.A.dylib`+[NSObject alloc]:
- 汇编:断点,运行,然后在Xcode顶部找到Debug菜单,下面Debug WorkFlow中勾选 Always Show Disassembly,就来到汇编,在下面bl跳转行可以看到会进入
objc_alloc
里面
通过这三种方法,我们都能得出,要想看alloc的源码,要找到libobjc这个动态库,我们去官网找一下是可以找到的。
直接下载无法运行,去调试一下:objc4-756.2源码编译调试
【注意:小编之前是在配置好的 756.2 上调试研究的,最新版的 779.1 针对callalloc方法进行了优化,后续有时间再进行更新,有兴趣的同学,请自行探索哦~】
目前最新的是 objc4-779.1
,下载地址: 苹果开源-10.15
直接下载无法运行,去调试一下:objc4-779.1源码编译调试
二、开始探索alloc
我们知道,alloc是用来创建对象,申请内存空间(申请内存空间是让它有了相应的指针。此时对象就有了相应的指针地址,也就意味着它就拥有了这片内存空间)。
我们可以通过卡断点,卡到alloc里面,然后通过LLDB调试,读寄存器register read
。看x0
是否返回一个指针地址,就知道alloc是否有申请内存空间的能力。(这里有一项规定:x0 是第一个参数的传递者,在返回的时候,也是返回值的存储地方
)
有兴趣的,可以自己去调试一下试试,结果当然是肯定的!alloc是具有为对象申请内存空间的能力。
而我们这里是在 配置好的libObjc-756.2源码
中探索,比在项目中调试汇编要爽多了~
1、准备工作
在libObjc源码中,创建新的target自定义项目文件来跑。执行下面代码(注意这个是动态库,要在mac上跑,无法跑进iOS的沙盒,不能再iOS上跑)
Person *p = [Person alloc];
我们可以先command进去所有关联的方法都进行断点,然后具体看alloc的都进了哪些函数,就知道了它的执行流程。
2、alloc在源码中的执行流程
2.1 alloc
首先command选中来到alloc
方法
+ (id)alloc {
return _objc_rootAlloc(self);
}
2.2 _objc_rootAlloc
第二步来到_objc_rootAlloc
方法
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
2.3 callAlloc
第三步来到callAlloc
方法,但是!!!出现了岔路了!不要急,我们慢慢看,它会走到class_createInstance
方法
(objc有2个版本:objc、objc2 。我们现在用的都是最新的objc2)
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
/*
#define fastpath(x) (__builtin_expect(bool(x), 1)) 表示 x 的值为真的可能性更大
#define slowpath(x) (__builtin_expect(bool(x), 0)) 表示 x 的值为假的可能性更大
我们仔细解读一下:
fastpath(x) 表示执行if后面的操作的可能性大
slowpath(x) 表示执行else后面的操作的可能性大,意思就是不执行if
这里是一个判空操作
*/
if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// No alloc/allocWithZone implementation. Go straight to the allocator.
// 大概意思:既没有实现 alloc,也没有实现 allocWithZone 就会来到这里,下面直接进行内存开辟操作。
// fixme store hasCustomAWZ in the non-meta class and
// add it to canAllocFast's summary
//大概意思:修复没有元类的类,也就是没有继承于 NSObject
//判断当前类是否可以快速开辟内存,注意,这里永远不会被调用,因为 canAllocFast 内部返回的是false
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];
}
2.4 class_createInstance
第四步来到class_createInstance
方法,继续!
id
class_createInstance(Class cls, size_t extraBytes)
{
return _class_createInstanceFromZone(cls, extraBytes, nil);
}
2.5 _class_createInstanceFromZone
第五步来到_class_createInstanceFromZone
方法。这里接着往下读,会发现calloc
点不进去了!因为这部分代码在malloc的源码当中,我们之后再说。
cls->instanceSize
拿的class需要的内存大小
obj = (id)calloc(1, size);
calloc开辟了这么大的空间,这个空间就是obj
obj->initInstanceIsa
通过isa把obj和class关联起来
最后返回obj,就完成了整个流程
static __attribute__((always_inline))
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{
// 对 cls 进行判空操作
if (!cls) return nil;
// 断言 cls 是否实现了
assert(cls->isRealized());
// Read class's info bits all at once for performance
// cls 是否有 C++ 的初始化构造器
bool hasCxxCtor = cls->hasCxxCtor();
// cls 是否有 C++ 的析构器
bool hasCxxDtor = cls->hasCxxDtor();
// cls 是否可以分配 Nonpointer,如果是,即代表开启了内存优化
bool fast = cls->canAllocNonpointer();
// 这里传入的 extraBytes 为0,然后获取 cls 的实例内存大小
size_t size = cls->instanceSize(extraBytes);
// 这里 outAllocatedSize 是默认值 nil,跳过
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
// 这里 zone 传入的也是nil,而 fast 拿到的是 true,所以会进入这里的逻辑
if (!zone && fast) {
/*
来看这个方法,calloc就是开辟的意思,源码在malloc中
calloc开辟了obj的空间,并且obj是这个方法的返回值
通过initInstanceIsa方法,把obj和class(我们传进来的第一个参数)通过isa关联起来,于是就成功为我们的对象开辟了空间
那calloc中的size呢?开辟了多少内存空间呢?是这个cls->instanceSize方法实现的
*/
// 根据 size 开辟内存
obj = (id)calloc(1, size);
// 如果开辟失败,返回 nil
if (!obj) return nil;
// 将 cls 和是否有 C++ 析构器传入给 initInstanceIsa,实例化 isa
obj->initInstanceIsa(cls, hasCxxDtor);
}
else {
// 如果 zone 不为空,经过测试分析,一般来说调用 alloc 不会来到这里,只有 allocWithZone
// 或 copyWithZone 会来到下面的逻辑
if (zone) {
// 根据给定的 zone 和 size 开辟内存
obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
} else {
// 根据 size 开辟内存
obj = (id)calloc(1, size);
}
// 如果开辟失败,返回 nil
if (!obj) return nil;
// Use raw pointer isa on the assumption that they might be
// doing something weird with the zone or RR.
// 初始化 isa
obj->initIsa(cls);
}
// 如果有 C++ 初始化构造器和析构器,进行优化加速整个流程
if (cxxConstruct && hasCxxCtor) {
obj = _objc_constructOrFree(obj, cls);
}
return obj;
}
2.6 alloc流程总结
我们是根据自定义一个类去执行alloc方法查找流程的,如果是其他情况可能出现不同的流程走向。那么我们对初始化的大致流程进行总结一下:
2.7 allocWithZone
我们根据一步步点击进去看实现方法(就是2.1~2.5的流程),但是当我们细心一点,在所以alloc有关函数打断点再运行调试会发现,首先进入的是objc_alloc
这个方法
// Calls [cls alloc].
id
objc_alloc(Class cls)
{
return callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/);
}
然后在callAlloc
方法中,因为传入参数checkNil = true, allocWithZone = false
,然后直接走到return [cls alloc]
方法,然后就是我们2.1~2.5的流程。
我也去查阅了一些资料,有大神指出,是在macho生成的时候会绑定一个symbol,里面会把sel_alloc的编号绑定到了objc_alloc上,这部分没有真正开源。但是objc_alloc只会走一次,最终也是会走到alloc的正常流程。并且还找出验证猜想的地方:
/***********************************************************************
* fixupMessageRef
* Repairs an old vtable dispatch call site.
* vtable dispatch itself is not supported.
**********************************************************************/
static void
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
//***看这里!!!这个if判断里
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;
}
}
else if (msg->imp == &objc_msgSendSuper2_fixup) {
msg->imp = &objc_msgSendSuper2_fixedup;
}
else if (msg->imp == &objc_msgSend_stret_fixup) {
msg->imp = &objc_msgSend_stret_fixedup;
}
else if (msg->imp == &objc_msgSendSuper2_stret_fixup) {
msg->imp = &objc_msgSendSuper2_stret_fixedup;
}
#if defined(__i386__) || defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fpret_fixup) {
msg->imp = &objc_msgSend_fpret_fixedup;
}
#endif
#if defined(__x86_64__)
else if (msg->imp == &objc_msgSend_fp2ret_fixup) {
msg->imp = &objc_msgSend_fp2ret_fixedup;
}
#endif
}
fixupMessageRef
实际上是在_read_images
里调用的,是在启动后读取文件时候,大致就是出现问题了,然后走到这里进行修复,然后进行了绑定,才会调用objc_alloc
这个方法。(期待后面苹果会开源出来让我们继续研究)
从苹果官方文档也能看出,其实 allocWithZone
本质上和 alloc
是没有区别的,只是在OC开发的早期,程序员需要使用诸如 allocWithZone
来优化对象的内存结构,但是现在,我们写 alloc
和 allocWithZone
在底层其实是一模一样的。
三、开始探索init
看完了alloc,我们再来看看init。你会发现,init就是懒省事,最终直接返回self。其实就是把大部分工作交给了alloc完成。
init的唯一作用就是规范代码,交给子类去自定义重写,其他没有实质性的功能作用 看下源码:
+ (id)init {
return (id)self;
}
- (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;
}
四、开始探索new
点到源码,你会发现,它比init还懒,相当于直接返回alloc+init方法。
+ (id)new {
return [callAlloc(self, false/*checkNil*/) init];
}
五、内存的Size 及 字节对齐
我们在刚刚探索alloc的时候,cls->instanceSize
方法拿到对象的大小,那么这个大小是怎么计算出来的呢?也就是说我们的对象需要开辟多少内存空间,来存放它的属性?就好比,要自己建房子,根据都有谁住,总共住多少人来规划要有多少个房间?
我们一步步分析:
1、首先从这个size
方法入手
//这个方法计算的size,extraBytes是传进来的,是0,在callAlloc方法里面可以看见
size_t size = cls->instanceSize(extraBytes);
2、接着来到instanceSize
方法, 先不管计算的size,看后面一句,size最小是16字节
size_t instanceSize(size_t extraBytes) {
size_t size = alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
//最小返回16字节
if (size < 16) size = 16;
return size;
}
3、接着来到alignedInstanceSize
方法,看名字就是知道:要和我们的实例对象进行对齐
// Class's ivar size rounded up to a pointer-size boundary.
uint32_t alignedInstanceSize() {
//把没有字节对齐的东西,进行字节对齐 之后,返回出去
return word_align(unalignedInstanceSize());
}
4、接着来到word_align
方法,它是一个字节对齐的算法:表示内存是以8字节为倍数对齐的
(代码里有演算注释)
补充一下:数据存储以字节
为单位,数据传输以位
为单位。一个位
就代表二进制的一个0或者1。每8个位
组成一个字节
。
指针:
在64位下,是8字节(64个位 = 8个字节)。
在32位下,是4字节(32个位 = 4个字节)。
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
/*
WORD_MASK是7,当前对象没有声明属性,只有一个指针,而指针是8字节,所以x=8
(x + WORD_MASK) = 15 二进制: 0000 1111
WORD_MASK = 7 二进制: 0000 0111
~WORD_MASK = ~7 二进制取反: 1111 1000
(x + WORD_MASK) & ~WORD_MASK = 15 & ~7 二进制计算:
0000 1111
&
1111 1000
=
0000 1000 (其实就是8!)
这里~7 是为了补齐8字节, +7 是为了向上取整,拿到8的倍数。
这里再随便举个例子,x=12, x + WORD_MASK = 19 二进制:0001 0011
0001 0011
&
1111 1000
=
0001 0000 (就是16!)
所以说:这个算法,就是拿到以8为倍数的结果!
也就是说:内存是以8字节为倍数对齐的!!!
*/
return (x + WORD_MASK) & ~WORD_MASK;
}
5、最后还有unalignedInstanceSize()
方法,这个方法主要是拿到实例对象data段里面的一些信息,也就是要计算这些信息需要的size,然后进行字节对齐,返回最终的size
// May be unaligned depending on class's ivars.
uint32_t unalignedInstanceSize() {
assert(isRealized());
return data()->ro->instanceSize;
}
小扩展
为什么要以8字节对齐?
举个例子:我有5个属性,分别是3、5、2、4、6个字节,存储在内存当中。但是,当我们去读的时候,就会发现很难受,我怎么知道你第一个属性有几个字节?或者说,我取多少个字节,才能拿到你的第一个属性呢? 即便,我知道了几个字节,第一次查3个字节,读出来。然后再查5个字节,读出来。也是很费时的操作~
于是,CPU就有个“以空间换取时间”的方式,存的时候8字节一段一段地存,读的时候也8字节一段一段地读。这样易读性就提高了很多!
六、总结
1、alloc底层流程是:alloc
-> _objc_rootAlloc
-> callAlloc
-> class_createInstance
-> _class_createInstanceFromZone
进行计算内存大小instanceSize
、开辟空间calloc
、isa把空间指向对象initInstanceIsa
。
2、init底层分析:init
-> _objc_rootInit()
直接返回self
。它的意义实际上是交给子类去自定义重写。
3、new底层分析:它的底层是直接返回[callAlloc() init]
方法。其中callAlloc
和alloc
中是一模一样的,也相当于交给 alloc + init
去做的。
4、内存分配: (64位下)内存中对象的属性
是以8字节为倍数对齐的,最小开辟16字节(大概是为了防止一些越界操作,如果小于16字节的,会预留一些位置)
今天探索alloc的底层流程,还有一些疑虑点,比如:真正的内存对齐原则?calloc里面的实现?isa如何关联对象和类? 这些问题留在下个篇章去探索~