iOS中Runtime的常用方法

作者: Legend_劉先森 | 来源:发表于2017-02-19 23:49 被阅读535次

Runtime是什么?

Apple关于Runtime的详细文档链接:Runtime Guide
其实大家对Runtime算是既熟悉又陌生的,因为在学习Objective-C的时候就知道这门语言的强大之处在于其动态性,那么什么是动态性呢,这个时候就会接触到Runtime的概念了,顾名思义,Runtime是在一种进行时的特性,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用,就是说在工程编译阶段才会确定所有函数的执行路径等,这个就是进行时的特色了。
那么知道了这个特点,对于我们来说与什么实际价值呢?
这边文章介绍了几个常用的场景可以让你快速的领悟Runtime的精神并且可以拿去分(zhang)享(B)。。。

Runtime实现原理R简介

Runtime是一套比较底层的纯C语言API, 属于一个C语言库, 包含了很多底层的C语言API。
在我们平时编写的OC代码中, 程序运行过程时, 其实最终都是转成了Runtime的C语言代码。

Runtime算是Objective-C的幕后工作者!

例如,下面一个创建Dog对象的方法中,

在OC中 : 
[[Dog alloc] init] 
在Runtime中就变成 : 
objc_msgSend(objc_msgSend(“Dog” , “alloc”), “init”)

Runtime用来做什么?

1、在程序运行过程中, 动态创建一个类(比如KVO的底层实现)
2、在程序运行过程中, 动态地为某个类添加属性\方法, 修改属性值\方法
3、遍历一个类的所有成员变量(属性)\所有方法
例如:我们需要对一个类的属性进行归档解档的时候属性特别的多,这时候,我们就会写很多对应的代码,但是如果使用了runtime就可以动态设置!
4、就是今天要着重讲的最常用到的一些使用:可以利用Runtime,避免UIButton 重复点击, 可变数组和可变字典为nil,或者数组越界导致的Crash问题。

利用Runtime解决数组字典的崩溃问题

适用场景:当我们从后台请求到的数据,需要把其中一个插入到数组的时候,需要先判断该对象是否为空值,非空才能插入,否则会引起崩溃。Runtime可以从根本上解决,即使我插入的是空值,也不会引起崩溃。

Method Swizzling

在Objective-C中调用一个方法,其实是向一个对象发送消息,而查找消息的唯一依据是selector的名字。所以,我们可以实现在运行时交换selector对应的方法实现以达到效果。
每个类都有一个方法列表,存放着SEL(selector)的名字和方法实现的映射关系。IMP(Implementation Method Path)有点类似函数指针,指向具体的Method实现。
关于SEL与IMP请参考文章:Class、IMP、SEL是什么?

在+load方法中进行

Swizzling应该在+load方法中实现,因为+load方法可以保证在类最开始加载时会调用。因为method swizzling的影响范围是全局的,所以应该放在最保险的地方来处理是非常重要的。+load能够保证在类初始化的时候一定会被加载,这可以保证统一性。试想一下,若是在实际时需要的时候才去交换,那么无法达到全局处理的效果,而且若是临时使用的,在使用后没有及时地使用swizzling将系统方法与我们自定义的方法实现交换回来,那么后续的调用系统API就可能出问题。
类文件在工程中,一定会加载,因此可以保证+load会被调用。

使用dispatch_once保证只交换一次,确保性能

方法交换应该要线程安全,而且保证只交换一次,除非只是临时交换使用,在使用完成后又交换回来。
最常用的用法是在+load方法中使用dispatch_once来保证交换是安全的。因为swizzling会改变全局,我们需要在运行时采取相应的防范措施。保证原子操作就是一个措施,确保代码即使在多线程环境下也只会被执行一次。而diapatch_once就提供这些保障,因此我们应该将其加入到swizzling的使用标准规范中。

注意使用+load方法和dispatch_once确保实现!

创建一个交换IMP的通用扩展很必要

@interface NSObject (Swizzling) 

+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector; 

@end


#import "NSObject+Swizzling.h"

#import <objc/runtime.h>

// 实现代码如下

@implementation NSObject (Swizzling)

