一.什么是runtime?
runtime是一套底层的C语言API,包含很多强大实用的C语言数据类型和C语言函数。
二.runtime能做什么?
可以在程序运行时创建,检查,修改类、对象和它们的方法。
具体应用举例:
- 获取类的方法列表/参数列表;
- 方法调用;
- 方法拦截、动态添加方法;
- 方法替换: method swizzling
- 关联对象,动态添加属性;
再具体些:
1.对象关联
2.解归档
3.字典与模型互转
4.方法的调用,交换,插入,拦截,动态添加。
。。。。。。。
三.基础知识
实践出真理,开始吧。
先从Objective-C 中的方法调用说起。其实 [receiver message] 会被编译器转化为: objc_msgSend(receiver, selector)。
例:
创建一个名为testClass的新类
#import "testClass.h"
@implementation testClass
-(instancetype)init{
if (self = [super init]) {
[self runTimeTestAction];
}
return self;
}
-(void)runTimeTestAction{
NSLog(@"runTime");
}
@end
在终端使用 clang 重写命令,得到testClass.cpp文件:
clang -rewrite-objc /Users/apple/Desktop/runTimeTest/runTimeTest/TestClass/testClass.m
双击打开,command+F 搜索“static instancetype”,可以看到被编译器转化后的Init方法
static instancetype _I_testClass_init(testClass * self, SEL _cmd) {
if (self = ((testClass *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("testClass"))}, sel_registerName("init"))) {
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("runTimeTestAction"));
}
return self;
}
可以看到[self runTimeTestAction];被编译后的样子:
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("runTimeTestAction"));
objc_msgSend 函数被定义在 objc/message.h 目录下,其函数原型为
OBJC_EXPORT void objc_msgSend(void /* id self, SEL op, ... */ )
可以看到该函数有两个参数,一个id类型,一个SEL类型,再来看看这两个类型被如何定义,他们都被定义在objc/objc.h目录下:
SEL:
typedef struct objc_selector *SEL;
//这是个映射到方法的C字符串,你可以用 Objective-C 编译器命令 @selector() 或者 Runtime 系统的 sel_registerName 函数来获得一个 SEL 类型的方法选择器。
id:
typedef struct objc_object *id;
//id 是一个结构体指针类型,它可以指向 Objective-C 中的任何对象。
//objc_object 结构体定义如下:
struct objc_object { Class isa OBJC_ISA_AVAILABILITY;};
这就是所谓的对象,isa为其一个Class类型的成员变量,isa指定对象所属的类。
那么再来看看Class是如何定义的:
Class:
Class 也是一个结构体指针类型:
typedef struct objc_class *Class;
objc_class 结构体:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
·Class 也有一个 isa 指针,指向其所属的元类(meta).
·super_class:指向其超类.
·name:类名.
·version:类的版本信息.
·info:类的详情.
·instance_size:该类的实例对象的大小.
·ivars:指向该类的成员变量列表.
·methodLists:指向该类的实例方法列表,它将方法选择器和方法实现地址联系起来。methodLists 指向 ·objc_method_list 指针的指针,也就是说可以动态修改 *methodLists 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因.
·cache:Runtime 系统会把被调用的方法存到 cache 中(理论上讲一个方法如果被调用,那么它有可能今后还会被调用),下次查找的时候效率更高.
·protocols:指向该类的协议列表.
PS:
methodLists 指向该类的实例方法列表,实例方法即-方法,那么类方法(+方法)存储在哪儿呢?类方法被存储在元类中,Class 通过 isa 指针即可找到其所属的元类.
上图实线是 super_class 指针,虚线是 isa 指针。根元类的超类是NSObject,而 isa 指向了自己。NSObject 的超类为 nil,也就是它没有超类。
现在结合上图来看下方法调用的整体流程:
1.首先,Runtime 系统会把方法调用转化为消息发送,即 objc_msgSend,并且把方法的调用者,和方法选择器,当做参数传递过去
2.此时,方法的调用者会通过 isa 指针来找到其所属的类,然后在 cache 或者 methodLists 中查找该方法,找得到就跳到对应的方法去执行
3.如果在类中没有找到该方法,则通过 super_class 往上一级超类查找(如果一直找到 NSObject 都没有找到该方法的话,这种情况,我们放到后面消息转发的时候再说)
4.如果一直找到 NSObject 都没有找到该方法的话就进入消息转发了,消息转发的大致过程如图(我们可以利用这一过程,添加动态方法)
消息转发的大致过程如图:
①进入 resolveInstanceMethod: 方法,指定是否动态添加方法。若返回NO,则进入下一步,若返回YES,则通过 class_addMethod 函数动态地添加方法,消息得到处理,此流程完毕.
②resolveInstanceMethod: 方法返回 NO 时,就会进入 forwardingTargetForSelector: 方法,这是 Runtime 给我们的第二次机会,用于指定哪个对象响应这个 selector。返回nil,进入下一步,返回某个对象,则会调用该对象的方法.
③若 forwardingTargetForSelector: 返回的是nil,则我们首先要通过 methodSignatureForSelector: 来指定方法签名,返回nil,表示不处理,若返回方法签名,则会进入下一步.
④当第 methodSignatureForSelector: 方法返回方法签名后,就会调用 forwardInvocation: 方法,我们可以通过 anInvocation 对象做很多处理,比如修改实现方法,修改响应对象等.
⑤如果到最后,消息还是没有得到响应,程序就会crash
一般情况下程序会crash,错误信息类似下面这样:
unrecognized selector sent to instance 0x7fd0a141afd0
四.实践及应用
上面说了这么多只是根据自己的理解通过Runtime的消息发送机制初步整理了下,runtime基础原理和内部结构,插图及解释大部份摘自网络。下面开始说些具体应用。
1.对象关联
对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态给对象增加成员变量。相对的,对象的方法定义都保存在类的可变区域中。
methodList,通过修改methodList指针所指向的指针的值,我们可以实现为类动态增加方法实现。
因此,我们可以实现动态为一个类增加成员方法,但是却不能直接为类增加成员变量,这就是category的实现原理。
对象关联允许开发者对已经存在的类在 Category 中添加自定义的属性:
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
·object 是源对象.
·value 是被关联的对象.
·key 是关联的键,objc_getAssociatedObject 方法通过不同的 key 即可取出对应的被关联对象.
·policy 是一个枚举值,表示关联对象的行为:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy){
OBJC_ASSOCIATION_ASSIGN = 0, // 表示弱引用关联,通常是基本数据类型
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // 表示强引用关联对象,是线程安全的
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // 表示关联对象copy,是线程安全的
OBJC_ASSOCIATION_RETAIN = 01401, // 表示强引用关联对象,不是线程安全的
OBJC_ASSOCIATION_COPY = 01403 // 表示关联对象copy,不是线程安全的
};
要取出被关联的对象使用 objc_getAssociatedObject 方法。
要删除一个被关联的对象,使用 objc_setAssociatedObject 方法将对应的 key 设置成 nil 即可:
objc_setAssociatedObject(self, associatedKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_removeAssociatedObjects 方法将会移除源对象中所有的关联对象.
举个栗子,假如我们要给 UIButton 添加一个监听单击事件的 block 属性,新建 UIButton 的 Category:
#import <UIKit/UIKit.h>
typedef void(^whenClickBlock)(void);
@interface UIButton(ClickBlock)
@property (nonatomic,copy) whenClickBlock whenClickBlock;
@end
#import "UIButton+ClickBlock.h"
#import <objc/runtime.h>
static const void *associatedKey = "associatedKey";
@implementation UIButton(ClickBlock)
-(void)setWhenClickBlock:(whenClickBlock)whenClickBlock{
objc_setAssociatedObject(self, associatedKey, whenClickBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self removeTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
if (whenClickBlock) {
[self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
}
}
-(whenClickBlock)whenClickBlock{
return objc_getAssociatedObject(self, associatedKey);
}
-(void)buttonClick{
if (self.whenClickBlock) {
self.whenClickBlock();
}
}
@end
调用:
//用对象关联在Category内加属性
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = self.view.bounds;
[self.view addSubview:button];
__weak UIButton * weakbutton=button;
button.whenClickBlock = ^{
//随机改变颜色
weakbutton.backgroundColor=randomColor;
};
2.交换方法,动态加载方法
方法交换
关于这部分操作,都被定义在#import <objc/runtime.h>目录下,下面介绍几个常用的,其他的感兴趣的话可以自行查看:
class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)// 替换类方法的定义
method_exchangeImplementations(Method m1, Method m2) //交换两个方法的实现
method_setImplementation(Method m, IMP imp) //设置一个方法的实现
PS:class_replaceMethod 试图替换一个不存在的方法时候,会调用class_addMethod为该类增加一个新方法
例:
#import "ViewController+login.h"
#import <objc/runtime.h>
@implementation UIViewController (login)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class=[self class];
SEL originalSelector = @selector(viewDidAppear:);
SEL swizzledSelector = @selector(swizzledViewDidAppear:);
swizzleMethod(class, originalSelector, swizzledSelector);
});
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector) {
// Method : 包含了一个方法的 方法名 + 实现 + 参数个数及类型 + 返回值个数及类型 等信息
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
//IMP:方法的地址
IMP swizzledImp = method_getImplementation(swizzledMethod);
char *swizzledTypes = (char *)method_getTypeEncoding(swizzledMethod);
IMP originalImp = method_getImplementation(originalMethod);
char *originalTypes = (char *)method_getTypeEncoding(originalMethod);
BOOL success = class_addMethod(class, originalSelector, swizzledImp, swizzledTypes);
//避免UIViewController父类实现swizzledViewDidAppear影响
if (success) {
//替换
class_replaceMethod(class, swizzledSelector, originalImp, originalTypes);
}else {
// 失败,表明已经有这个方法,直接交换
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)swizzledViewDidAppear:(BOOL)animation {
[self swizzledViewDidAppear:animation];
NSLog(@"YLZ:%@ viewDidAppear", NSStringFromClass([self class]));
}
@end
调用:
ViewController * VC=[ViewController new];
动态加载方法
首先看下消息转发,上文也提到过,具体过程:
// 第一步,消息接收者没有找到对应的方法时候,会先调用此方法,可在此方法实现中动态添加新的方法
// 返回YES表示相应selector的实现已经被找到,或者添加新方法到了类中,否则返回NO
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return YES;
}
// 第二步, 如果第一步的返回NO或者直接返回了YES而没有添加方法,该方法被调用
// 在这个方法中,我们可以指定一个可以返回一个可以响应该方法的对象, 注意如果返回self就会死循环
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
// 第三步, 如果forwardingTargetForSelector:返回了nil,则该方法会被调用,系统会询问我们要一个合法的『类型编码(Type Encoding)』
// 若返回 nil,则不会进入下一步,而是无法处理消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
// 当实现了此方法后,-doesNotRecognizeSelector: 将不会被调用
// 在这里进行消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
// 在这里可以改变方法选择器
[anInvocation setSelector:@selector(unknown)];
// 改变方法选择器后,需要指定消息的接收者
[anInvocation invokeWithTarget:self];
}
- (void)unknown {
NSLog(@"unkown method.......");
}
// 如果没有实现消息转发 forwardInvocation 则调用此方法
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"unresolved method :%@", NSStringFromSelector(aSelector));
}
根据消息转发过程,一般我们在resolveInstanceMethod:(SEL)sel动态添加方法:
构建一个Cat ,动态添加miaow:方法,我们可以这样做,例:
#import "cat.h"
#import <objc/runtime.h>
@implementation cat
- (void)eat{
NSLog(@"cat eat....");
}
- (void)jump{
NSLog(@"cat jump....");
}
//prama:传人参数,可自定义
void miaow(id self ,SEL _cmd,NSString * param){
NSLog(@"%@",param);
}
+(BOOL)resolveInstanceMethod:(SEL)sel{
if (sel==NSSelectorFromString(@"miaow:")) {
/**
* 动态添加方法
*
* @param self cls:为哪个类添加方法
* @param sel SEL:添加方法的方法编号(方法名)是什么
* @param IMP IMP:方法实现
* @param const char * types方法类型
*
*/
BOOL isSuccess = class_addMethod(self, sel, (IMP)miaow, "v@:@");
return isSuccess;
}
return [super resolveInstanceMethod:sel];
}
@end
调用:
[[cat new]performSelector:@selector(miaow:) withObject:@"喵~"];
自动解归档
在归档的过程中,当属性较多时,用普通的解归档操作,比较麻烦,🔎很多人深有体会,通过runtime可以简化这一操作,具体思路为:
-使用 class_copyIvarList 方法获取当前 Model 的所有成员变量.
-使用 ivar_getName 方法获取成员变量的名称.
-通过 KVC 来归档,解档.
代码:
#import <Foundation/Foundation.h>
@interface myModel : NSObject<NSCoding>
@property (nonatomic, copy) NSNumber *sex;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, copy) NSNumber *height;
@property (nonatomic, copy) NSNumber *weight;
@end
///////////////////////////////////
#import "myModel.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation myModel
- (id) initWithCoder: (NSCoder *)aDecoder{
if (self=[super init]) {
unsigned int outcount=0;
Ivar * vars=class_copyIvarList([self class], &outcount);
for (int i=0; i<outcount; i++) {
Ivar var=vars[i];
const char * name=ivar_getName(var);
NSString * key=[NSString stringWithUTF8String:name];
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
//释放vars
free(vars);
}
return self;
}
- (void) encodeWithCoder: (NSCoder *)aCoder{
unsigned int outcount=0;
Ivar * vars=class_copyIvarList([self class],&outcount );
for (int i=0 ; i<outcount; i++) {
Ivar var=vars[i];
const char * name=ivar_getName(var);
NSString * key=[NSString stringWithUTF8String:name];
id value=[self valueForKey:key];
[aCoder encodeObject:value forKey:key];
}
free(vars);
}
@end
3.字典模型互转
跟归档遇到的问题一样,当有很多属性的时候,常规方法会比较麻烦
说下利用runtime做字典模型互转的思路:
字典转模型:
1.根据字典的 key 生成 setter 方法.
2.使用 objc_msgSend 调用 setter 方法为 Model 的属性赋值(或者 KVC).
模型转字典:
1.调用 class_copyPropertyList 方法获取当前 Model 的所有属性.
2.调用 property_getName 获取属性名称.
3.根据属性名称生成 getter 方法.
4.使用 objc_msgSend 调用 getter 方法获取属性值(或者 KVC).
代码:
#import <Foundation/Foundation.h>
@interface NSObject (KeyValues)
+(id)objectWithKeyValues:(NSDictionary *)aDictionary;
-(NSDictionary *)keyValuesWithObject;
@end
//////////////////////////////////////////////////////////////
#import "NSObject+KeyValues.h"
#import <objc/runtime.h>
#import <objc/message.h>
@implementation NSObject (KeyValues)
//字典转模型
+(id)objectWithKeyValues:(NSDictionary *)aDictionary{
id objc = [[self alloc] init];
for (NSString *key in aDictionary.allKeys) {
id value = aDictionary[key];
/*判断当前属性是不是Model*/
objc_property_t property = class_getProperty(self, key.UTF8String);
unsigned int outCount = 0;
objc_property_attribute_t *attributeList = property_copyAttributeList(property, &outCount);
objc_property_attribute_t attribute = attributeList[0];
NSString *typeString = [NSString stringWithUTF8String:attribute.value];
if ([typeString isEqualToString:@"@\"TestModel\""]) {
value = [self objectWithKeyValues:value];
}
/**********************/
//生成setter方法,并用objc_msgSend调用
NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];
SEL setter = sel_registerName(methodName.UTF8String);
if ([objc respondsToSelector:setter]) {
((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);
}
free(attributeList);
}
return objc;
}
//模型转字典
-(NSDictionary *)keyValuesWithObject{
unsigned int outCount = 0;
objc_property_t *propertyList = class_copyPropertyList([self class], &outCount);
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < outCount; i ++) {
objc_property_t property = propertyList[i];
//生成getter方法,并用objc_msgSend调用
const char *propertyName = property_getName(property);
SEL getter = sel_registerName(propertyName);
if ([self respondsToSelector:getter]) {
id value = ((id (*) (id,SEL)) objc_msgSend) (self,getter);
/*判断当前属性是不是Model*/
if ([value isKindOfClass:[self class]] && value) {
value = [value keyValuesWithObject];
}
/**********************/
if (value) {
NSString *key = [NSString stringWithUTF8String:propertyName];
[dict setObject:value forKey:key];
}
}
}
free(propertyList);
return dict;
}
@end
*-_
写在最后:
到这里,日常常用的基本都有了。一直想写一篇简单的介绍runtime的短文,供自己和大家借鉴学习,工作繁忙,前后断断续续写了几个星期,也总算完成了,严格来说这篇文章算所有人共同完成的,借鉴了大量网络资源,作者也算是在自己理解的基础上对网络上现有资源的初步整理,非常感谢各位大神的分享🙏。后面如果有机会再补充一些别的用例,供大家参考。
参考:
Runtime全方位装逼指南🙏
代码:
runTimeTest