长路漫漫,唯剑作伴--Runtime
一、为什么写这篇文章
-
正是有了Runtime ,OC才有了面向对象的能力,也正是有了Runtime,OC语言的动态性才能体现的淋淋尽致,作为一个iOS开发者,Runtime是不可避免的重点话题之一。
二、Runtime之于OC
-
OC是在C语言的基础之上增加了一层面向对象的语言,同时又是一种动态语言。它将很多静态语言在编译和链接时期所做的事放到了运行时来处理,OC的这种特性决定了它不仅需要一个编译器,而且需要一个运行时系统执行编译的代码。这个运行时系统可以让所有的工作可以正常执行,这个运行时系统就是Runtime。
-
Runtime其实是一个库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。Runtime使OC的编程更具有灵活性,例如可以把消息转发给我们想要的对象,随意交换两个方法的实现等等。
三、总结一下Runtime做了那些事情
-
封装:
-
在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被Runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
-
找到最终执行的代码:
四、OC的方法调用
-
OC中方法调用的本质是让对象发送消息:
// 创建person对象 Person *p = [[Person alloc] init]; // 调用对象方法 [p eat]; // 本质:让对象发送消息 objc_msgSend(p, @selector(eat));
-
函数调用过程
-
编译器执行上述转换。在objc_msgSend函数中,首先通过obj的isa指针找到obj对应的class。每个对象内部都默认有一个isa指针指向这个对象所使用的类。isa是对象中的隐藏指针,指向创建这个对象的类。
-
在Class中先去cache中通过SEL查找对应函数method(cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度)。
-
若cache中未找到,再去methodList中查找,若methodlist中未找到,则取superClass中查找。
-
若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
-
消息转发:实现方式有三种
-
动态方法解:
// 默认方法都有两个隐式参数, void eat(id self,SEL sel) { NSLog(@"%@ %@",self,NSStringFromSelector(sel)); } // 第一步:动态方法解析 // 对象在收到无法解读的消息后,首先将调用其所属类的下列类方法: // resolve : [rɪˈzɑ:lv] 决定 // instance : [ˈɪnstəns] 实例 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(eat)) { // 第一个参数 : 方法接收者 // 第二个参数 : 方法名 // 第三个参数 : 方法指针 // 第四个参数 : 方法签名,V代表方法返回值为void,@表示方法接受者,:表示_cmd,即方法名 class_addMethod(self, @selector(eat), (IMP)eat, "v@:"); } return [super resolveInstanceMethod:sel]; } + (BOOL)resolveClassMethod:(SEL)sel { return [super resolveClassMethod:sel]; }
-
备援接收者
// 第二步:备援接收者 // 如果动态方法解析无法处理消息,实现此方法可以把这条消息转给其他接收者来处理 // forwarding : ['fɔ:wədɪŋ] 转发 // Target 目标 - (id)forwardingTargetForSelector:(SEL)aSelector { return [[Drink alloc] init]; }
-
完整的消息转发:
// 第三步:完整的消息转发 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSString *sel = NSStringFromSelector(aSelector); if ([sel isEqualToString:@"drink"]) { return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } return [super methodSignatureForSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)anInvocation { SEL sel = anInvocation.selector; Drink *drink = [[Drink alloc] init]; if ([drink respondsToSelector:sel]) { [anInvocation invokeWithTarget:drink]; } }
五、 动态性三方面:动态类型、动态绑定、动态加载
-
动态类型
-
即id类型。动态类型和静态类型是相对的,如NSString、int等就属于静态类型。
-
在静态类型中,如果数据类型不对应,就会提示警告。而动态类型则不会,动态类型会在运行时决定其属于哪一种数据类型。
-
-
动态绑定
-
动态语言和静态语言一个区别就是,静态语言在编译时就已经确定了其逻辑,而动态语言则会在运行时决定。这是静态语言运行效率搞的原因之一。
-
其实在OC中并没有函数调用这一概念,函数调用被称为消息发送,所谓消息发送就是给对象发送一条消息。
-
-
动态加载
六、Runtime使用场景
-
遍历
#pragma mark--实例变量 - (void)queryProperty{ unsigned int count; objc_property_t *propertyList = class_copyPropertyList([Person class], &count); for (unsigned int i = 0; i < count; i++) { objc_property_t pro = propertyList[i]; const char *key = property_getName(pro); NSString *keyString = [NSString stringWithUTF8String:key]; NSLog(@"%@",keyString); } } #pragma mark--成员变量 - (void)qyeryVar{ unsigned int count; Ivar *ivar = class_copyIvarList([Person class], &count); for (unsigned int i = 0; i < count; i++) { Ivar var = ivar[i]; const char *key = ivar_getName(var); NSString *keyString = [NSString stringWithUTF8String:key]; NSLog(@"%@",keyString); } } #pragma mark--方法遍历 - (void)querymethod{ unsigned int count; Method *methodList = class_copyMethodList([Person class], &count); for (unsigned int i = 0; i < count; i++) { Method method = methodList[i]; SEL select = method_getName(method); NSString *selectString = NSStringFromSelector(select); NSLog(@"%@",selectString); } }
-
动态添加方法
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. Person *p = [[Person alloc] init]; // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。 // 动态添加方法就不会报错 [p performSelector:@selector(eat)]; } @end @implementation Person // void(*)() // 默认方法都有两个隐式参数, void eat(id self,SEL sel) { NSLog(@"%@ %@",self,NSStringFromSelector(sel)); } // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来. // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(eat)) { // 动态添加eat方法 // 第一个参数:给哪个类添加方法 // 第二个参数:添加方法的方法编号 // 第三个参数:添加方法的函数实现(函数地址) // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd class_addMethod(self, @selector(eat), eat, "v@:"); } return [super resolveInstanceMethod:sel]; } @end
-
动态添加属性
// 定义关联的key static const char *key = "name"; @implementation NSObject (Property) - (NSString *)name { // 根据关联的key,获取关联的值。 return objc_getAssociatedObject(self, key); } - (void)setName:(NSString *)name { // 第一个参数:给哪个对象添加关联 // 第二个参数:关联的key,通过这个key获取 // 第三个参数:关联的value // 第四个参数:关联的策略 objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
-
Method Swizzling
//load方法会在类第一次加载的时候被调用 //调用的时间比较靠前,适合在这个方法里做方法交换 + (void)load{ //方法交换应该被保证,在程序中只会执行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //获得viewController的生命周期方法的selector SEL systemSel = @selector(viewWillAppear:); //自己实现的将要被交换的方法的selector SEL swizzSel = @selector(swiz_viewWillAppear:); //两个方法的Method Method systemMethod = class_getInstanceMethod([self class], systemSel); Method swizzMethod = class_getInstanceMethod([self class], swizzSel); //首先动态添加方法,实现是被交换的方法,返回值表示添加成功还是失败 // class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>) BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod)); if (isAdd) { //如果成功,说明类中不存在这个方法的实现 //将被交换方法的实现替换到这个并不存在的实现 class_replaceMethod(self, swizzSel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); }else{ //否则,交换两个方法的实现 method_exchangeImplementations(systemMethod, swizzMethod); } }); } - (void)swiz_viewWillAppear:(BOOL)animated{ //这时候调用自己,看起来像是死循环 //但是其实自己的实现已经被替换了 [self swiz_viewWillAppear:animated]; NSLog(@"swizzle"); }
-
字典转模型
#import "NSObject+Model.h" #import <objc/runtime.h> @implementation NSObject (Model) + (NSArray *)propertList { unsigned int count = 0; //获取模型属性, 返回值是所有属性的数组 objc_property_t objc_property_t *propertyList = class_copyPropertyList([self class], &count); NSMutableArray *arr = [NSMutableArray array]; //便利数组 for (int i = 0; i< count; i++) { //获取属性 objc_property_t property = propertyList[i]; //获取属性名称 const char *cName = property_getName(property); NSString *name = [[NSString alloc]initWithUTF8String:cName]; //添加到数组中 [arr addObject:name]; } //释放属性组 free(propertyList); return arr.copy; } + (instancetype)modelWithDict:(NSDictionary *)dict { id obj = [self new]; // 遍历属性数组 for (NSString *property in [self propertList]) { // 判断字典中是否包含这个key if (dict[property]) { // 使用 KVC 赋值 [obj setValue:dict[property] forKey:property]; } } return obj; } @end
-
打印Model属性是否正确
-
方式一:直接打印
- 此方式往往只打印出对象地址
-
方式二:重写
description
方法- 如果属性较少,此方式比较适用
-
方式三:结合Runtime重写
description
方法- 如果属性较多,此方式比较实用。在控制台则会打印出很多属性. 看着就不舒服~~而且还有一个问题就是, 有时候我们其实并不需要打印
model
的属性.. 那这样重写description
方法反而适得其反
了。
- 如果属性较多,此方式比较实用。在控制台则会打印出很多属性. 看着就不舒服~~而且还有一个问题就是, 有时候我们其实并不需要打印
-
方式四:结合Runtime重写debugDescription方法
debugDescription
是在Xcode
控制台里使用po
命令的时候调用的~!而debugDescription
的实现其实也就是调用了description
方法而已。- 当需要打印
model
的属性的时候, 在控制台里使用po
命令即可.
-
七、总结
八、补充