+ (void)swizzleSelector:(SEL)originalSelector withSwizzledSelector:(SEL)swizzledSelector 
{
    Class class = [self class];

    Method originalMethod = class_getInstanceMethod(class, originalSelector);

    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

// 若已经存在,则添加会失败

    BOOL didAddMethod = class_addMethod(class,originalSelector,

    method_getImplementation(swizzledMethod),

    method_getTypeEncoding(swizzledMethod));

// 若原来的方法并不存在,则添加即可

    if (didAddMethod) {

        class_replaceMethod(class,swizzledSelector,

        method_getImplementation(originalMethod),

        method_getTypeEncoding(originalMethod));

    } else {

        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end

因为方法可能不是在这个类里,可能是在其父类中才有实现,因此先尝试添加方法的实现,若添加成功了,则直接替换一下实现即可。若添加失败了,说明已经存在这个方法实现了,则只需要交换这两个方法的实现就可以了。

尽量使用method_exchangeImplementations函数来交换,因为它是原子操作的,线程安全。尽量不要自己手动写这样的代码:

IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);

NSMutableArray中

还记得那些调用数组的addObject:方法加入一个nil值是的崩溃情景吗?还记得[__NSPlaceholderArray initWithObjects:count:]因为有nil值而崩溃的提示吗?还记得调用objectAtIndex:时出现崩溃提示empty数组问题吗?那么通过swizzling特性,我们可以做到不让它崩溃,而只是打印一些有用的日志信息。

我们先来看看NSMutableArray的扩展实现:

#import "NSMutableArray+Swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"

@implementation NSMutableArray (Swizzling)

+ (void)load {
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    [self swizzleSelector:@selector(removeObject:)withSwizzledSelector:@selector(safeRemoveObject:)];
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(addObject:) withSwizzledSelector:@selector(safeAddObject:)];
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(removeObjectAtIndex:) withSwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(insertObject:atIndex:) withSwizzledSelector:@selector(safeInsertObject:atIndex:)];
    [objc_getClass("__NSPlaceholderArray") swizzleSelector:@selector(initWithObjects:count:) withSwizzledSelector:@selector(safeInitWithObjects:count:)];
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(objectAtIndex:) withSwizzledSelector:@selector(safeObjectAtIndex:)];
  });
}

- (instancetype)safeInitWithObjects:(const id  _Nonnull     __unsafe_unretained *)objects count:(NSUInteger)cnt
 {
    BOOL hasNilObject = NO;
    for (NSUInteger i = 0; i < cnt; i++) {
        if ([objects[i] isKindOfClass:[NSArray class]]) {
        NSLog(@"%@", objects[i]);
    }
    if (objects[i] == nil) {
        hasNilObject = YES;
        NSLog(@"%s object at index %lu is nil, it will be     filtered", __FUNCTION__, i);

//#if DEBUG
//      // 如果可以对数组中为nil的元素信息打印出来,增加更容    易读懂的日志信息,这对于我们改bug就好定位多了
//      NSString *errorMsg = [NSString     stringWithFormat:@"数组元素不能为nil,其index为: %lu", i];
//      NSAssert(objects[i] != nil, errorMsg);
//#endif
    }
 }

  // 因为有值为nil的元素,那么我们可以过滤掉值为nil的元素
  if (hasNilObject) {
      id __unsafe_unretained newObjects[cnt];
      NSUInteger index = 0;
      for (NSUInteger i = 0; i < cnt; ++i) {
          if (objects[i] != nil) {
              newObjects[index++] = objects[i];
          }
      }
      return [self safeInitWithObjects:newObjects count:index];
  }
  return [self safeInitWithObjects:objects count:cnt];
}

- (void)safeAddObject:(id)obj {
    if (obj == nil) {
        NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
    } else {
        [self safeAddObject:obj];
    }
}
- (void)safeRemoveObject:(id)obj {
   if (obj == nil) {
      NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
      return;
   }
   [self safeRemoveObject:obj];
}

- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
    if (anObject == nil) {
        NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
    } else if (index > self.count) {
        NSLog(@"%s index is invalid", __FUNCTION__);
    } else {
        [self safeInsertObject:anObject atIndex:index];
    }
  }

- (id)safeObjectAtIndex:(NSUInteger)index {
    if (self.count == 0) {
        NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
        return nil;
    }
    if (index > self.count) {
        NSLog(@"%s index out of bounds in array", __FUNCTION__);
        return nil;
    }
    return [self safeObjectAtIndex:index];
}

- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
    if (self.count <= 0) {
        NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
        return;
    }
    if (index >= self.count) {
        NSLog(@"%s index out of bound", __FUNCTION__);
        return;
    }
    [self safeRemoveObjectAtIndex:index];
}
@end

然后,我们测试nil值的情况,是否还会崩溃呢?

NSMutableArray *array = [@[@"value", @"value1"]     mutableCopy];
[array lastObject];

[array removeObject:@"value"];
[array removeObject:nil];
[array addObject:@"12"];
[array addObject:nil];
[array insertObject:nil atIndex:0];
[array insertObject:@"sdf" atIndex:10];
[array objectAtIndex:100];
[array removeObjectAtIndex:10];

NSMutableArray *anotherArray = [[NSMutableArray alloc] init];
[anotherArray objectAtIndex:0];

NSString *nilStr = nil;
NSArray *array1 = @[@"ara", @"sdf", @"dsfdsf", nilStr];
NSLog(@"array1.count = %lu", array1.count);

// 测试数组中有数组
NSArray *array2 = @[@[@"12323", @"nsdf", nilStr],     @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];

都不崩溃了,而且还打印出崩溃原因。是不是很神奇?如果充分利用这种特性,是不是可以给我们带来很多便利之处?

上面只是swizzling的一种应用场景而已。其实利用swizzling特性还可以做很多事情的,比如处理按钮重复点击问题等。

NSMutableDictionary中

#import <Foundation/Foundation.h>

@interface NSMutableDictionary (Swizzling)
@end


#import "NSMutableDictionary+Swizzling.h"
#import <objc/runtime.h>
#import "NSObject+Swizzling.h"

