一直想弄明白runtime是怎么回事,因为面试的时候这是一道必备问题,但是平时用的机会真的少之又少,我一度以为runtime只是用来装13的利器,没什么卵用。但是随着学习的增多,发现runtime真的很有用,但也没那么神秘。我相信看了我这篇博客,您对runtime肯定会有自己的理解。
先说说OC与C的对比:
1.OC是对OC的面向对象的封装,OC中的对象只是C中指向结构体的指针。
2.OC的方法,本质上就是C语言中的函数,OC中的任意一个方法,在runtime中都会有一个与之对应的函数。eg:[objc sendMessage:@"I am back"];
-> objc_msg(self,@selector(sendMessage),"I am back");
所以说在OC中对象调用方法,到运行的时候,都会变成向对象发送消息,这就是runtime中最著名的消息机制。
3.既然本质都是函数,那是不是和C语言的函数没有区别呢?绝对不是。
(1)C语言只能调用实现过的函数,只声明了是不行的,编译是不能通过的。
(2)OC无所谓,只要声明了就能调用,即时你没声明都能调用,编译阶段都不会报错,只会报警告。
- (id)performSelector:(SEL)aSelector;
这样据说是保证了编程的灵活性,反正大家都这么说,但是我觉得这就是不够严谨,因为真要是需要这个方法执行了,程序就得崩溃,在编译的时候就能解决的问题,为什么要等到程序崩溃再修改代码呢,有点浪费时间啊。
下面列举了几个runtime的应用实例,先用起来,用的多了,自然就理解了。
1.runtime的常用方法
runtime可以动态获取一个对象的成员变量、属性、方法、遵守的协议。
#import <Foundation/Foundation.h>
#import "ProtocolTest1.h"
#import "ProtocolTest2.h" @interface Person : NSObject <ProtocolTest1,ProtocolTest2> @property (nonatomic, strong) NSString *name; @property (nonatomic, strong) NSString *age; - (NSString *)getName; - (NSString *)getAge; + (void)classMethodTest; @end
#import "Person.h"
@implementation Person - (NSString *)getName { return @"I am Tomcat"; } - (NSString *)getAge { return @"I will be 18 years old forever"; } + (void)classMethodTest { NSLog(@"This is a class method"); } - (NSString *)description { return [NSString stringWithFormat:@"name=%@,age=%@",_name,_age]; } @end
先看看我们的小白鼠Person类,我们就拿他做实验,动态获取他的成员变量、属性、方法、遵守的协议。
#import "FirstViewController.h"
#import <objc/runtime.h>
#import "Person.h" @interface FirstViewController () @end @implementation FirstViewController - (void)viewDidLoad { [super viewDidLoad]; Person *tomcat = [Person new]; unsigned int count; NSLog(@"\n1.获得属性列表"); // 1.get describes of all properties (获得属性列表) objc_property_t *propertyList = class_copyPropertyList([tomcat class], &count); for (unsigned int i = 0; i < count; i++) { const char *propertyName = property_getName(propertyList[i]); printf("property = %s\n",propertyName); } // 2.get describes of all methods (获得方法列表) NSLog(@"\n\n2.获得方法列表"); Method *methodList = class_copyMethodList([tomcat class], &count); for (unsigned int i = 0; i < count; i++) { SEL methodName = method_getName(methodList[i]); NSLog(@"methodName = %@",NSStringFromSelector(methodName)); } // 3.get describes of all variables (获得成员变量列表) NSLog(@"\n\n3.获得成员变量列表"); Ivar *ivarList = class_copyIvarList([tomcat class], &count); for (unsigned int i = 0; i < count; i++) { const char *ivarNmae = ivar_getName(ivarList[i]); printf("ivarNmae = %s\n",ivarNmae); // 动态变量控制 object_setIvar(tomcat, ivarList[i], @"哈哈,你被我改了"); } NSLog(@"动态变量控制: name = %@",tomcat.name); //4.get describes of all protocols adopted by a class (获得当前对象遵守的协议列表) NSLog(@"\n\n4.获得协议列表"); __unsafe_unretained Protocol **protocolList = class_copyProtocolList([tomcat class], &count); for (unsigned int i = 0; i < count; i++) { const char *protocolNmae = protocol_getName(protocolList[i]); printf("protocolNmae = %s\n",protocolNmae); }
注意:我们要使用runtime库,首先要 #import <objc/runtime.h>。
2.拦截并替换方法
想要拦截和替换方法,首先要找到方法,根据什么找呢?方法名。
//5. 通过方法名获得类方法
Class personClass = object_getClass([Person class]); SEL classSel = @selector(classMethodTest); Method classMethod = class_getInstanceMethod(personClass, classSel); //6. 通过方法名获得实例方法 SEL objSel1 = @selector(getName); Method objMethod1 = class_getInstanceMethod([tomcat class], objSel1); SEL objSe2 = @selector(getAge); Method objMethod2 = class_getInstanceMethod([tomcat class], objSe2);
我们还可以交换这两个方法的实现
//7. 交换两个方法的实现
NSLog(@"\n\n交换两个方法的实现");
NSLog(@"交换之前 --- getName = %@,getAge = %@", [tomcat getName],[tomcat getAge]); method_exchangeImplementations(objMethod1, objMethod2); NSLog(@"交换之后 --- getName = %@,getAge = %@", [tomcat getName],[tomcat getAge]);
拦截并替换方法,多用于给系统方法添加新的功能和修改第三方库。我们现在实现一个功能,就是给计算按钮的点击计数。
#import "UIButton+Count.h"
#import <objc/runtime.h>
#import "Tool.h" @implementation UIButton (Count) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class selfClass = [self class]; // 1.原来的方法 SEL oriSEL = @selector(sendAction:to:forEvent:); Method oriMethod = class_getInstanceMethod(selfClass, oriSEL); // 2.现在的方法 SEL cusSel = @selector(mySendAction:to:forEvent:); Method cusMethod = class_getInstanceMethod(selfClass, cusSel); // 3.给原来的方法添加实现,防止原来的方法没有实现,只有声明崩溃 BOOL addSuc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod)); if (addSuc) { // 添加成功,用现在的方法的实现替换原来方法的实现 class_replaceMethod(selfClass, cusSel, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else { // 没添加成功,证明原来的方法有实现,直接交换两个方法 method_exchangeImplementations(oriMethod, cusMethod); } }); } // 现在的方法 - (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { [[Tool shareInstance] countClicks]; [super sendAction:action to:target forEvent:event]; } @end
Tool.h
#import <Foundation/Foundation.h>
@interface Tool : NSObject @property (nonatomic, assign) NSInteger count; + (instancetype)shareInstance; - (void)countClicks; @end
Tool.m
#import "Tool.m"
@implementation Tool static id _instance; + (instancetype)shareInstance { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instance = [[Tool alloc] init]; }); return _instance; } - (void)countClicks { _count += 1; NSLog(@"您点击了%ld次",_count); } @end
3.实现NSCoding自动归档解档
NSCoding用于存储模型对象,必须实现代理NSCoding。
#import "Student.h"
#import <objc/runtime.h>
@implementation Student - (void)encodeWithCoder:(NSCoder *)aCoder { // 如果不用runtime // [aCoder encodeObject:self.name forKey: @"name"]; // [aCoder encodeObject:self.stuID forKey: @"stuID"]; // [aCoder encodeObject:self.score forKey: @"score"]; // [aCoder encodeObject:self.name forKey: @"myFriend"]; unsigned int count = 0; // 获取变量列表 Ivar *ivars = class_copyIvarList([self class], &count); for (int i = 0; i < count; i++) { // 获取变量名 const char *name = ivar_getName(ivars[i]); NSString *key = [NSString stringWithUTF8String:name]; id value = [self valueForKey:key]; // 用变量名归档变量 [aCoder encodeObject:value forKey:key]; } free(ivars); } - (instancetype)initWithCoder:(NSCoder *)aDecoder { if (self = [super init]) { // 如果不用runtime // self.name = [aDecoder decodeObjectForKey:@"name"]; // self.stuID = [aDecoder decodeObjectForKey:@"stuID"]; // self.score = [aDecoder decodeObjectForKey:@"score"]; // self.myFriend = [aDecoder decodeObjectForKey:@"myFriend"]; unsigned int count = 0; // 获取变量列表 Ivar *ivars = class_copyIvarList([self class], &count); for (int i = 0; i < count; i++) { // 获取变量名 const char *name = ivar_getName(ivars[i]); NSString *key = [NSString stringWithUTF8String:name]; // 用变量名解档变量 id value = [aDecoder decodeObjectForKey:key]; [self setValue:value forKey:key]; } free(ivars); } return self; } - (NSString *)description { return [NSString stringWithFormat:@"name=%@\nstuID=%@\nscore=%@\nmyFriend:%@",_name,_stuID,_score,_myFriend]; } @end
如果模型属性少无所谓,如果多的话,最好用runtime。有100个属性,你不可能写100次解档归档啊。
4.实现字典转模型的自动转换
其实这个用系统方法就很好了,这次我用runtime实现一下,估计系统方法也是这个逻辑。
#import "NSObject+Model.h"
#import <objc/runtime.h>
@implementation NSObject (Model) + (instancetype)allocWithDic: (NSDictionary *)dic { id objc = [[self alloc] init]; unsigned int count = 0; Ivar *ivarList = class_copyIvarList(self, &count); for (int i = 0; i < count; i++) { Ivar ivar = ivarList[i]; NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)]; NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; NSString *key = [ivarName substringFromIndex:1]; id value = dic[key]; NSLog(@"type = %@",ivarType); // 把模型转化的字典再转换成模型 // 先判断value类型,value类型是自字典,但是ivarType又不是NSDictionary,说明value实际上是一个模型转化的字典 if ([value isKindOfClass:[NSDictionary class]] && ![ivarType containsString:@"NS"]) { // @"Person" -> "Person" ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""]; // "Person" -> Person ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""]; // 生成模型 Class modelClass = NSClassFromString(ivarType); if (modelClass) { // 给模型里的属性赋值 value = [modelClass allocWithDic:value]; } } if (value) { [objc setValue:value forKey:key]; }else { NSLog(@"没找到value"); } } return objc; } @end
rumtime库是一个非常强大的,我列举的这几个用法是比较常用和基础的。例外runtime是开源的,不过看起来应该很难,基本上是用c语言和汇编写的,现在懂汇编的应该很少。本文完整案例已经上传到Github,欢迎下载。