本文主要介绍Runtime四种使用情况:
1、交换方法
2、动态添加方法
3、动态添加属性
4、日志统计
Objective-C 是面向运行时的语言(runtime oriented language),就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。这就给了你很大的灵活性,你可以按需要把消息重定向给合适的对象,你甚至可以交换方法的实现,等等。
RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错)。OC只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。
以下面的代码为例:
[obj makeText];
其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成
objc_msgSend(obj,@selector(makeText));
首先,编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若cache中未找到。再去methodList中查找,若methodList中未找到,则去superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
Demo地址:runtime
Method Swizzling
Method Swizzling也称苹果的“黑魔法”,本质上就是对IMP和SEL进行交换。
Method Swizzling原理
Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。
我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP
我们可以利用 class_replaceMethod 来修改类
我们可以利用 method_setImplementation 来直接设置某个方法的IMP
归根结底,都是偷换了selector的IMP
而且Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。
RunTime实践
1. 交换方法
iOS中有很多可变容器,如:NSMutableArray。在调用可变数组的addObject方法时,如果我们加入了一个nil对象,就会crash。所以在添加数据之前,我们就需要对数据做判空处理。同样,在拿数据时,经常也会遇到角标越界的问题,所以在拿数据之前,需要对角标进行判断,这样太麻烦了,有了runtime我们就可以这样解决这个问题。
Runtime的思想就是交换方法,把系统的方法和我们自定义的方法进行交换,然后我们在定义的方法里,对数据进行处理。
1、先定义自己的方法
给NSMutableArray新建一个类别,.m文件实现:
#import "NSMutableArray+safe.h"
#import <objc/runtime.h>
@implementation NSMutableArray (safe)
+ (void)load {
}
@end
代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。我们会将safeAddObject方法和系统的addObject方法交换。当你调用addObject方法时,其实调用的是safeAddObject。当调用safeAddObject方法时,调用的是系统的addObject方法。
- (void)safeAddObject:(id)anObject {
if(anObject) {
[self safeAddObject:anObject];
}else{
NSLog(@"obj is nil");
}
}
2、使用Method Swizzing交换两个方法
+ (void)load {
static dispatch_once_t oneToken;
dispatch_once(&oneToken, ^{
id obj = [[self alloc] init];
[obj swizzleMethod:@selector(addObject:) withMethod:@selector(safeAddObject:)];
});
}
- (void)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector {
Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, origSelector);
Methods wizzledMethod = class_getInstanceMethod(class, newSelector);
BOOL didAddMethod = class_addMethod(class,
origSelector
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if(didAddMethod) {
class_replaceMethod(class,
newSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
class_addMethod 。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations 替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector ,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。
3、调用方法
- (void)viewDidLoad {
NSMutableArray *array = [NSMutableArray array];
[array addObject:nil]; //->obj is nil
[array objectAtIndex:3];//->index is beyond bounds
}
2. 动态添加方法
1、新建一个类,命名为Student,.m文件实现以下代码:
#import "Student.h"
#import <objc/runtime.h>
@implementation Student
void study(id self, SEL sel) {// 要添加的方法
NSLog(@"%@ %@",self, NSStringFromSelector(sel));
}
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+(BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == NSSelectorFromString(@"study")) {
// 动态添加study方法
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, sel, (IMP)study,"v@:");
}
return [super resolveInstanceMethod:sel];
}
@end
2、调用方法
- (void)viewDidLoad {
Student *p = [[Student alloc] init];
[p performSelector:@selector(eat)];
}
3. 动态添加属性
场景一:给系统类动态添加属性
有这样一种情况,在一个页面有很多模块,每个模块里又有很多Button,当点击button时,我们想知道是点击了哪个模块里的哪个button。button有自带的tag属性,可以标示button的唯一性,但是这里我们还想知道是哪个模块里的button,此时我就想希望button还有个模块属性,类似tableview里的section和row的关系一样。不用苦恼,我们可以利用runtime给button动态添加一个我们自己定义的属性。(当然,我们也可以)
第一步:给UIButton新建一个类别文件 .h文件 实现代码:
@interfaceUIButton (property)
// 在列别中定义属性,只有声明方法,没有实现方法,直接访问属性会报错
@property (strong, nonatomic) NSString *section;
@end
.m文件 实现代码:
#import "UIButton+property.h"
#import <objc/runtime.h>
// 定义关联的key
static const char *key = "section";
@implementationUIButton (property)
- (NSString*)section{
// 根据关联的key,获取关联的值。
return objc_getAssociatedObject(self, key);
}
- (void)setSection:(NSString*)section{
// 第一个参数:给哪个对象添加关联
// 第二个参数:关联的key,通过这个key获取
// 第三个参数:关联的value
// 第四个参数:关联的策略
objc_setAssociatedObject(self, key, section, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
第二步:调用
UIButton *button = [[UIButton alloc] init];
button.section =@"1";
NSLog(@"%@", button.section);
如果我们希望给系统所有的类都添加一个属性,可以给NSObject新建类别文件,实现动态添加属性。
场景二:给自定义的类动态添加属性
有这样的场景,我们使用别人的类,然后再页面中间,我们会传递这个类的实例,用来传值,此时根据情况,我们需要添加一个属性,但这个属性,知识临时用一下,我们没有必要去修改别人的代码。同样,我们也可以利用runtime来动态添加一个临时的属性。
具体代码和上面的给系统类添加属性类似,可以参考具体代码:runtime实践
4. 日志统计
有时候市场同事会提出,他们想知道产品具体某个页面的的打开次数,这就要求,某个页面打开,我会要告知一下后台。
方法一:让所有的Controller继承自一个BaseController。我们在BaseController的生命周期方法(viewDidLoad)里去写向后台请求的代码。这就要求所有的类必须继承一个Base,如果项目已经开发,并且你没有这么做,那么,你的改动就大了。
方法二:使用AOP加runtime实现。
1、给UIViewController新建一个类别并且实现 .m 文件:
#import "UIViewController+track.h"
#import <objc/runtime.h>
@implementationUIViewController (track)
/*
创建一个Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码
*/
+ (void)load {
[super load];
// 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
Method fromMethod =class_getInstanceMethod([self class],@selector(viewDidLoad));
Method toMethod =class_getInstanceMethod([self class],@selector(swizzlingViewDidLoad));
/**
* 我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
* 而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
* 所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
*/
if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
method_exchangeImplementations(fromMethod, toMethod);
}
}
// 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。
- (void)swizzlingViewDidLoad {
NSString *str = [NSString stringWithFormat:@"%@", self.class];
// 我们在这里加一个判断,将系统的UIViewController的对象剔除掉
if(![str containsString:@"UI"]){
NSLog(@"页面统计 : %@",self.class);
}
[self swizzlingViewDidLoad];
}
@end
网友评论