runtime(二)
前言
上一篇中我们大致的了解了runtime的一些基本概念,这一篇我们一起来看看如何使用它。
3、如何使用runtime。
3.1 方法交换
举一个老生常谈的例子。当你接手一个新的项目,需要查看这个程序运行时出现的UI对应的控制器的时候,如果你单纯的去通过UI上面的关键词全局搜索或是在viewwillappear里面打印该类的类名,你会浪费很长的时间,而且你会很难受。这个时候用runtime就会很好处理这种情况。
UI出现的时候,都会调用方法viewWillAppear。也就是说,如果我能让程序统一不走这个方法,走另一个方法(假设名字是myViewWillAppear),而这个方法内部还是会调用viewWillAppear,那么我就可以在myViewWillAppear中打印将要出现的UI的类名。看看下面的代码:
#import "UIViewController+WillAppear.h" #import <objc/runtime.h> @implementation UIViewController (WillAppear) + (void)load{ Method method1 = class_getInstanceMethod([self class], @selector(viewWillAppear:)); Method method2 = class_getInstanceMethod([self class], @selector(logViewWillAppear:)); method_exchangeImplementations(method1, method2); } - (void)logViewWillAppear:(BOOL)animated{ NSString *className = NSStringFromClass([self class]); if ([className hasPrefix:@"LMF"]) { NSLog(@"%@ will appear", className); } [self logViewWillAppear:animated]; } @end
将系统的viewwillappear的指针与logviewwillappear的指针交换,这样,UI出现的时候会先调用logviewwillappear然后再调用viewwillappear方法,目的便达到了。
以此类推,我们有时会碰到unrecognized selector这种错误,这种错误很低级,但确实存在。对于初级开发者来讲时常会碰到,这个时候如果用runtime检测对象是否实现了此方法判断是否需要将该方法替换成其他事先已经实现的方法,可以避免这种错误。不过,推荐别这样玩,因为bug暴露越早越好,这里只是谈一下runtime解决这种问题的可行性。
3.2 可拓展性(动态添加方法和属性)
讲道理,我们还是要遵守一下可拓展性的。谁都不希望接手一份处处都要修改源码的代码,这个时候如果拓展性高,在不改变类内部源码的情况下给这个类添加额外的方法或者属性无疑是最好的。正好,oc的runtime很好的提供了这种特性。
其实我们一直都在用runtime,category就是最好的证明。它可以帮助我们给某些类在不需要修改该类内部的源码的情况下添加额外的方法,我们形象的称之为类别。类别怎么写我就不说了,你不会写算我输。添加方法用类别,那么添加属性能不能用类别呢?非常遗憾,oc没有这种语法。这个时候,还是用到runtime,看看下面代码,就懂了怎么添加:
objc_setAssociatedObject([UIApplication sharedApplication].delegate, @"USEROperationUnReadCount", [NSNumber numberWithInteger:count], OBJC_ASSOCIATION_RETAIN); objc_getAssociatedObject([UIApplication sharedApplication].delegate, @"USEROperationUnReadCount");
上述代码是给AppDelegate动态添加了一个属性叫USEROperationUnReadCount,值是个NSNumber对象,OBJC_ASSOCIATION_RETAIN表示这个属性的内存管理模式。get方法是将这个新加的属性值取出来,这样形成一个完整的结构。
通过类别动态的添加方法是非常方便的,这里还说一种比较不常用的方式,不推荐使用。话不多说,直接上代码。
.m中代码:
#import "LMFView.h" #import <objc/runtime.h> void show(id self, SEL _cmd, NSString *message){ NSLog(@"%@", message); } void eat(id self, SEL _cmd, NSString *who){ NSLog(@"%@ eat meat", who); } @implementation LMFView + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(show)) { class_addMethod([self class], sel, (IMP)show, "v@:@:"); return YES; }else if (sel == @selector(eat)) { class_addMethod([self class], sel, (IMP)eat, "v@:@:"); return YES; } return [super resolveInstanceMethod:sel]; } @end
.h中代码:
#import <UIKit/UIKit.h> @interface LMFView : UIView @end
我们可以看到,并没有声明方法eat。然后我们在外部调用eat,当然不能直接通过[]调用,因为编译通不过。用选择器调用:
- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. self.view.backgroundColor = [UIColor whiteColor]; self.lmfView = [[LMFView alloc] initWithFrame:CGRectMake(100, 200, 100, 100)]; self.lmfView.backgroundColor = [UIColor redColor]; [self.view addSubview:self.lmfView]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showNextView)]; [self.lmfView addGestureRecognizer:tap]; UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; button.frame = CGRectMake(100, 100, 100, 50); button.backgroundColor = [UIColor cyanColor]; [button setTitle:@"runtime" forState:UIControlStateNormal]; [button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; [button addTarget:self action:@selector(show) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } - (void)show { [self.lmfView performSelector:@selector(eat) withObject:@"😄"]; }
当点击button的时候,self调用show,然后self.lmfView调用“eat:”,然而这个方法并没有声明,正常情况下,编译器是会报错的。但是在LMFView内部已经重写了resolveInstanceMethod:这个方法,如果没有找到eat:那么会新添加一个方法eat:,当然你可以新添加方法run以及其他的,不过指针要指向你想调用的方法。因为eat只是选择器,是方法ID,从本质上讲是一个整型。用这种方式,也可以动态添加方法,不过不推荐使用(除非有特殊需求)。这里只是简单介绍。