一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天 点击查看活动详情。
Runtime这个词相信大家不陌生了把,和Runloop一样,属于面试必备的知识点(考不考不知道,但是当面试官问起来,一定要能答的出),再加上Runytime的知识点确实比较复杂和繁琐,就造成了面试时,很多人只能答出Runtime的基本使用(例如method-swizzing)和简单概述,不能深入的理解其原理和内部构造,给面试官一种基础知识不牢固的感觉(就是卷),所以这篇文章来给大家深入说明一下,当面试官问Runtime时,最想听的答案是什么。
初级(0-3年)
作为初级iOSer,在这个时间段里,应该掌握的是runtime的基本概念
1、runtime 是什么
runtime是一套主要由C语言编写的api,在程序运行过程中都是转换成了runtime的C语言代码。
2、runtime 可以干嘛
runtime的主要功能是消息、方法的传递与转发
以上这两个解释都是比较笼统的归纳和概括,可能会觉得比较泛,但是浓缩就是精华嘛,看到后面就会明白的。
中级(3-6年)
这个阶段的iOSer,就应该要去掌握数据结构相关的知识了。
首先我们来看看一个类的数据结构(不是runtime么? 别急,一步一步来)
1、class的数据结构
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。
结构体类型objc_class继承自objc_object,其中objc_object也是一个结构体,且有一个isa属性,
所以objc_class也拥有了isa属性
所以说OC中所有继承自NSObject的类都有isa指针
上面的说明应该能很好的解释了objc_object
、objc_class
和class
的关系,我们现在就一个一个来说
1)objc_object的数据结构
在这个结构体里面 主要的就是有一个 isa 指针,同时 objc_class
又继承自 objc_object
,所以 objc_class
也有了 isa 指针。
2)objc_class的数据结构
在OC语言发展的过程中,其实有两种objc_class结构,一个是 OBJC1
(已过期),还有一个是OBJC2
(现役)
OBJC1
:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
这里为什么要说明一下呢,因为有些面试题会考你 objc_ivar_list
或 objc_method_list
,这个是已经过期了,没有用。如果想了解的可以去看看别的资料,这里就不细说了。
OBJC2
:
(图是网上找的)
这里来重点说下OBJC2
下的objc_class
- superClass:是一个class类,同样是一个指向objc_class的结构体,有isa指针,用于找到父类
- cache_t:是一个包裹者
bucket_t
的数组。bucket_t
里包括了SEL和IMP,当需要查找某个方法时,是会先将SEL作为参数,通过哈希算法(与运算)找到SEL对应的bucket_t的下标,最后获取到IMP(函数对应的地址) - Class_data_bits_t:是对
class_rw_t
的封装 - class_rw_t:代表了类的读写信息,包括
class_ro_t
、methods
(函数)、properties
(属性)、protocols
(协议),是对class_ro_t
的封装。其中methods
(函数)、properties
(属性)、protocols
(协议)都是继承自list_array_tt
,是个二维数组,每给一个类添加分类时,都会被装入这个二维数组中。 - class_ro_t:代表了类相关的只读信息,包括
name
、methodList
、ivars
、properties
、protocols
等,这些表示在储存了当前类在编译期就已经确定的属性、方法以及遵循的协议
这里用一个图来展现
在obj_class里面其实考点也比较多,在这个阶段,我们可以先了解其数据结构,知道是如何组成的.
最后 我们看看总体的结构图
2、指针指向
通过上面的数据结构可以发现,每一个class,都有一个isa指针,这个是用来干嘛的呢?,最主要的就是通过isa指针进行消息、方法的传递!当你要进行消息、方法的传递时,就必须要找到消息、方法出自哪个类,这就是isa指针的作用。
下面这个图就可以明确表示出来
实例的isa指针指向class
class的isa指针指向metaclass(元类)
元类的指针指向根原类
根原类的指针指向自己
根原类的父类指向NSObject
知道了这个以后,我们在来看看,是如何进行消息、方法的传递的
3、消息、方法的传递流程
我们在看看下张图
结合一个实例man为例子来说明一下
person继承自NSObject
person *man = [person new]
- 先通过man的isa指针找到person类
- 从person类的cache_t数组中通过HASH算法去查找是否有方法
- 找到了结束,找不到在去找
class_data_bits_t
—>class_rw_t
— >class_ro_t
— >methodList
中找(有序用二分法,无序用一般查找) - 找到了就结束,找不到再去
class_data_bits_t
—>class_rw_t
— >class_ro_t
中的methods分类中去查找。 - 找到了就结束,找不到就通过superClass的isa指针去NSObject中的cache_t数组中找
- 循环至第2步
- 找到了就结束,找不到就去消息转发流程
流程其实不算特别复杂,就是自己这找不到,就去爸爸那找,爸爸那找不到,就去爷爷那找。但是其中关键的就是查找的方法,比如在 methodList
中,有序用二分法,无序用一般查找,以及 cache_t 中用HASH 查找,这个就是关键。如果有兴趣,可以去看看如何用HASH查找。
4、消息、方法的转发流程
unrecognized selector sent to instance 0x100524a90
这个问题应该大家都见过把,就是系统找不到对应的消息、方法时的经典报错,所以,我们来看看报错前,系统都干嘛了。
当消息和方法在子类、父类和根类都找不到时,就会进入消息转发流程,下面上图
这几个方法就是OC给我们防止crash的机会,如果当消息无法处理时,就真的要crash了
///当调用一个不存在的类方法时调用
+ (BOOL)resolveClassMethod:(SEL)sel;
///当调用一个不存在的实例方法时调用
+ (BOOL)resolveInstanceMethod:(SEL)sel;
///将这个不存在的方法重定向到其他类进行处理,返回一个类的实例
- (id)forwardingTargetForSelector:(SEL)aSelector;
//返回适当的方法签名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
///将这个不存在的方法打包成NSInvocation丢进来,需要调用invokeWithTarget:给某个能执行方法的实例
- (void)forwardInvocation:(NSInvocation *)anInvocation;
先说说这两个函数
///当调用一个不存在的类方法时调用
+ (BOOL)resolveClassMethod:(SEL)sel;
///当调用一个不存在的实例方法时调用
+ (BOOL)resolveInstanceMethod:(SEL)sel;
首先 这两个函数是放在 _class_resolveMethod
函数里 看下面代码
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
流程就是
- 方法不存在元类时,调用
_class_resolveInstanceMethod
- 方法存在元类时,调用
_class_resolveClassMethod
- 调用完
_class_resolveClassMethod
后,又查找方法流程,如果!nil,再调用一次_class_resolveInstanceMethod
而_class_resolveMethod
在什么时候使用呢? 看下图
就是消息发送后,经过一系列查找都没结果时,会进入_class_resolveMethod
所以当我们在一个类中去重写这两个方法时,返回YES 就相当于告诉系统,我已经处理了,你不用管了。
在说说这个函数
///将这个不存在的方法重定向到其他类进行处理,返回一个类的实例
- (id)forwardingTargetForSelector:(SEL)aSelector;
通俗的讲就是换一个对象来接受消息
注意:返回的对象将用作新的接收者对象,消息分派将继续到这个新对象。(显然,如果从这个方法返回self,代码将陷入无限循环。)
看下面的例子
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(work)) {
return [Person alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
最后来说说下面两个函数
//返回适当的方法签名。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
///将这个不存在的方法打包成NSInvocation丢进来,需要调用invokeWithTarget:给某个能执行方法的实例
- (void)forwardInvocation:(NSInvocation *)anInvocation;
为什么要一起说呢,因为如果如果forwardingTargetForSelector
未处理,则交给开销大得多的forwardInvocation
接管。而forwardInvocation
这个方法呢 需要需要同步重写methodSignatureForSelector
方法来给指定的选择器提供适当的方法签名。
To respond to methods that your object does not itself recognize,
you must override methodSignatureForSelector: in addition to
forwardInvocation:. The mechanism for forwarding messages uses
information obtained from methodSignatureForSelector: to create
the NSInvocation object to be forwarded. Your overriding method
must provide an appropriate method signature for the given
selector, either by pre formulating one or by asking another
object for one.
译:为了响应对象本身不能识别的方法,您必须重写methodSignatureForSelector:
和forwardInvocation:。转发消息的机制使用methodSignatureForSelector:获
得的信息来创建要转发的NSInvocation对象。重写方法必须为给定的选择器提供适当的
方法签名,可以通过预先构造一个选择器,也可以通过向另一个对象请求一个选择器。
methodSignatureForSelector
这个 就是用来获取方法签名的。(方法签名是什么,可以自行搜索,这里就不多解释了)
上代码
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(work)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
SEL aSelector = [anInvocation selector];
if ([[Person alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[Person alloc]];
else
[super forwardInvocation:anInvocation];
}
最后如果forwardInvocation
也未处理,则系统就会调用doesNotRecognizeSelector
方法。
我们来看看:
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
这就会抛出上面说的 unrecognized selector sent to instance 0x100524a90
至此,我想大家应该都了解了消息、方法是如何传递的,就是Runtime的底层实现流程,其实中间有些知识点还是比较粗浅,只讲了大致的流程,所以,我们在高级篇里继续说。
高级(6年以上)
大家都看到这里了,点个赞不过分把 ^_^
如何进入消息、方法接受
通过上面的讲解,我们明白了系统收到消息后,是如何运转的。但是,系统是如何收到消息的呢?这就要引入这个关键点
objc_msgSend(self,@selector(xxx))
程序中所有的消息/方法调用都会转为消息/方法的传递
objc_msgSend
是使用汇编语言编写的。为啥呢?
1,是接近底层
2,因为快
所以 当我们调用一个方法时
Person *person = [Person alloc];
[person sayHello];
test();
通过clang
编译命令转为cpp
文件就可以看到
Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
test();
同样 我们也可以用 objc_msgSend
来实现person类的初始化和调用方法
objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));
test();
所以, 不管是 alloc
方法 还是 sayHello
方法 ,底层都是通过 objc_msgSend
函数来实现的。因此 OC
方法的本质就是通过 objc_msgSend
来发送消息。
项目中和Runtime相关的东西
1 @synthesize和@dynamic
@synthesize的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法
@dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成(当然对于readonly的属性只需提供getter即可)
假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,但是当程序运行到instance.var = someVar,由于缺setter方法会导致程序崩溃;或者当运行到 someVar = instance.var时,由于缺getter方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定
2 Category的实现原理
Category是被添加到class_rw_t 的对应结构里, 实际上是 Category_t 的结构体,在运行时,新添加的方法,都被以倒序插入到原有方法列表的最前面,所以不同的 Category,添加了同一个方法,执行的实际上是最后一个。
这个方法列表就是methods,一个二维数组。
Category 在刚刚编译完的时候,和原来的类是分开的,只有在程序运行起来后,通过 Runtime ,Category 和原来的类才会合并到一起。
如何合并:
mememove,memcpy:这俩方法是位移、复制,简单理解就是原有的方法移动到最后,根根新开辟的控件,
把前面的位置留给分类,然后分类中的方法,按照倒序依次插入,可以得出的结论就就是,越晚参与编译的分类,里面的方法才是生效的那个。
什么时候合并:
- 程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init。
- 然后会 map_images。
- 接下来调用 map_images_nolock。
- 再然后就是 read_images,这个方法会读取所有的类的相关信息。
- 最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。
- 在 reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入 Class 和 Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t 结构体
3 method-swizzing
这个其实就不用多说了,也是Runtime的一个重要体现,网上一搜一大把。
结语
Runtime 可说的东西其实非常多,我也是越写越发现,有很多可以深入探讨的地方,所以在上面有些东西写的不一定准确和完整,望大家海涵。
最后希望大家能 知其然且知其所以然。 共勉,点赞🤞