@implementation NSMutableDictionary (Swizzling)

+(void)load
 {
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{

    [objc_getClass("__NSDictionaryM") swizzleSelector:@selector(setValue:forKey:) withSwizzledSelector:@selector(safeSetValue:forKey:)];
    [objc_getClass("__NSDictionaryM") swizzleSelector:@selector(setObject:forKey:) withSwizzledSelector:@selector(safeSetObject:forKey:)];
    [objc_getClass("__NSDictionaryM") swizzleSelector:@selector(removeObjectForKey:) withSwizzledSelector:@selector(safeRemoveObjectForKey:)];
         
     });
 }
 - (void)safeSetValue:(id)value forKey:(NSString *)key
 {
     if (key == nil || value == nil || [key isEqual:[NSNull null]] || [value isEqual:[NSNull null]]) {
 #if DEBUG
        NSLog(@"%s call -safeSetValue:forKey:, key或vale为nil或null", __FUNCTION__);
 #endif
         return;
     }
    
     [self safeSetValue:value forKey:key];
 }
 
 - (void)safeSetObject:(id)anObject forKey:(id<NSCopying>)aKey
 {
     if (aKey == nil || anObject == nil || [anObject isEqual:[NSNull null]]) {
 #if DEBUG
         NSLog(@"%s call -safeSetObject:forKey:, key或vale为nil或null", __FUNCTION__);
 #endif
         return;
     }
     
     [self safeSetObject:anObject forKey:aKey];
 }
 
 - (void)safeRemoveObjectForKey:(id)aKey
 {
     if (aKey == nil || [aKey isEqual:[NSNull null]] ) {
 #if DEBUG
         NSLog(@"%s call -safeRemoveObjectForKey:, aKey为nil或null", __FUNCTION__);
 #endif
         return;
     }
     [self safeRemoveObjectForKey:aKey];
 }
 @end

UIButton避免重复恶意点击

#import <UIKit/UIKit.h>

#define defaultInterval 0.5  //默认时间间隔

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



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

@interface UIButton()
/**bool 类型 YES 不允许点击   NO 允许点击   设置是否执行点UI方法*/
@property (nonatomic, assign) BOOL isIgnoreEvent;
@end
@implementation UIButton (Swizzling)

+(void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        [objc_getClass("UIButton") swizzleSelector:@selector(sendAction:to:forEvent:) withSwizzledSelector:@selector(customSendAction:to:forEvent:)];
        
    });
}

- (void)customSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{

    if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) {
        
        self.timeInterval =self.timeInterval ==0 ?defaultInterval:self.timeInterval;
        if (self.isIgnoreEvent){
            return;
        }else if (self.timeInterval > 0){
            [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval];
        }
    }
    //此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环
    self.isIgnoreEvent = YES;
    [self customSendAction:action to:target forEvent:event];
}

- (NSTimeInterval)timeInterval
{
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval
{
    objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
}
//runtime 动态绑定 属性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
    // 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
    objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnoreEvent{
    //_cmd == @select(isIgnore); 和set方法里一致
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)resetState{
    [self setIsIgnoreEvent:NO];
}
@end

大功告成,其他使用场景后续更新。

timg.gif

相关文章

网友评论

  • iOS_小张:请问一下交换 objectForKey: 方法,如何实现?
    90b4270312ac:@Legend_劉先森 我这边需求是将项目中的NSUserDefaults存储的内容进行加密,所以用自定义方法替换了setObject:forKey:和objectForKey:,分别在新的方法里进行对数据的加解密,但是ZDY_newObjForKey方法里用修改的object值return后老是崩溃,求解

    @implementation NSUserDefaults (ZDYUserDefaults)

    + (void)load {
    Method fromMethod = class_getInstanceMethod([self class], @selector(setObject:forKey:));
    Method toMethod = class_getInstanceMethod([self class], @selector(ZDY_setNewObj:forKey:));
    method_exchangeImplementations(fromMethod, toMethod);

    Method fromMethod1 = class_getInstanceMethod([self class], @selector(objectForKey:));
    Method toMethod1 = class_getInstanceMethod([self class], @selector(ZDY_newObjForKey:));
    method_exchangeImplementations(fromMethod1, toMethod1);
    }

    - (void)ZDY_setNewObj:(id)object forKey:(id)key {

    object = @"1111111";
    [self ZDY_setNewObj:object forKey:key];
    }

    - (id)ZDY_newObjForKey:(id)key{
    id object = [self ZDY_newObjForKey:key];
    if ([object isKindOfClass:[NSNull class]]) {
    object = @"";
    }else{
    object = @"222222";
    }
    return object;
    }

    @end:disappointed_relieved: :pensive: :pensive:
    Legend_劉先森:@安夏_ff68 你是指类簇类的实现吗,是需要指定类簇进行交换的,有几种类簇可以自行Google
    90b4270312ac:请问你objectForKey的解决了吗,我这边修改了object的值后return,老是报错

本文标题:iOS中Runtime的常用方法

本文链接:https://www.haomeiwen.com/subject/aaqswttx.html