load和initialize方法的调用原则和调用顺序?
- load方法的调用时在dyld加载程序的时候调用,在main函数之前,调用顺序:父类,子类,分类,如果有多个分类,看谁先编译,编译的顺序可以通过
Build Phases -> Compile Sources
设置顺序 - 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
汇编中我们可以看到
[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吗?带着这个疑问我们调试看看?
首先我们断点来到这边
然后开始lldb打印寄存器
x0
地址
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_name
的get
方法,get
方法实现实际就是通过实例对象的指针偏移获取,目前我们只有一个成员变量,所以要获取到hf_name需要偏移8个字节(前面还有个isa指针),所以p指针也会偏移8个字节,但是他是往哪里偏移呢?我们来看一幅图
通过lldb我们可以清楚知道是往高地址偏移
p指针如何往高地址偏移呢?也就是偏移后指向哪里?
我们还是lldb查看一下
lldb看到,person地址就是p地址偏移8字节后的地址。其实也不难理解,
person
,cls
都是栈上的变量,而这边我们也得出栈地址是从高位开始分配。
接下来我们在探究一下如果继续往上偏移打印的会是什么呢?
@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
函数隐含着两个参数:self
和sel
,而这两个参数都是会入栈的,[super viewDidLoad];
这边我们知道实际调用的是objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...),
所以栈里面会有struct objc_super * super
这个参数.
所以刚刚的struct objc_super * super
的值,这个我们可以通过lldb来验证一下
通过lldb我们就可以看到偏移16个字节的时候打印的
---
就是struct objc_super * super
里面的receiver
当我们添加三个成员属性的时候打印的是struct objc_super * super
里面的super_class
。
可能这时候又有一个疑问,ViewController的super_class不是UIViewController吗?
注意:这边评估注释:/* super_class is the first class to search */,super_class是第一个开始查找的类,并没有说是父类,而第一个开始查找的类是当前类。
通过上面分析我们也可以得出,结构体成员变量入栈顺序是从后面往前,也就是越后面的成员越先入栈用一幅图来表示