之前接触到了一道面试题目,分析之后觉得这道题目很有意思,考察了很多的底层知识。记录下来以便帮自己整理思路...
有这样的一个简单的Person
类:
// ------ .h中
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
// ------ .m中
@implementation Person
- (void)print{
NSLog(@"---self.name is---%@------",self.name);
}
@end
然后在ViewController
中是这样子的:
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
问题就是:- (void)print;
方法是否可以调用,如果可以调用,打印结果是什么?
运行一下程序,查看打印结果:
---self.name is---------
当我看到打印结果的时候是一脸懵逼状态,我猜对了可以调用- (void)print;
方法,但是却没有猜对打印的结果。
那么下面我们就一步步去分析,为何可以调用方法以及打印结果是这样的。
为何可以调用?
平时我们调用方法的时候是这个样子的:
Person *person = [[Person alloc] init];
[person print];
这两句代码做了什么呢?我是这样理解的:在函数栈内存放了一个叫person
的指针,指针里的存放的是Person类
的一个instance
实例对象在堆空间中的内存地址。调用- (void)print;
方法是通过person
指针找到instance
对象,再通过instance
对象的isa
指针找到到Person
类对象。从Person
类对象中的方法缓存或方法列表中取出方法,进行调用。
现在我们再来看面试题中的变量之间的关系:
cls
中存放的是Person
类对象的地址,那么功能上等价于instance
对象的isa
指针。
那么从流程上似乎就可以说的通了,通过一个指针找到isa
或存储着类对象地址的指针,再通过它找到Person
类对象。
对于计算机来讲没有类或者对象,计算机只需要知道,去哪里读写数据,读取/写入多大的数据。
我一开始有个疑问,因为我们知道从64位CPU开始,isa
并不是直接指向类对象,而是要&
上一个ISA_MASK
值,来获取真正的类对象地址(用33位来存储指向的地址,其余位存储一些其他的信息,如:引用计数,是否关联对象,是否有析构函数等)。那么cls
中存储的是类对象的真实地址,所以cls
和isa
功能类似,值不一定相同,那么它们怎么都能找到类对象呢?
测试发现,虽然值不一定相同,但是在objc_msgSend
的时候,通过cls
和isa
来&ISA_MASK
结果是相同的。所以,都可以找到正确的类对象。
以ARM64平台为例:
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
最后一位为8
,换成二进制就是1000
,那么假如一个数A
来&
上ISA_MASK
,A & ISA_MASK
的作用就是将A
中高28位和低3位清零,取出中间33位数来。
A & ISA_MASK & ISA_MASK & ISA_MASK...
的结果依然和一次按位与结果相同。
总结一下吧,在面试题中调用- (void)print;
方法,转换成底层的objc_msgSend
方法的时候,由于和正常时实例对象调用方法流程相同,可以通过cls
找到类对象,从而找到- (void)print;
方法,那么就可以调用方法成功了。
打印结果为何如此?
Person
的instance
对象,在内存中的结构:
isa
是个指针,在64位处理器中,指针占用8个字节。那么访问成员变量_name
的时候,就是在isa
地址+8个字节就可以访问到_name
了。
那么在这道面试题中,方法调用者是cls
,而cls
是在函数栈中,那么我们就要分析函数调用栈。
随之这里又考察了一个点,super
关键字的理解。
struct objc_super {
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
super
关键字在底层会生成一个结构体,结构体两个成员,一个receiver
,一个receiver
的super_class
。这两个成员都放在栈中。
通过编程经验或者如果懂得汇编代码,可以比较轻松画出上面的栈图。
附上debug模式下,编译器生成的汇编代码(release模式下编译器会做优化,产生的汇编代码不同):
TEST`-[ViewController viewDidLoad]:
0x100352524 <+0>: sub sp, sp, #0x40 ; =0x40 // 提升sp
0x100352528 <+4>: stp x29, x30, [sp, #0x30] // 保护x29,x30寄存器
0x10035252c <+8>: add x29, sp, #0x30 ; =0x30 // 提升fp(x29)
0x100352530 <+12>: add x8, sp, #0x10 ; =0x10
0x100352534 <+16>: adrp x9, 2
0x100352538 <+20>: add x9, x9, #0xdf8 ; =0xdf8
0x10035253c <+24>: adrp x10, 2
0x100352540 <+28>: add x10, x10, #0xe30 ; =0xe30 //猜测:应该是查找UIViewCOntroller类
0x100352544 <+32>: stur x0, [x29, #-0x8] //在栈中存self
0x100352548 <+36>: stur x1, [x29, #-0x10] //在栈中存方法viewDidLoad
0x10035254c <+40>: ldur x0, [x29, #-0x8]
0x100352550 <+44>: str x0, [sp, #0x10] //在栈中再存入一个self
0x100352554 <+48>: ldr x10, [x10]
0x100352558 <+52>: str x10, [sp, #0x18] //将UIViewCOntroller存入栈中
0x10035255c <+56>: ldr x1, [x9]
0x100352560 <+60>: mov x0, x8
0x100352564 <+64>: bl 0x100352b00 ; symbol stub for: objc_msgSendSuper2 //调用objc_msgSendSuper2方法
0x100352568 <+68>: adrp x8, 2
0x10035256c <+72>: add x8, x8, #0xe00 ; =0xe00
0x100352570 <+76>: adrp x9, 2
0x100352574 <+80>: add x9, x9, #0xe20 ; =0xe20
0x100352578 <+84>: ldr x9, [x9]
0x10035257c <+88>: ldr x1, [x8]
0x100352580 <+92>: mov x0, x9
0x100352584 <+96>: bl 0x100352af4 ; symbol stub for: objc_msgSend
0x100352588 <+100>: mov x29, x29
0x10035258c <+104>: bl 0x100352b18 ; symbol stub for: objc_retainAutoreleasedReturnValue
0x100352590 <+108>: adrp x8, 2
0x100352594 <+112>: add x8, x8, #0xe08 ; =0xe08
0x100352598 <+116>: add x9, sp, #0x8 ; =0x8
0x10035259c <+120>: str x0, [sp, #0x8] //将Person类对象地址放入栈中
0x1003525a0 <+124>: str x9, [sp] // 将cls的地址放入栈中
0x1003525a4 <+128>: ldr x0, [sp]
0x1003525a8 <+132>: ldr x1, [x8]
0x1003525ac <+136>: bl 0x100352af4 ; symbol stub for: objc_msgSend
0x1003525b0 <+140>: add x0, sp, #0x8 ; =0x8
0x1003525b4 <+144>: mov x8, #0x0
-> 0x1003525b8 <+148>: mov x1, x8
0x1003525bc <+152>: bl 0x100352b30 ; symbol stub for: objc_storeStrong
0x1003525c0 <+156>: ldp x29, x30, [sp, #0x30]
0x1003525c4 <+160>: add sp, sp, #0x40 ; =0x40
0x1003525c8 <+164>: ret
这样在访问_name
的时候,其实是访问栈空间内的cls
地址值+8个字节的地址里存放的内容,就是viewController
对象了。
可能有的同学还是对super
关键字不是很理解,没事,下面这道题目分析一下:
self和super的测试
FGObject1:
@interface FGObject1 : NSObject
@end
@implementation FGObject1
- (Class)class
{
return [NSObject class];
}
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"FGObject1 --- %@ --- %@",[self class],[super class]);
}
return self;
}
@end
FGObject2:
@interface FGObject2 : FGObject1
@end
@implementation FGObject2
- (Class)class
{
return [UIView class];
}
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"FGObject2 --- %@ --- %@",[self class],[super class]);
}
return self;
}
@end
当分别创建它们两个不同的对象的时候,控制台如何输出?
FGObject1 *object1 = [[FGObject1 alloc] init];
FGObject2 *object2 = [[FGObject2 alloc] init];
输出结果:
2018-04-27 ARCStudy[80422:11252273] FGObject1 --- NSObject --- FGObject1
2018-04-27 ARCStudy[80422:11252273] FGObject1 --- UIView --- FGObject2
2018-04-27 ARCStudy[80422:11252273] FGObject2 --- UIView --- NSObject
那么结果你答对了吗?
方法中的隐藏参数
我们经常在方法中使用self
关键字来引用实例本身,但从没有想过为什么self
就能取到调用当前方法的对象吧。其实self
的内容是在方法运行时被偷偷的动态传入的。
当objc_msgSend
找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
- 接收消息的对象(也就是
self
指向的内容) - 方法选择器(
_cmd
指向的内容)
之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。
在这两个参数中,self
更有用。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
而当方法中的super
关键字接收到消息时,编译器会创建一个objc_super
结构体:
struct objc_super {id receiver; Class class;};
/**
* Sends a message with a simple return value to the superclass of an instance of a class.
*
* @param super A pointer to an \c objc_super data structure. Pass values identifying the
* context the message was sent to, including the instance of the class that is to receive the
* message and the superclass at which to start searching for the method implementation.
* @param op A pointer of type SEL. Pass the selector of the method that will handle the message.
* @param ...
* A variable argument list containing the arguments to the method.
*
* @return The return value of the method identified by \e op.
*
* @see objc_msgSend
*/
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
附录苹果官方对objc_msgSendSuper
的注释,写的比较清楚,receive
是类的instance
对象,而查找方法IMP
时是从superclass
来查找的。
这个结构体指明了消息应该被传递给特定超类的定义。但receiver
仍然是self
本身,这点需要注意。
如果我们这样来写:
- (instancetype)init
{
if (self = [super init]) {
NSLog(@"FGObject1 --- %@ --- %@",[self class],[super class]);
}
return self;
}
打印结果:
2018-04-27 ARCStudy[80476:11297894] FGObject1 --- FGObject1 --- FGObject1
因为当我们想通过[super class]
获取超类时,编译器只是将指向self
的id
指针和class
的SEL
传递给了objc_msgSendSuper
函数,因为只有在NSObject
类才能找到class
方法,然后class
方法调用object_getClass()
,接着调用objc_msgSend(objc_super->receiver, @selector(class))
,传入的第一个参数是指向self
的id
指针,与调用[self class]
相同,所以我们得到的永远都是self
的类型。
那么最上面那个测试的结果呢?不同就是在类中都重写了- (Class)class;
方法。
FGObject1初始化时:
-
[self class]
:调用自己重写的方法,返回NSObject
。 -
[super class]
:给父类NSObject
发送消息,那么就是去查找实例对象isa指针指向的类,所以结果是FGObject1
。
FGObject2初始化时:
-
[super init]
中[self class]
:会调用FGObject2
的- (Class)class;
方法,打印UIView
。 -
[super init]
中[super class]
:给父类NSObject
发送消息,那么就是去查找实例对象isa
指针指向的类,所以结果是FGObject2
。 - 轮到自己的
[self class]
时,调用自己的- (Class)class;
方法,打印UIView
。 - 轮到自己的
[super class]
时,调用FGObject1
的- (Class)class;
方法,打印的是NSObject
。