1.开篇
首先让我们来看下对应代码:
HPWPerson *p = [HPWPerson alloc];
HPWPerson *p1 = [p init];
HPWPerson *p2 = [p init];
NSLog(@"p = %@",p);
NSLog(@"p1 = %@,p2 = %@",p1,p2);
其对应的输出为
2022-04-18 23:18:41.252721+0800 001--alloc[5857:116065] p = <HPWPerson: 0x6000021bc1b0>
2022-04-18 23:18:41.252772+0800 001--alloc[5857:116065] p1 = <HPWPerson: 0x6000021bc1b0>,p2 = <HPWPerson: 0x6000021bc1b0>
这个时候我们可以发现alloc 后 ,然后将p对象init后的p1和p2值内存地址一样,那alloc是干了啥,以及init是干了啥呢。其实当我们给p1和p2设置成员变量的时候,其输出的值也是一样的。
HPWPerson *p = [HPWPerson alloc];
p.name = @"hpw";
HPWPerson *p1 = [p init];
HPWPerson *p2 = [p init];
NSLog(@"p = %@",p.name);
NSLog(@"p1 = %@,p2 = %@",p1.name,p2.name);
输出如下:
2022-04-18 23:32:56.587929+0800 001--alloc[6185:126956] p = hpw
2022-04-18 23:32:56.587974+0800 001--alloc[6185:126956] p1 = hpw,p2 = how
通过上面我们知道了,用init进行操作的时候是不会开辟内存空间的。那我们继续探究下底层操作,objc的源码在alloc和init里究竟干了啥。
2.alloc方法探索
当我们调试发现当alloc时候,第一步走的下面callAlloc代码:
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
再接着运行会走alloc方法
+ (id)alloc {
return _objc_rootAlloc(self);
}
再运行会走_objc_rootAlloc方法
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
再运行下,奇迹就会发生,我们发现竟然还会走callAlloc这个方法,也就是callAlloc这个方法走了两次,那为什么会走两次callAlloc呢,带着这个问题我们继续去探究。
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
接着我们用汇编调试器进行调试看下对应的原因在哪里。选择 Xcode的Debug里Debug Workflow --> Always Show Disassembly,进行汇编调试,汇编中会用到b和bl指令,这个是个跳转指令,也就是一个函数的调用。ret也就是return的意思,也就是函数的返回,;在汇编里是注释的作用。 当我们运行汇编调试的时候发现断在了如下图:
也就是当我们走alloc方法,其实它是走的是objc_alloc,在其注释里可以发现。然后我们去objc源码里去找下objc_alloc方法的使用,我们发现其在fixupMessageRef方法里使用了,其中sel代表的是方法名,imp代表指向方法的实现。从下面代码里我们可以看到,当方法名为alloc的时候,其会把方法名指向objc_alloc方法,所以这里我们知道了汇编里当我们调用alloc方法后其调用的是objc_alloc方法。
fixupMessageRef(message_ref_t *msg)
{
msg->sel = sel_registerName((const char *)msg->sel);
if (msg->imp == &objc_msgSend_fixup) {
if (msg->sel == @selector(alloc)) {
msg->imp = (IMP)&objc_alloc;
} else if (msg->sel == @selector(allocWithZone:)) {
msg->imp = (IMP)&objc_allocWithZone;
} else if (msg->sel == @selector(retain)) {
msg->imp = (IMP)&objc_retain;
} else if (msg->sel == @selector(release)) {
msg->imp = (IMP)&objc_release;
} else if (msg->sel == @selector(autorelease)) {
msg->imp = (IMP)&objc_autorelease;
} else {
msg->imp = &objc_msgSend_fixedup;
}
}
这样就结束了吗,并没有,我们继续探究,我们添加下objc_alloc这个断点,然后运行跳到如下图位置
我们继续运行,发现个奇怪的现象,它不会走_objc_rootAllocWithZone这个方法,如下图
而是走了下面的objc_msgSend这个方法,这样我们知道了objc_msgSend这个方法是当我们运行alloc方法调用起来的
然后通过) register 也就是寄存器进行读取,寄存器的作用是把参数传递给函数返回,其实通过x0寄存器进行操作的,我们通过
在调试端输入register read x0,就会打印HPWPerson,然后我们再读取下x1寄存器看下,register read x1,这个时候打印的是一串地址,我们需要通过po进行打印以及(char*)强转,打印后我们发现神奇的”alloc“出现了。
(lldb) register read x1
x1 = 0x00000001c9d1789b
(lldb) po (char *)0x00000001c9d1789b
"alloc"
这里也就说明objc_msgSend 这个方法是通过HPWPerson这个类调用alloc方法,然后我们继续探究,打一个[NSObject alloc]断点,我们发现其走到了_objc_rootAlloc这个方法里,然后我们继续调试走到_objc_rootAlloc方法里面。
我们会发现其会走_objc_rootAllocWithZone这个方法,然后我们把这个方法到objc源码里去找下,发现其返回的id类型。也就是通过_class_createInstanceFromZone这个方法把我们的对象创建后返回了,那我们如何去验证呢,我们继续去探究下
id
_objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}
然后在代码中,我们添加一个断点在ret这个汇编里,因为ret是个返回,再通过register read x0去读取这个值,然后再把这个值进行一个po,就会出现一个HPWPerson对象。
0x1801891e0 <+80>: ret
其实在上面运行objc源码时候,调试objc源码时候我们发现其实其走了两次callAlloc方法,调用汇编指令时候没有发现callAlloc方法调用两次,这个是因为编译器起到了一个优化的作用。那为什么会走两次呢,我们在objc源码里进行打断点操作,第一次发现走的是 return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc))语句;
在callAlloc方法里调用了objc_msgSend函数,然后objc_msgSend函数调用了alloc方法。第二次发现走到了callAlloc方法里 return _objc_rootAllocWithZone(cls, nil)语句;
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if __OBJC2__
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
// No shortcuts available.
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
通过上面一系列的操作,我们可以探索到当对象被alloc后,会按如下顺序进行调用:
alloc-> objc_alloc —> callAlloc —> objc_msgSend —> alloc —> _objc_rootAlloc —> callAlloc —>
_objc_rootAllocWithZone —> _class_createInstanceFromZone
3.init方法探索
HPWPerson *p = [HPWPerson alloc];
p.name = @"hpw";
HPWPerson *p1 = [p init];
HPWPerson *p2 = [p init];
NSLog(@"p = %@",p);
NSLog(@"p1 = %@,p2 = %@",p1.name,p2.name);
还是针对这个代码,我们添加一个[NSObject init]这个断点,我们发现只要一个ret的指令,这个也就是汇编指令的返回意思.
我们用register read x0进行读取,然后通过po (char *)把x0打印出会发现是HPWPerson,register read x1进行读取,然后通过po (char *)把x1打印出会发现是init
(lldb) register read x0
x0 = 0x00006000006600f0
(lldb) po (char *)0x00006000006600f0
<HPWPerson: 0x6000006600f0>
(lldb) register read x1
x1 = 0x00000001c9d176dd
(lldb) po (char *)0x00000001c9d176dd
"init"
然后我们继续去探索下源码,源码如下图,我们发现init方法啥都没干,直接返回了obj。既然init方法啥都没干,那为什么要创建init方法呢,其实这个是一个工厂模式,就是让你自己去重写init方法,在init方法里进行一些操作。
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;
}
4.在探索时发现额外知识点补充
1)字节对其
在进行操作的时候,alignedInstanceSize()方法是字节对其算法。同时我们还看到 if (size < 16) size = 16,这样我们知道objc对象它最小的大小是16个字节。字节对其是指用8字节进行对其的。
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
void *bytes;
size_t size;
// Can't create something for nothing
if (!cls) return nil;
// Allocate and initialize
size = cls->alignedInstanceSize() + extraBytes;
// CF requires all objects be at least 16 bytes.
if (size < 16) size = 16;
if (zone) {
bytes = malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
bytes = calloc(1, size);
}
return objc_constructInstance(cls, bytes);
}
苹果里8字节对其源码如下:其实也就是进行一个移位,
(x + 7) >> 3 << 3
当x=8时候,
8+7 = 15换成2进制如下所示
0000 1111
右移3位为 0000 0001
左移3位为 00001000 这里也就是8,所以说苹果是把不满足8的通过这个算法抹掉。
得出结论为:苹果是以8字节为倍数分配内存(8字节对其),且最小为16字节,这样做本质就是以时间换空间。那为什么说是以空间换时间呢,因为苹果存储的时候即使存储对象不满8字节,也按8字节去存储,然后CPU读取的时候就很方便,直接用8字节去读取就可以。但是如果按存储对象实际大小存储,那么这样会有大有小,CPU读取就会很慢,一下4字节读取,一下8字节读取,一下1字节读取......,这样对CPU的速度影响很大,所以用8字节统一去读取就是为了提高CPU的速度,以空间换时间。
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}
1)对象里存放着什么
首先我们打开终端,用clang指令clang -rewrite-objc main.m编译我们main.m,然后会得到main.cpp,如下:
编译后我们在main.cpp里搜索下HPWPerson,发现有个objc_object,所以说对象的本质就是一个objc_object的结构体
typedef struct objc_object HPWerson;
接着我们再看下,有个叫NSObject_IMPL里有个isa,同时还有其成员变量,所以说isa指针和成员变量的值是存放在对象里的(注意这里是成员变量的值不是成员变量的名字),对象的地址是存在栈里面。
struct NSObject_IMPL {
Class isa;
};
struct LGPerson_IMPL {
struct NSObject_IMPL NSObject_IVARS;
int _age;
NSString *_name;
};