runtime的学习整理
对象
类
消息
应用程序加载、类、分类初始化
相关面试题
1. load
和initialize
方法的调用原则和调用顺序?
-
load
方法-
load
方法在应用程序加载过程中(dyld
)完成调用,在main
函数之前
- 在底层进行
load_images
处理时,维护了两个load
加载表,一个类
的表,另一个为分类
的表,优先对类的load
方法发起调用 - 在对类
load
方法进行处理时,进行了递归处理
,以确保父类优先
被处理 - 所以
load
方法的调用顺序为父类
、子类
、分类
- 而
分类
中load
方法的调用顺序根据编译顺序
为准
-
-
initialize
方法-
initialize
在第一次消息发送
的时候调用,所以load
先于initialize
调用 - 分类的⽅法是在类
realize
之后attach
进去的插在前⾯,所以如果分类中实现了initialize
方法,会优先调⽤分类的initialize
方法 -
initialize
内部实现原理是消息发送
,所以如果子类没有实现initialize
会调用父类的initialize
方法,并且会调用两次
- 因为内部同时使用了
递归
,所以如果子类
和父类
都实现了initialize
方法,那么会优先
调用父类
的,再调用子类
的
-
具体的实现以及底层逻辑在类的加载(上)-- _objc_init&read_images
中。
补充c++
构造函数
- 在分析
dyld
之后,可以确定这样的一个调用顺序,load->c++->main
函数 - 但是如果
c++
写在objc
工程中,在objc_init()
调用时,会通过static_init()
方法优先调用c++
函数,而不需要等到_dyld_objc_notify_register
向dyld
注册load_images
之后再调用 - 同时,如果
objc_init()
自启的话也不需要dyld
进行启动,也可能会发生c++
函数在load方法之前
调用的情况
2.Runtime
是什么?
-
Runtime
是由C
和C++
汇编实现的⼀套API
,为OC
语⾔加⼊了⾯向对象
,运⾏时
的功能 - 运⾏时
(Runtime
)是指将数据类型的确定由编译时
推迟到了运⾏时
,如类扩展
和分类
的区别 - 平时编写的
OC
代码,在程序运⾏过程中,其实最终会转换成Runtime
的C
语⾔代码,Runtime
是Object-C
的幕后⼯作者
3.⽅法的本质,sel
是什么?IMP
是什么?两者之间的关系⼜是什么?
- ⽅法的本质:
发送消息
,消息会有以下⼏个流程:-
快速查找
(objc_msgSend
)~cache_t
缓存消息 -
慢速查找
~ 递归⾃⼰或⽗类 ~lookUpImpOrForward
-
查找不到消息
:动态⽅法解析
~resolveInstanceMethod
-
消息快速转发
~forwardingTargetForSelector
-
消息慢速转发
~methodSignatureForSelector
和forwardInvocation
-
-
sel
是⽅法编号
,在read_images
期间就编译进⼊了内存
-
sel
的内存结构:typedef struct objc_selector *SEL
;
-
-
imp
就是我们函数实现指针
,找imp
就是找函数的过程 -
sel
就相当于书本的⽬录tittle
,imp
就是书本的⻚码
4.能否向编译后
的得到的类中增加实例变量?能否向运⾏时创建的类中添加实例变量?
-
不能
向编译后的得到的类中增加实例变量- 编译好的实例变量存储的位置在
ro
,⼀旦编译完成,内存结构就完全确定
; - 可以通过
分类
向类中添加方法
和属性
(关联对象
)
- 编译好的实例变量存储的位置在
-
可以
向运行时
创建的类中添加实例变量,只要内没有注册到内存
还是可以添加- 可以通过
objc_allocateClassPair
在运行时创建类,并向其中添加成员变量和属性,见下面代码:
- 可以通过
// 使用objc_allocateClassPair创建一个类Class
const char * className = "SelClass";
Class SelfClass = objc_getClass(className);
if (!SelfClass){
Class superClass = [NSObject class];
SelfClass = objc_allocateClassPair(superClass, className, 0);
}
// 使用class_addIvar添加一个成员变量
BOOL isSuccess = class_addIvar(SelfClass, "name", sizeof(NSString *), log2(_Alignof(NSString *)), @encode(NSString *));
class_addMethod(SelfClass, @selector(addMethodForMyClass:), (IMP)addMethodForMyClass, "V@:");
5.[self class]
和[super class]
区别和解析?
通过代码案例分析
这个问题,首先创建LGTeacher
继承LGPerson
,并在LGTeacher
的init
初始化方法中,调用了[self class]和[super class]
,查看输出结果。
// LGPerson
@interface LGPerson : NSObject
@end
@implementation LGPerson
@end
// LGTeacher
@interface LGTeacher : LGPerson
@end
@implementation LGTeacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@", [self class], [super class]);
}
return self;
}
@end
案例分析:
很清楚LGTeaher
与LGPerson
都没有实现class
方法,那么根据消息发送的原理,他们最终都会调用到NSObject
的实例方法class,该方法实现如下:
- (Class)class {
return object_getClass(self);
}
调用方法的本质是发送消息objc_msgSend
,并且有两个隐藏参数
,分别是id self
和SEL sel
,这里的隐藏参数self
就是我们要分析的类型。
-
[self class]
输出是LGTeacher
,这个没有什么问题!因为消息的发送者是LGTeacher
对象,通过消息发送机制,找到NSObejct
并调用class
方法,但是消息的接受者没有发生改变,依然是LGTeacher
对象! -
[super class]
就不一样了,同过xcrun
查看main.cpp
文件,查看底层源码得出以下:
super
关键字,在底层最终使用了objc_msgSendSuper
方法,同时其接受者是(id)self
,全局搜搜objc_msgSendSuper
的逻辑,见下图:
根据 Objc-818.2源码查看objc_super
如下:
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 */
};
可以看到结构体中只有两个参数,分别是id receiver
和Class super_class
,其中super_class
表示第一个要去查找的类,至此我们可以得出结论,在LGTeacher
中调用[super class]
,其内部会调用objc_msgSendSuper
方法,并且会传入参数objc_super
,其中receiver
是LGTeacher
对象,super_class
是LGTeacher
类通过class_getsuperclass
获取的父类,也就是要第一个
查找的类。
通过下符号断点--objc_msgSendSuper2
,查看寄存器,其中第一个地址为发放的第一个隐藏
参数,也就是objc_super
,通过类型强制,该结构体封装的recevier
是LGTeacher
,super_class
是LGPerson
,具体看下图:
得出结论:
[super class]
的接收者依然是LGTeacher
对象,去调用父类
的方法。
最后查看运行结果:
果然输出都是
LGTeacher!!!
补充:objc_msgSendSuper
为什么会调用到了objc_msgSendSuper2
?
通过 Objc-818.2源码查看的出:
全局搜索
objc_msgSendSuper
,进入汇编实现流程中,在汇编流程中,最终会调用objc_msgSendSuper2
,见下图:注意:这题还不够明白的话建议参考以上的消息相关文章,写得比较详细哦。
5.指针平移和消息发送原理案例分析
LGPerson
类有一个实例方法saySomething
,在viewDidLoad
中通过两种
方式调用该方法,一种是通过创建LGPerson
对象调用,另一种是通过桥接调用
,见下面代码:
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [LGPerson alloc];
[person saySomething];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s - %@",__func__);
}
@end
问题1:是否能够调用成功?
- 方法调用的本质是
发送消息
,通过对象的isa
找到类
地址,进行地址平移,通过sel
找到对应的方法实现imp
毋庸置疑,
person saySomething];
此种方式肯定是没问题的
通过person
对象的isa
指针找到对应的类,在类中进行地址平移
,首先在
cache_t
中快速查找
,如果找不到,则在方法列表
以及父类的方法列表
中查找,总结一下就是:以类的地址作为入口,进行地址平移,最终找到对应的imp
。[(__bridge id)kc saySomething];
是否可以呢?
首先Class cls = [LGPerson class];
,cls
是什么?cls
是一个指针,Class
的定义是一个指针,指向一个objc_class
的指针,这里就是指向LGPerson
类。将cls的地址赋值给kc
,此时kc
为cls
的地址,也指向了类
。
分析得出:两者调用的入口是一致
的,从同一个地址开始进行方法查找流程,肯定是可以调用
到的,person
除了有地址,还有内存数据结构
;kc
只有一个地址,是一个伪装的person
对象。请看下图:
通过lldb
调试可以发现,kc
指向类,见下图:
最后运行代码:
案例扩展
在LGPerson
中添加一个属性kc_name
,实现代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
LGPerson *person = [LGPerson alloc];
person.kc_name = @"name123";
[person saySomething];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
}
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *kc_name;
- (void)saySomething;
@end
@implementation LGPerson
- (void)saySomething{
NSLog(@"%s - %@", __func__, self.kc_name);
}
@end
那么这样子的输出结果优势怎么样子呢?是不是跟上面的一样都能输出呢?哈哈,以下继续进行lldb
调试,请继续走!
经过调试可以知道
person
进行地址平移
获取属性kc_name
,此数据结构是在堆
中,而kc
只是一个地址,获取kc
数据结构只是输出了其在栈
中的数据信息。
引申出压栈的概念
通过上面的案例分析,可以知道根本原因是栈中地址平移
的问题,那么在程序运行过程中,压栈逻辑是怎样的呢?先入后出,这个比较清楚,那结构体是如何压栈
的呢,函数调用中参数的压栈逻辑
又是怎样的?
-
压栈
,地址从大到小,先进去的地址大(栈开辟由高地址到低地址
)
- 添加
结构体
,查看栈中的地址
添加完结构体后,通过lldb的出下图:
明显看出结构体占用了16
字节,那么结构体内容在栈中的位置是怎么样子的呢?继续进行lldb
调试:
通过lldb
输出结构体中两个属性的地址,发现,num1
在num2
的上面,所以在压栈过程中,按照下图中的方式进行的:
函数参数压栈顺序
通过下面的案例代码进行进一步探索:
有上面可以得出:
-
viewDidLoad
方法中person
指针的地址和kcFunction
中person
指针地址是不一样
的,虽然他们都执行了同一片堆区
- 根据指针的地址发现,参数在压栈时是根据
参数的顺序进行
的,第一个参数先入栈,然后依次压栈
补充:runtime面试题持续更新中哦。。。敬请期待!