iOS 关于runtime的那些事

关于runtime的那些事

runtime 简介

  • runtime简称运行时,程序在运行过程中都会转成runtime的C代码执行。
  • OC中的一切都被设计成对象,实际上类的本质也是一个对象,在runtime中用结构体表示。
  • 对于oc来说,在编译时并不能决定真正调用哪个函数,只有在真正运行时才会根据函数名找到对应的函数来调用。
  • runtime在编译阶段可以调用任何的函数,即使这个函数没有实现,只要声明过就不会报错。

runtime 应用


1.交换方法


使用场景:通常我们如果系统自带的方法所完成的功能不能满足我们的需求时,我们可以用runtime的方法的交换给系统的方法扩展一些功能,并且保持原有的功能。(其实继承系统类重新方法也可达到同样的效果)

注意事项:在给分类添加方法时,除非你明白你要实现什么样的功能(如将字典或者数组中的文字打印出来,而不是乱码),否则不能重写系统方法,因为这样会覆盖系统方法的实现。

代码示例:我们要实现这样的一个功能。当app有点卡的情况下,如果我们多次点击一个按钮,就会出现多次跳转到同一个页面的bug,影响了用户的体验。为了解决这一问题,我们使用runtime交换方法的功能,替换UIButton响应事件,完成根据响应时间间隔来判断是否执行消息发送。
#import <UIKit/UIKit.h>

@interface UIButton (Custom)
/* 响应的时间间隔 */
@property (assign, nonatomic) NSTimeInterval acceptEventInterval;
@end

#import "UIButton+Custom.h"
#import <objc/runtime.h>

@interface UIButton ()
@property (assign, nonatomic) NSTimeInterval custom_acceptEventTime;
@end

@implementation UIButton (Custom)

+ (void)load {
    
    SEL sysSEL = @selector(sendAction:to:forEvent:);

    Method systemMethod = class_getInstanceMethod(self, sysSEL);
    
    SEL customSEL = @selector(custom_sendAction:to:forEvent:);

    Method customMethod = class_getInstanceMethod(self, customSEL);
    
    // 如果不存在该方法的实现,就替换系统的方法,否则就交换两个方法的实现
    
    BOOL didAddMethod = class_addMethod(self, sysSEL, method_getImplementation(customMethod), method_getTypeEncoding(customMethod));
    
    if (didAddMethod) {
        class_replaceMethod(self, customSEL, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod));
    } else {
        method_exchangeImplementations(systemMethod, customMethod);
    }
    
}

- (void)custom_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
       
    //  当前时间的时间戳
    NSTimeInterval currentTimeInterval = [[NSDate date] timeIntervalSince1970];
    
    BOOL needSendAction = (currentTimeInterval - self.custom_acceptEventTime) >= self.acceptEventInterval;
    
    // 更新上次点击的时间戳
    if (self.acceptEventInterval > 0) {
        self.custom_acceptEventTime = currentTimeInterval;
    }
    
    if (needSendAction) {
        //  此处其实调用的是系统方法 sendAction:to:forEvent:,并不存在死循环。
        [self custom_sendAction:action to:target forEvent:event];
    } else {
        NSLog(@"点击过于频繁,请稍候重试!");
    }
}

#pragma mark - Setter & Getter

const NSString *kAcceptEventIntervalKey = @"acceptEventIntervalKey";

- (NSTimeInterval)acceptEventInterval {
    return [objc_getAssociatedObject(self, &kAcceptEventIntervalKey) doubleValue];
}

- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval {
    objc_setAssociatedObject(self, &kAcceptEventIntervalKey, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

const NSString *kAcceptEventTimeKey = @"acceptEventTimeKey";

- (NSTimeInterval)custom_acceptEventTime {
    return [objc_getAssociatedObject(self, &kAcceptEventTimeKey) doubleValue];
}

- (void)setCustom_acceptEventTime:(NSTimeInterval)custom_acceptEventTime {
    objc_setAssociatedObject(self, &kAcceptEventTimeKey, @(custom_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end

2.拦截调用


应用场景:在程序运行过程中,如果没有找到相应方法的实现,就会转向拦截调用。通俗点讲,再找不到调用方法,程序崩溃之前,我们有机会重新NSObject的四个方法来拦截处理。
+ (BOOL)resolveClassMethod:(SEL)sel;
+ (BOOL)resolveInstanceMethod:(SEL)sel;
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
说明:
  • 第一个方法是当你调用一个不存在的类方法的时候,会调用此方法,默认返回NO,你可以加上自己的实现后返回YES。
  • 第二个方法是当你调用一个不存在的实例方法时会调用,与方法一类似。
  • 第三个方法是将你调用的不存在的方法重定向到一个其他声明了这个方法的类,只需要你返回一个有这个方法的target。
  • 最后一个方法是将你调用的不存在的方法打包成NSInvocation传给你。做完你自己的处理后,调用invokeWithTarget:方法让某个target触发这个方法。
代码示例:

1.对象在接收到无法响应的消息后,首先会调用所属类的 +(BOOL)resolveInstanceMethod:(SEL)sel 方法
[self performSelector:@selector(doSomething)];
void dynamicMethod(id self, SEL _cmd) {
    NSLog(@"doSomething %@",NSStringFromSelector(_cmd));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 消息转发
    if (sel == @selector(doSomething)) {
        NSLog(@"动态添加方法");
        class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
上面与3.添加方法中示例相似

2.如果在+(BOOL)resolveInstanceMethod:(SEL)sel 中没有找到或者添加方法,消息会继续往下传递,会调用方法- (id)forwardingTargetForSelector:(SEL)aSelector,看看有没有对象可以执行这个方法,我们在第一个控制器FirstViewController中注掉上述+(BOOL)resolveInstanceMethod:(SEL)sel 方法的实现。在控制器SecondViewController中添加一个方法:
@implementation SecondViewController
- (void)myMethod {
    NSLog(@"%@",NSStringFromClass([self class]));
}

重点来了,现在我想在FirstViewController调用SecondViewController中的- (void)myMethod这个方法,因为这两个控制器并无继承关系,按照正常的逻辑肯定是不会执行的,因此而程序崩溃。现在我们来处理一下消息的转发,在FirstViewController添加如下:
    [self performSelector:@selector(myMethod)];
    - (id)forwardingTargetForSelector:(SEL)aSelector {
        Class class = NSClassFromString(@"SecondViewController");
        UIViewController *viewController = [[class alloc] init];
        if (aSelector == @selector(myMethod)) {
            return viewController;
        }
        return nil;
    }

这样我们就在FirstViewController调用SecondViewController中的- (void)myMethod这个方法,消息转发成功,同时,也相当于完成了一个多继承。

3.添加方法


使用场景:加载一个类到内存,需要给每个方法生成映射表,这样是比较耗费资源的。我们可以使用runtime动态为某个类添加方法。
代码示例:
person没有cry方法,但是可以通过performSelector:调用,如果就这样运行我们的程序,必然会出现错误 -[SFPerson cry]: unrecognized selector sent to instance 0x7fd4b1425a70
SFPerson *person = [[SFPerson alloc] init];
[person performSelector:@selector(cry)];
#import "SFPerson.h"
#import <objc/runtime.h>

@implementation SFPerson

// 默认方法都有两个隐式参数
void cry(id self, SEL sel) {
    NSLog(@"%@, %@",self, NSStringFromSelector(sel));
}

// 一个对象调用未实现的方法,会调用resolveInstanceMethod:进行处理,并且会把对象的方法列表传过来
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    
    if (sel == @selector(cry)) {
        // 参数说明
        // 给哪个类添加方法,这里我们写成self
        // 添加的方法名称,本例是重写的拦截调用传过来的selector
        // 添加方法的函数实现(函数地址),C方法的实现可以直接获得。如果是OC方法,可以用+(IMP)instanceMethodForSelector:(SEL)aSelector方法获得
        // 函数的类型(返回值+参数类型),v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)cry, "v@:");
	return YES;
    }
    return [super resolveInstanceMethod:sel];
    
}
@end

4.关联对象


应用场景:当我们想为现有类添加一个属性时,如果继承得到一个子类,那么就显得有点麻烦。这时候我们可为其分类添加一个属性(除非万不得已,否则不建议在分类中添加属性,尽量在本类中添加),本质是给这个类添加属性上的关联。
在交换方法代码示例中已有体现,此处不再赘述。

5.字典转模型


应用场景:我们从网络或者服务器拿到数据后(一般来说,会返回一个字典),需要封装成对应的模型,供我们方便实用。那么我们就可根据字典中的key自动生成对应的属性。
代码示例:
    const char kPropertiesKey = '\0';
    // 判断是否存在关联对象
    NSArray *propertyLists = objc_getAssociatedObject(self, &kPropertiesKey);
    
    if (propertyLists) {
        return propertyLists;
    }
    
    // 获取本类的属性
    unsigned int propertyCount = 0;// 属性的计数指针
    
    // 获取所有属性,并存储到数组中
    
    objc_property_t *propertys = class_copyPropertyList([self class], &propertyCount);
    
    NSMutableArray *allNames = [NSMutableArray arrayWithCapacity:propertyCount];
    
    for (unsigned int i = 0;  i < propertyCount; i++) {
        // 获取到属性
        objc_property_t property = propertys[i];
        // 获取到属性的名称
        const char *propertyName = property_getName(property);
        
        [allNames addObject:[NSString stringWithUTF8String:propertyName]];
    }
    
    free(propertys);
    
    objc_setAssociatedObject(self, &kPropertiesKey, allNames, OBJC_ASSOCIATION_COPY_NONATOMIC);
    
    return [allNames copy];


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值