iOS中Runtime的常用方法

Runtime是什么?

Apple关于Runtime的详细文档链接:Runtime Guide
其实大家对Runtime算是既熟悉又陌生的,因为在学习Objective-C的时候就知道这门语言的强大之处在于其动态性,那么什么是动态性呢,这个时候就会接触到Runtime的概念了,顾名思义,Runtime是在一种进行时的特性,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用,就是说在工程编译阶段才会确定所有函数的执行路径等,这个就是进行时的特色了。
那么知道了这个特点,对于我们来说与什么实际价值呢?
这边文章介绍了几个常用的场景可以让你快速的领悟Runtime的精神并且可以拿去分(zhuang)享(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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容