前言
前面都是各种枯燥乏味的底层,今天整点轻松的探究下经典的面试题
面试题
【1】load
和 initialize
顺序
load
方法调用
在类的加载中已经探究了load
方法的调用顺序,现在做一个总结,在探究load
方法注意点
类的load
方法和分类的load
方法
load
方法调用是是通过load_images
调用。过程中底层有两张表,一张是类的load
方法表,一张是分类的load
方法。类的load
方法先于分类的load
方法- 如果类之间不存在继承关系,那么优先加载的类,其
load
方法先调用。即按照编译顺序进行进行调用 - 如果类之间存在继承关系,那么在
load
方法调用时,底层会先去查找父类的load
方法的递归,然后将其添加到类的load
方法表中。即父类的load
方法先于类中的load
方法调用 - 分类之间的
load
方法调用是按照编译的顺序进行调用 load
方法的调用顺序:父类
>类
>分类
这个顺序是大体的排序,细节可以按照上面的顺序继续细分
initialize
方法调用
initialize
方法是第一次消息发送
的时候调用,也是系统底层主动调用,在消息慢速查找是时进行调用
- 显而易见
initialize
是在load
方法之后调用的 initialize
其实和普通的方法一样,如果主类和分类中的有相同的普通方法只会调用分类的方法- 如果类之间存在继承关系,在
initialize
方法调用底层处理中也是先递归调用父类的initialize
方法 - 如果父类实现了
initialize
方法,类没有实现,那么回去调用父类的initialize
方法 initialize
调用顺序:父类
>分类
或者父类
>类
【2】Runtime
是什么
runtime
是由C
和C++
汇编实现的一套API
,为OC
添加了面前对象,以及运行时的功能- 运行时是将数据类型的确定由编译时推迟到了运行时,比如分类
- 平时编写的
OC
代码,在程序运行时的过程中,其实最终会转换成runtime
的C
语言代码,runtime
是Object—C
的幕后工作者
【3】方法的本质,sel
是什么?IMP
是什么?两者之间的关系是什么?
-
方法的本质:发送消息,消息会有以下几个流程
- 快速查找流程(
objc_msgSend
) ~cache_t
缓存查找 - 慢速查找递归自己和父类 ~
lookUpImpOrForward
- 查到不到消息:动态方法决议~
resolveInstanceMethod
- 消息快速转发 ~
forwardingTargetForSelector
- 消息慢速转发 ~
methodSignatureForSelector & forwardInvocation
- 快速查找流程(
-
sel
是方法编号在read_images
期间就编译进入了内存,sel
也是一个结构体指针类型 -
imp
就是函数实现指针,找imp
过程就是找函数的过程 -
sel
相当于书本的目录,imp
就是书本的页码 -
查找具体的函数就是想看这本书⾥⾯具体篇章的内容
- 我们⾸先知道想看什么 ~
tittle
(sel
) - 根据⽬录对应的⻚码 (
imp
) - 通过页码定位具体的内容,方法实现
- 我们⾸先知道想看什么 ~
【4】能否像编译后的内存中添加实例变量?能否向运行时创建的类中添加实例变量
- 不能像编译后的类中添加实例变量
- 主要类没有注册到内存还是可以添加属性和方法
原因:编译好的实例变量存储在
ro
,一旦编译完成,内存结构就完全确定,无法修改
动态创建类和添加变量和方法
int main(int argc, const char * argv[]) {
@autoreleasepool {
const char * className = "newClass";
Class newClass = objc_getClass(className);
//动态创建类
objc_allocateClassPair(class_getSuperclass(newClass), className, 0);
//添加变量和方法
class_addIvar(newClass, "helloWord", sizeof(NSString *),
log2(_Alignof(NSString *)), @encode(NSString *));
class_addMethod(newClass, @selector(sayHello), (IMP)sayHello, "v@:");
}
return 0;
}
【5】[self class]
和[super class]
的区别以及原理分析
案例分析测试:创建LWSubPerson
类继承LWPerson
代码如下
@implementation LWSubPerson
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"--%@ -- %@--",[self class],[super class]);
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
//调用init方法
LWSubPerson * subPerson = [[LWSubPerson alloc] init];
}
return 0;
}
2021-07-31 19:34:54.186162+0800 KCObjcBuild[7226:272861] --LWSubPerson -- LWSubPerson--
打印结果两个都是LWSubPerson
。[self class]
的结果是LWSubPerson
没有任何问题,[super class]
为什么返回的也是LWSubPerson
。很奇怪,下面来探究下class
方法的源码
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
源码显示 object_getClass(self)
中的self
是- (Class)class
方法的隐藏参数中的一个,这两个隐藏参数是id self
和 SEL _cmd
。那么这个隐藏参数的self
是哪里传过来的呢?方法的本质是消息发送通过objc_msgSend
,在objc_msgSend
方法中有两个参数一个是消息的接收者一个是sel
,现在很清楚了[self class]
的消息接收者是self
也就是LWSubPerson
类的实例化对象所以object_getClass(self)
返回的就是LWSubPerson
。
现在只要搞清楚[super class]
这个消息的接收者是谁那么这个问题就迎刃而解了。通过clang把LWSubPerson.m
文件生成LWSubPerson.cpp
文件
[super class]
方法是通过objc_msgSendSuper
进行消息发,参数的类型是__rw_objc_super
类型和还有一个就是SEL
。其实这里可以简单理解super
的作用其实就是向父类发送消息。那么super
可以说是一个关键字
也可以说是标识符
。在cpp
文件中全局搜索__rw_objc_super
__rw_objc_super
是个结构体类型,里面有两个变量object
和superClass
,此时在结合objc_msgSendSuper
方法的参数,在objc
源码中全局搜索objc_msgSendSuper
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
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_super
结构体类型对应cpp
文件中的__rw_objc_super
其实消息的接收者还是receiver
就是self
所以[super class]
返回的就是LWSubPerson
类,objc_msgSendSuper
的作用就是去父类开始查找方法仅此而已,并不是返回父类。这就是为什么重写init
方法,需要[super init]
而不是[self init]
,[self init]
就会递归死循环
有点人好奇可以不写[super init]
答案是可以,但是如果不写就不能继承父类的属性和方法,父类的一些自带的就不能用所以最好写[super init]
下面通过汇编在验证下
明明cpp
文件是objc_msgSendSuper
,汇编是调用的objc_msgSendSuper2
这是什么原因。全局搜索objc_msgSendSuper
汇编中发现其实objc_msgSendSuper
汇编也是调用了objc_msgSendSuper2
的核心实现,而如果直接调用objc_msgSendSuper2
方法会发现objc_msgSendSuper2
的参数super_class
其实是class
,只不过底层自己调用了superClass
,全局搜索objc_msgSendSuper2
objc_msgSendSuper2
注释中发现里面的super_class
是传的class
而不是父类。实际上objc_msgSendSuper
和objc_msgSendSuper2
结果都是一样就是从父类开始找,这样的好处就是可以防止递归,还有就是明确告诉这个方法是我父类的,你不用在从你本类开始查找减少一步查找流程更快捷
【6】内存平移案例分析
内存平移问题,在我看来就是理解的问题,如果理解了其实就很简单。现在直接上案列
//ViewController类的实现
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LWPerson * p = [LWPerson alloc];
p.name = @"helloWord";
[p sayHello];
Class cls = [LWPerson class];
void * lw_p = &cls;
[(__bridge id)lw_p sayHello];
}
@end
//LWPerson类的实现
@implementation LWPerson
-(void)sayHello{
NSLog(@"--%s--%@--",__func__,self.name);
}
@end
代码中出现两个问题
[p sayHello]
这个方法可以调用,大家可以理解。但是[(__bridge id)lw_p sayHello]
可以调用原理是什么?[(__bridge id)lw_p sayHello]
调用以后打印的结果是什么?
[(__bridge id)lw_p sayHello]
调用
探究[(__bridge id)lw_p sayHello]
可以调用,首先要搞清楚&cls
的含义是什么。我们知道sayHello
是普通的实例方法,一般情况下是对象调用,所以&cls
应该也是一个类似对象的类型。其中p
的类型是LWPerson *
类型,那么&cls
应该也是LWPerson *
类型才可以,不然没法调用sayHello
方法,下面进行源码调试下
图中的结果显示:&cls
是Class *
类型,而在代码中的Class
就是LWPerson
,所以Class *
类型就是LWPerson *
。其实理解起来也很简单cls
里面是LWperson
类,现在找到一个指针变量指向cls
,怎么指向cls
呢?那么指针变量里面必须存储的是cls
地址即&cls
,而这个指针变量就是代码中的lw_p
。而lw_p
和p
的区别,p
开辟了一块内存用来存储数据,而lw_p
只是一个指针
类中消息查找图
图中可以看出实例对象是通过isa
去找到类
,然后在类中进行方法查找,而lw_p
也是一个指针,这个指针指向了cls
,而cls
里面存放的是LWPerson
类的地址,到最后都找打了LWPerson
类
[(__bridge id)lw_p sayHello]
打印结果
图中显示[p sayHello]
的调用结果就是name
的真实值,而[(__bridge id)lw_p sayHello]
的调用结果是一个指针地址,我们知道实例对象中的变量的值都是存储在实力对象的内存中正常情况下是通过内存偏移来获取对应的值
变量的值是按照编译的先后顺序进行存储,name
的地址就是实例对象的首地址
+0x8
。那么 也会按照相同方式去获取对应地址里面的值
因为lw_p
是没有开辟内存的,所以它会首先找到指针cls
,获取cls
的地址然后加上0x8
既首地址
+0x8
,然后就找到了p
。在栈空间中地址是高地址到低地址即先进后出,先进入栈中的地址是高于后进入栈中的地址,所以最后打印的是指针p
。其实现在回头看内存平移很简单,只要弄清楚原理,一切都不是问题
只要大家搞清楚堆栈进入的顺序,那么这道面试题就很简单,现在补充两个类型的压栈顺序
结构体压栈顺序
压栈顺序就是看谁的地址大,地址大的先压栈
lldb
调试很明显可以看出&p
和&numStruct
地址相差16
个字节,因为numStruct
有两个指针类型,而指针类型大小字节是8
字节。&numFirst
地址小于&numLast
,说明numLast
先入栈。从lldb
调试结果得出两个结论
- 先入栈的地址大于后入栈的地址
- 在结构体中后面的变量先入栈
[super viewDidLoad]
在上面探究过,实际上是向父类发送消息,父类的第一个参数是结构体。就像上面的例子一样先创建结构体,然后在去发送消息。注意的向父类发送消失是通过objc_msgSendSuper2
方法,而objc_msgSendSuper2
第一个参数的结构体中的第二个变量的super_class
传的是类而不是父类
函数参数压栈顺序
函数的参数也会保存在当前的栈区,目的是在函数的范围内供其使用。探究下函数参数的压栈顺序
OC
的方法都会默认有两个参数id self
,SEL _cmd
。所以用C
函数来做测试,避免默认参数的问题
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
testFunc(10, 20, 30);
}
void testFunc(int a, int b,int c){
printf("----%p---\n",&a);
printf("----%p---\n",&b);
printf("----%p---\n",&c);
}
@end
打印的结果显示函数的参数是按照参数的正序进行压栈的,参数位置靠前的进行先压栈。 这样一个一个打印很费时间,下面提供一个方法打印入栈的地址大小
void *sp = (void *)&self;// 开始的地址
void *end = (void *)&lw_p;// 结束的地址
long count = (sp - end) / 0x8;// 中间的地址
for (long i = 0; i<count; i++) {
void *address = sp - 0x8 * i;
if ( i == 1) {
NSLog(@"%p : %s",address, *(char **)address);
}else{
NSLog(@"%p : %@",address, *(void **)address);
}
}
打印的结果和上面的探究的结果是一样的,打印的顺序图中已经标明,有兴趣的可以继续给
LWPerson
添加属性会出现不同的结果,但是原理还是不会改变的
总结
这种面试题探究起来就很得劲,不过不知道的就是一脸懵逼。我就想问到底谁出的这种面试题啊,不是难为人嘛。继续学习,面试题也会不断进行更新