在开始Runtime前,先看一个例子。
新建一个继承于NSObject的Dog类,.h文件添加一个run方法,run方法在.m文件不实现。
ViewController中创建Dog类实例。
各位看官猜猜会发生什么事?
没错,unrecognized selector sent to instance 0x600003e17170, 这在开发中是很常见的。找不到这个方法。
再做一个变更,假如Dog继承自Animal类,Animal的.m文件实现run方法,那会怎样?
run方法在控制台上打印出来了,这能证明在运行时,如果找不到方法,会向父类查找吗?
我们都知道,NSObject是所有类的父类,如果run方法能在NSObject这个类实现,那么就能证明实例对象在找方法时,会先从本类查找,如果本类没有,就向父类查找,如果仍没有,则报错。
先创建一个NSObect分类。此处多加一个func(不知此方法用途的,请自行百度哦)
控制台打印的是:
至此,可以大胆确定:
实例对象在找方法时,会先从本类查找,如果本类没有,就向父类查找,如果仍没有,则报错。
理论已经了解,那么代码实现如何?
Runtime的主要特性是什么呢? ---- 消息(方法)传递
如果消息(方法)在对象中找不到,就进行转发。
这里依照以下3个方面进行。
1.Runtime消息传递
2.Runtime消息转发
3.Runtime应用
Runtime消息传递:
以此代码为例:
[doLin run],转化过来就是objc_msgSend(doLin, run)
这有4个步骤:
- 通过obj的isa指针找到它的 class(此例为Dog) ;
- 在 class 的 method list 找 run ;
- 如果 class 中没到 run,继续往它的 superClass 中找 ;
- 一旦找到 run 这个函数,就去执行它的实现IMP 。(IMP指针,IMP就是Implementation的缩写,顾名思义,它是指向一个方法实现的指针,每一个方法都有一个对应的IMP)
这确实实现了方法查找,那可能有人要问,如果每次都需要遍历查找,查找效率是否太低下了?
别担心,苹果也帮你想好了。这就要提objc_cache。
objc_class 中另一个重要成员objc_cache 做的事情 - 再找到run 之后,把run 的method_name 作为key ,method_imp作为value 给存起来。当再次收到run 消息的时候,可以直接在cache 里找到,避免去遍历objc_method_list。objc_cache是存在objc_class 结构体中的。
objec_msgSend的方法定义如下:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
那消息传递是怎么实现的呢?我们看看对象(object),类(class),方法(method)这几个的结构体:
- 系统首先找到消息的接收对象,然后通过对象的isa找到它的类。
- 在它的类中查找method_list,是否有selector方法。
- 没有则查找父类的method_list。
- 找到对应的method,执行它的IMP。
- 转发IMP的return值。
消息传递用到的概念:
类对象(objc_class)
实例(objc_object)
元类(Meta Class)
Method(objc_method)
SEL(objc_selector)
IMP
类缓存(objc_cache)
Category(objc_category)
- 类对象(objc_class)
OC类是由Class类型来表示的,实际是指向objc_class结构体的指针。
typedef struct objc_class *Class;
objc/runtime.h,objc_class结构体的定义:
从上到下包含了:
- isa指针
- 类名
- 版本号
- 信息
- 实例大小
- 实例变量
- 方法列表
- 缓存
- 遵守的协议列表
这个类结构体的第一个成员变量是isa, 也证明了,类是对象,称为类对象。
类对象就是一个结构体struct objc_class,这个结构体存放的数据称为元数据(metadata),
-
实例(objc_object)
类对象中的元数据存储什么?
答:如何创建一个实例的相关信息。
那么类对象和类方法应该从哪里创建呢?
答:从isa指针指向的结构体创建,类对象的isa指针指向的我们称之为元类(metaclass),元类中保存了创建类对象以及类方法所需的所有信息,因此整个结构应该如下图所示:
元类(Meta Class)
上图整个体系构成了一个自闭环,struct objc_object结构体实例isa指针指向类对象,类对象的isa指针指向了元类,super_class指针指向了父类的类对象,
而元类的super_class指针指向了父类的元类,那元类的isa指针又指向了自己。
元类(Meta Class)是一个类对象的类。
在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。
为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,元类中保存了创建类对象以及类方法所需的所有信息。
任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。-
Method(objc_method)
Method就是表示能够独立完成一个功能的一段代码,比如:
- (void)run{
NSLog(@"This man run so quickly");
}//这段代码,就是一个函数。
我们来看下objc_method这个结构体的内容:
- SEL method_name 方法名
- char *method_types 方法类型
- IMP method_imp 方法实现
SEL为Key, IMP为Value, 方法查找过程,就是通过相应的SEL查找IMP实现
- SEL(objc_selector)
-
IMP
指向最终实现程序的内存地址的指针。
类缓存(objc_cache)
为了加速消息分发, 系统会对方法和对应的地址进行缓存,就放在上述的objc_cache,所以在实际运行中,大部分常用的方法都是会被缓存起来的,Runtime系统实际上非常快,接近直接执行内存地址的程序速度。Category(objc_category)
name:是指 class_name 而不是 category_name。(例如,为Dog类添加分类,此name则指Dog)
cls:要扩展的类对象,编译期间是不会定义的,而是在Runtime阶段通过name对 应到对应的类对象。
instanceMethods:category中所有给类添加的实例方法的列表。
classMethods:category中所有添加的类方法的列表。
protocols:category实现的所有协议的列表。
instanceProperties:表示Category里所有的properties,这就是我们可以通过objc_setAssociatedObject和objc_getAssociatedObject增加实例变量的原因,不过这个和一般的实例变量是不一样的。
从上面的category_t的结构体中可以看出,分类中可以添加实例方法,类方法,甚至可以实现协议,添加属性,不可以添加成员变量。
消息转发
先看看下面一则图片:此图标记为pNews;
发送消息会在相关的类对象中搜索方法列表,如果找不到则会沿着继承树向上一直搜索知道继承树根部(通常为NSObject),如果还是找不到并且消息转发都失败了就回执行doesNotRecognizeSelector:方法报unrecognized selector错。
那消息转发是什么??????
- 动态方法解析
- 备用接收者
- 完整消息转发
动态方法解析:
例如:
viewDidLoad中执行run:方法,但是ViewController中根本未实现run:方法,
如果不做补救,不可避免的就是奔溃的下场....
那好,要怎么补救呢?
-
import <objc/message.h>
- 实现resolveInstanceMethod:方法(这是返回BOOL的,应怎么返回呢?运用一句话,遇上对的人,勇敢说YES。上面提到SEL是Key, IMP要Value, 在没有IMP的情况下,首先选出SEL, 此例为@selector(run:), 找到后,即指定新的方法)
-
写新的c函数, 此例为void runMethod(id obj, SEL _cmd), 执行runMethod函数的IMP.
执行结果如下:
如果是下列情况呢?
这里不再添加新函数, 不可避免再次奔溃。
突然在想,+ (BOOL)resolveInstanceMethod:(SEL)sel ,返回YES和NO的区别在哪?上图pNews有写,如果返回YES, 那么就到了消息已处理了,那我强行转成YES, 是否可以标记消息已处理?
尝试过后,无语的发现,[ViewController run:]: unrecognized selector sent to instance 0x7f8786d14400'
大胆假设一下下,在执行run函数的时候,通过run函数这个SEL的key, 无法找到相应的IMP, 因此即使重写了+ (BOOL)resolveInstanceMethod:(SEL)sel, 返回了YES, 也没用,那么会走怎样的流程呢?
下一步就是找接收者了。
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法名的函义是:为选择器转发对象
这一步,要开始“造假对象”了。
“造假对象”??????
在Dog类中实现run方法,返回Dog对象,让Dog对象接收这个消息,
这时,控制台打印出Dog类执行run后的方法。
打印结果也证明我们成功实现了转发。
完整的消息转发流程如下:
其实省略以下两步完全不影响,这是因为
resolveInstanceMethod,返回YES/NO都不影响往下走流程
forwardingTargetForSelector一定返回nil。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector//给选择器的方法签名, 签名,是为了进入forwardInvocation方法
分析一下- (void)forwardInvocation:(NSInvocation *)anInvocation的实现
- 获取SEL
- 创建接收者,此例为Dog类型的实例d
- 如果d 响应SEL方法,实例d被调用去执行run:方法;否则,run:方法无法识别
Runtime应用
Runtime应用场景非常多,下面就介绍一些常见的应用场景。
- 关联对象(Objective-C Associated Objects)给分类增加属性
- 方法魔法(Method Swizzling)方法添加和替换和KVO实现
- 消息转发(热更新)解决Bug(JSPatch)
- 实现NSCoding的自动归档和自动解档
- 实现字典和模型的自动转换(MJExtension)
关联对象(Objective-C Associated Objects)
给分类添加方法是很常见的,这里就不展示了,这里只展示给分类添加属性。
可能有小伙伴立马来说,添加属性有什么好说的,随手就来一个。
然后立刻奔溃了。。。
分类中没有此新增属性的getter, setter方法,因此在调用getter时,会立马奔溃。
那是不是意味着不能支持添加属性?
不是,这时候就要到关联Objective-C Associated Objects了。
问题来了,怎样才能实现在分类中成功添加属性呢?
分析一下:
以上是实现getter, setter方法。
-
import <objc/message.h>
- setter方法中objc_setAssociatedObject设置关联,
- getter方法, return objc_getAssociatedObject(self, "cpoName"); //注意"cpoName"就等同于字典的key, 要保持一致。
- Dog示例调用setter, getter --- cpoName
黑魔法 --- 方法魔法(Method Swizzling)方法添加
方法交换是很有优势的体现,例子如下
如果是TableView, 默认刷新数据的方法有reloaddata, 如果希望在reloaddata前做一些个性化操作,那么黑魔法无疑是很有优势的。
注意:替换的方法内部要自己调用自己,否则不实现,这样可以做自己的操作后,再自行刷新
此文部分内容参考了来源:https://www.jianshu.com/p/6ebda3cd8052