一道面试题引发的思考

之前接触到了一道面试题目,分析之后觉得这道题目很有意思,考察了很多的底层知识。记录下来以便帮自己整理思路...

有这样的一个简单的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类对象中的方法缓存或方法列表中取出方法,进行调用。

一道面试题引发的思考_第1张图片
Snip20180607_2.png

现在我们再来看面试题中的变量之间的关系:


一道面试题引发的思考_第2张图片
Snip20180607_3.png

cls中存放的是Person类对象的地址,那么功能上等价于instance对象的isa指针。
那么从流程上似乎就可以说的通了,通过一个指针找到isa或存储着类对象地址的指针,再通过它找到Person类对象。

对于计算机来讲没有类或者对象,计算机只需要知道,去哪里读写数据,读取/写入多大的数据。
我一开始有个疑问,因为我们知道从64位CPU开始,isa并不是直接指向类对象,而是要&上一个ISA_MASK值,来获取真正的类对象地址(用33位来存储指向的地址,其余位存储一些其他的信息,如:引用计数,是否关联对象,是否有析构函数等)。那么cls中存储的是类对象的真实地址,所以clsisa功能类似,值不一定相同,那么它们怎么都能找到类对象呢?

测试发现,虽然值不一定相同,但是在objc_msgSend的时候,通过clsisa&ISA_MASK结果是相同的。所以,都可以找到正确的类对象。

以ARM64平台为例:

# if __arm64__
# define ISA_MASK        0x0000000ffffffff8ULL

最后一位为8,换成二进制就是1000,那么假如一个数A&ISA_MASKA & ISA_MASK的作用就是将A中高28位和低3位清零,取出中间33位数来。
A & ISA_MASK & ISA_MASK & ISA_MASK...的结果依然和一次按位与结果相同。

总结一下吧,在面试题中调用- (void)print;方法,转换成底层的objc_msgSend方法的时候,由于和正常时实例对象调用方法流程相同,可以通过cls找到类对象,从而找到- (void)print;方法,那么就可以调用方法成功了。

打印结果为何如此?

Personinstance对象,在内存中的结构:

一道面试题引发的思考_第3张图片
Snip20180607_4.png

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,一个receiversuper_class。这两个成员都放在栈中。

一道面试题引发的思考_第4张图片
Snip20180607_8.png

通过编程经验或者如果懂得汇编代码,可以比较轻松画出上面的栈图。

附上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找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  1. 接收消息的对象(也就是self指向的内容)
  2. 方法选择器(_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]获取超类时,编译器只是将指向selfid指针和classSEL传递给了objc_msgSendSuper函数,因为只有在NSObject类才能找到class方法,然后class方法调用object_getClass(),接着调用objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向selfid指针,与调用[self class]相同,所以我们得到的永远都是self的类型。

一道面试题引发的思考_第5张图片
Snip20180427_10.png

那么最上面那个测试的结果呢?不同就是在类中都重写了- (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

你可能感兴趣的:(一道面试题引发的思考)