面试题分析

load和initialize方法的调用原则和调用顺序?

  1. load方法的调用时在dyld加载程序的时候调用,在main函数之前,调用顺序:父类,子类,分类,如果有多个分类,看谁先编译,编译的顺序可以通过Build Phases -> Compile Sources设置顺序
  2. initialize 类第一次发送消息的时候调用,调用顺序先调用父类的,在调用子类的

Runtime是什么?

Runtime是由C和C++汇编实现的⼀套API,为OC语⾔加⼊了⾯向对象,运⾏时的功能
运⾏时(Runtime)是指将数据类型的确定由编译时推迟到了运⾏时,如类扩展和分类的区别
平时编写的OC代码,在程序运⾏过程中,其实最终会转换成Runtime的C语⾔代码,Runtime 是 Object-C 的幕后⼯作者

super相关面试题

@interface HFPerson : NSObject
@end
@interface HFTeacher : HFPerson
@end
@implementation HFTeacher

- (instancetype)init
{
    self = [super init];
    if (self) {
        NSLog(@"%@---%@", [self class], [super class]);
    }
    return self;
}

@end

打印的结果为
 HFTeacher---HFTeacher

是不是觉得很奇怪,[self class]打印HFTeacher能理解, [super class]就有点不能理解了
我们通过汇编来看看[super class] 底层调用的方法,前面的[self class]调用objc_msgSend方法而[super class]调用的是objc_msgSendSuper2

image.png

汇编中我们可以看到[super class]底层是通过objc_msgSendSuper2来发送消息的\

id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)

struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;  // 消息的接收者

    /// Specifies the particular superclass of the instance to message. 
#if !defined(__cplusplus)  &&  !__OBJC2__
    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
#else
    __unsafe_unretained _Nonnull Class super_class;
#endif
    /* super_class is the first class to search */
};

从上面的代码可以知道objc_msgSendSuper2的接收者在objc_super结构体里的receiver,那这个receiver会是self吗?带着这个疑问我们调试看看?

image.png

首先我们断点来到这边
然后开始lldb打印寄存器x0地址
image.png

x0地址的首地址就是self,也就是说receiver = self,接收对象还是self,也就是通过self去调用父类的方法,然后我们再来看看父类的class方法
我们已知这边的self是实例对象,所以调用的是父类的实例方法也就是

- (Class)class {
    return object_getClass(self);
}
Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

这边的self是HFTeacher实例对象,所以返回的也就是实例对象的isa-> HFTeacher

内存平移

接下来我们在来看一道更有意思的面试题

@interface HFPerson : NSObject

@property (nonatomic, strong) NSString *hf_name;

- (void)saySomething;

@end

@implementation HFPerson
- (void)saySomething{
    NSLog(@"%s - %@",__func__,self.hf_name);
}

- (void)sayHello {
    NSLog(@"123123---%@", self);
}

@end
首先定义一个类HFPerson

- (void)viewDidLoad {
    [super viewDidLoad];
    HFPerson *person = [[HFPerson alloc] init];
    [person sayHello];
    Class cls = [HFPerson class];
    void *p = &cls;
    [(__bridge id)p sayHello];
}

结果输出:
2021-08-03 14:34:35.432347+0800 内存平移[3075:666318] 123123---
2021-08-03 14:34:35.432440+0800 内存平移[3075:666318] 123123---

发现都可以调用到sayHello,接下来我们来分析一下
首先[person sayHello]是实例对象调用方法,能够输出是正常,但是[(__bridge id)p sayHello]为什么也能够调用呢,我们要清楚一点的是,方法是存放在哪里,对象的方法是存放在类里面的,对象能够找到方法是通过对象的首地址isa获取方法。而现在我们Class cls = [HFPerson class] void *p = &cls 这边的p指针指向的就是HFPerson的首地址isa,所以这边也能找到方法并执行调用。
接下来我们变换一下:

- (void)sayHello {
    NSLog(@"%@---%@", self, self.hf_name);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    HFPerson *person = [[HFPerson alloc] init];
    person.hf_name = @"hf";
    [person sayHello];
    Class cls = [HFPerson class];
    void *p = &cls;
    [(__bridge id)p sayHello];
}
输出结果:
2021-08-03 14:46:57.475086+0800 内存平移[3090:669951] ---hf
2021-08-03 14:46:57.475187+0800 内存平移[3090:669951] ---

对于第一条打印结果就不解释了,直接来看看第二条。
首先我们知道p指针指向的是HFPerson对象的isa,所以他也可以通过isa来查找到方法并调用,而这边的hf_name是对象的属性,self.hf_name调用的是hf_nameget方法,get方法实现实际就是通过实例对象的指针偏移获取,目前我们只有一个成员变量,所以要获取到hf_name需要偏移8个字节(前面还有个isa指针),所以p指针也会偏移8个字节,但是他是往哪里偏移呢?我们来看一幅图

image.png

通过lldb我们可以清楚知道是往高地址偏移
p指针如何往高地址偏移呢?也就是偏移后指向哪里?
我们还是lldb查看一下
image.png

lldb看到,person地址就是p地址偏移8字节后的地址。其实也不难理解,personcls都是栈上的变量,而这边我们也得出栈地址是从高位开始分配。
接下来我们在探究一下如果继续往上偏移打印的会是什么呢?

@interface HFPerson : NSObject
@property (nonatomic, strong) NSString *hf_data1;
@property (nonatomic, strong) NSString *hf_name;
@end
输出:
2021-08-03 15:12:11.497457+0800 内存平移[3119:677027] ---

@interface HFPerson : NSObject
@property (nonatomic, strong) NSString *hf_data1;
@property (nonatomic, strong) NSString *hf_data2;
@property (nonatomic, strong) NSString *hf_name;
@end
输出:
2021-08-03 15:12:55.672174+0800 内存平移[3125:677782] ---ViewController

通过添加两个成员属性和三个属性看到打印的结果都不一样,两个成员属性,需要偏移16字节,而这边打印的是三个属性的时候偏移24个字节打印的是ViewController,似乎是个对象,而ViewController似乎是个类名。首先我们要先知道目前栈里面到底有哪些对象。viewDidLoad函数隐含着两个参数:selfsel,而这两个参数都是会入栈的,[super viewDidLoad]; 这边我们知道实际调用的是objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...),所以栈里面会有struct objc_super * super这个参数.
所以刚刚的和ViewController应该是struct objc_super * super的值,这个我们可以通过lldb来验证一下

image.png

通过lldb我们就可以看到偏移16个字节的时候打印的---就是struct objc_super * super里面的receiver当我们添加三个成员属性的时候打印的是struct objc_super * super里面的super_class
可能这时候又有一个疑问,ViewController的super_class不是UIViewController吗?
image.png

注意:这边评估注释:/* super_class is the first class to search */,super_class是第一个开始查找的类,并没有说是父类,而第一个开始查找的类是当前类。
通过上面分析我们也可以得出,结构体成员变量入栈顺序是从后面往前,也就是越后面的成员越先入栈用一幅图来表示
image.png

你可能感兴趣的:(面试题分析)