iOS中Runtime的常用方法

Runtime是什么杨帽?

Apple關(guān)于Runtime的詳細(xì)文檔鏈接:Runtime Guide
其實(shí)大家對(duì)Runtime算是既熟悉又陌生的痪署,因?yàn)樵趯W(xué)習(xí)Objective-C的時(shí)候就知道這門語言的強(qiáng)大之處在于其動(dòng)態(tài)性霞势,那么什么是動(dòng)態(tài)性呢,這個(gè)時(shí)候就會(huì)接觸到Runtime的概念了,顧名思義,Runtime是在一種進(jìn)行時(shí)的特性抬伺,只有在真正運(yùn)行的時(shí)候才會(huì)根據(jù)函數(shù)的名稱找到對(duì)應(yīng)的函數(shù)來調(diào)用,就是說在工程編譯階段才會(huì)確定所有函數(shù)的執(zhí)行路徑等灾梦,這個(gè)就是進(jìn)行時(shí)的特色了峡钓。
那么知道了這個(gè)特點(diǎn),對(duì)于我們來說與什么實(shí)際價(jià)值呢若河?
這邊文章介紹了幾個(gè)常用的場景可以讓你快速的領(lǐng)悟Runtime的精神并且可以拿去分(zhuang)享(B)能岩。。萧福。

Runtime實(shí)現(xiàn)原理R簡介

Runtime是一套比較底層的純C語言API, 屬于一個(gè)C語言庫, 包含了很多底層的C語言API拉鹃。
在我們平時(shí)編寫的OC代碼中, 程序運(yùn)行過程時(shí), 其實(shí)最終都是轉(zhuǎn)成了Runtime的C語言代碼。

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

例如毛俏,下面一個(gè)創(chuàng)建Dog對(duì)象的方法中,

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

Runtime用來做什么饲窿?

1煌寇、在程序運(yùn)行過程中, 動(dòng)態(tài)創(chuàng)建一個(gè)類(比如KVO的底層實(shí)現(xiàn))
2、在程序運(yùn)行過程中, 動(dòng)態(tài)地為某個(gè)類添加屬性\方法, 修改屬性值\方法
3逾雄、遍歷一個(gè)類的所有成員變量(屬性)\所有方法
例如:我們需要對(duì)一個(gè)類的屬性進(jìn)行歸檔解檔的時(shí)候?qū)傩蕴貏e的多阀溶,這時(shí)候,我們就會(huì)寫很多對(duì)應(yīng)的代碼鸦泳,但是如果使用了runtime就可以動(dòng)態(tài)設(shè)置银锻!
4、就是今天要著重講的最常用到的一些使用:可以利用Runtime,避免UIButton 重復(fù)點(diǎn)擊, 可變數(shù)組和可變字典為nil,或者數(shù)組越界導(dǎo)致的Crash問題做鹰。

利用Runtime解決數(shù)組字典的崩潰問題

適用場景:當(dāng)我們從后臺(tái)請(qǐng)求到的數(shù)據(jù)击纬,需要把其中一個(gè)插入到數(shù)組的時(shí)候,需要先判斷該對(duì)象是否為空值钾麸,非空才能插入更振,否則會(huì)引起崩潰。Runtime可以從根本上解決饭尝,即使我插入的是空值肯腕,也不會(huì)引起崩潰钥平。

Method Swizzling

在Objective-C中調(diào)用一個(gè)方法,其實(shí)是向一個(gè)對(duì)象發(fā)送消息,而查找消息的唯一依據(jù)是selector的名字捷兰。所以,我們可以實(shí)現(xiàn)在運(yùn)行時(shí)交換selector對(duì)應(yīng)的方法實(shí)現(xiàn)以達(dá)到效果肴甸。
每個(gè)類都有一個(gè)方法列表寂殉,存放著SEL(selector)的名字和方法實(shí)現(xiàn)的映射關(guān)系原在。IMP(Implementation Method Path)有點(diǎn)類似函數(shù)指針,指向具體的Method實(shí)現(xiàn)庶柿。
關(guān)于SEL與IMP請(qǐng)參考文章:Class村怪、IMP、SEL是什么浮庐?

在+load方法中進(jìn)行

Swizzling應(yīng)該在+load方法中實(shí)現(xiàn)审残,因?yàn)?load方法可以保證在類最開始加載時(shí)會(huì)調(diào)用。因?yàn)閙ethod swizzling的影響范圍是全局的搅轿,所以應(yīng)該放在最保險(xiǎn)的地方來處理是非常重要的璧坟。+load能夠保證在類初始化的時(shí)候一定會(huì)被加載既穆,這可以保證統(tǒng)一性幻工。試想一下黎茎,若是在實(shí)際時(shí)需要的時(shí)候才去交換,那么無法達(dá)到全局處理的效果迁酸,而且若是臨時(shí)使用的俭正,在使用后沒有及時(shí)地使用swizzling將系統(tǒng)方法與我們自定義的方法實(shí)現(xiàn)交換回來焙畔,那么后續(xù)的調(diào)用系統(tǒng)API就可能出問題。
類文件在工程中儿惫,一定會(huì)加載,因此可以保證+load會(huì)被調(diào)用肾请。

使用dispatch_once保證只交換一次留搔,確保性能

方法交換應(yīng)該要線程安全,而且保證只交換一次铛铁,除非只是臨時(shí)交換使用饵逐,在使用完成后又交換回來。
最常用的用法是在+load方法中使用dispatch_once來保證交換是安全的掷豺。因?yàn)閟wizzling會(huì)改變?nèi)直∩覀冃枰谶\(yùn)行時(shí)采取相應(yīng)的防范措施。保證原子操作就是一個(gè)措施德频,確保代碼即使在多線程環(huán)境下也只會(huì)被執(zhí)行一次廓奕。而diapatch_once就提供這些保障,因此我們應(yīng)該將其加入到swizzling的使用標(biāo)準(zhǔn)規(guī)范中蒸绩。

注意使用+load方法和dispatch_once確保實(shí)現(xiàn)铃肯!

創(chuàng)建一個(gè)交換IMP的通用擴(kuò)展很必要

@interface NSObject (Swizzling) 

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

@end


#import "NSObject+Swizzling.h"

#import <objc/runtime.h>

// 實(shí)現(xiàn)代碼如下

@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);

// 若已經(jīng)存在押逼,則添加會(huì)失敗

    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

因?yàn)榉椒赡懿皇窃谶@個(gè)類里,可能是在其父類中才有實(shí)現(xiàn)雾消,因此先嘗試添加方法的實(shí)現(xiàn),若添加成功了狂窑,則直接替換一下實(shí)現(xiàn)即可桑腮。若添加失敗了,說明已經(jīng)存在這個(gè)方法實(shí)現(xiàn)了丛晦,則只需要交換這兩個(gè)方法的實(shí)現(xiàn)就可以了添忘。

盡量使用method_exchangeImplementations函數(shù)來交換,因?yàn)樗窃硬僮鞯母拢€程安全仲器。盡量不要自己手動(dòng)寫這樣的代碼:

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

NSMutableArray中

還記得那些調(diào)用數(shù)組的addObject:方法加入一個(gè)nil值是的崩潰情景嗎乏冀?還記得[__NSPlaceholderArray initWithObjects:count:]因?yàn)橛衝il值而崩潰的提示嗎?還記得調(diào)用objectAtIndex:時(shí)出現(xiàn)崩潰提示empty數(shù)組問題嗎昼捍?那么通過swizzling特性肢扯,我們可以做到不讓它崩潰蔚晨,而只是打印一些有用的日志信息。

我們先來看看NSMutableArray的擴(kuò)展實(shí)現(xiàn):

#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
//      // 如果可以對(duì)數(shù)組中為nil的元素信息打印出來银择,增加更容    易讀懂的日志信息浩考,這對(duì)于我們改bug就好定位多了
//      NSString *errorMsg = [NSString     stringWithFormat:@"數(shù)組元素不能為nil被盈,其index為: %lu", i];
//      NSAssert(objects[i] != nil, errorMsg);
//#endif
    }
 }

  // 因?yàn)橛兄禐閚il的元素析蝴,那么我們可以過濾掉值為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值的情況,是否還會(huì)崩潰呢裁赠?

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);

// 測試數(shù)組中有數(shù)組
NSArray *array2 = @[@[@"12323", @"nsdf", nilStr],     @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];

都不崩潰了赴精,而且還打印出崩潰原因蕾哟。是不是很神奇?如果充分利用這種特性谭确,是不是可以給我們帶來很多便利之處逐哈?

上面只是swizzling的一種應(yīng)用場景而已。其實(shí)利用swizzling特性還可以做很多事情的禀梳,比如處理按鈕重復(fù)點(diǎn)擊問題等肠骆。

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避免重復(fù)惡意點(diǎn)擊

#import <UIKit/UIKit.h>

#define defaultInterval 0.5  //默認(rèn)時(shí)間間隔

@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 不允許點(diǎn)擊   NO 允許點(diǎn)擊   設(shè)置是否執(zhí)行點(diǎn)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互換了郊艘,實(shí)際上執(zhí)行 sendAction唯咬;所以不會(huì)死循環(huán)
    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 動(dòng)態(tài)綁定 屬性
- (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
    // 注意BOOL類型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯(cuò)胆胰,否則set方法會(huì)賦值出錯(cuò)
    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

大功告成,其他使用場景后續(xù)更新瞎嬉。

timg.gif
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末氧枣,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子扎谎,更是在濱河造成了極大的恐慌烧董,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件预吆,死亡現(xiàn)場離奇詭異拐叉,居然都是意外死亡胶背,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門廷粒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來红且,“玉大人,你說我怎么就攤上這事嗤放”诔辏” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵岳服,是天一觀的道長吊宋。 經(jīng)常有香客問我颜武,道長拖吼,這世上最難降的妖魔是什么这吻? 我笑而不...
    開封第一講書人閱讀 58,339評(píng)論 1 293
  • 正文 為了忘掉前任橘原,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吩愧。我一直安慰自己,他們只是感情好脐帝,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評(píng)論 6 391
  • 文/花漫 我一把揭開白布堵腹。 她就那樣靜靜地躺著星澳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腿堤。 梳的紋絲不亂的頭發(fā)上如暖,一...
    開封第一講書人閱讀 51,287評(píng)論 1 301
  • 那天盒至,我揣著相機(jī)與錄音,去河邊找鬼枷遂。 笑死登淘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的黔州。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼笆制,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼涣达!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起匆篓,我...
    開封第一講書人閱讀 38,985評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤鸦概,失蹤者是張志新(化名)和其女友劉穎甩骏,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咨察,經(jīng)...
    沈念sama閱讀 45,420評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡福青,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評(píng)論 3 334
  • 正文 我和宋清朗相戀三年素跺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刊愚。...
    茶點(diǎn)故事閱讀 39,779評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡踩验,死狀恐怖箕憾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情袭异,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評(píng)論 5 345
  • 正文 年R本政府宣布碴里,位于F島的核電站咬腋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏根竿。R本人自食惡果不足惜寇壳,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望图贸。 院中可真熱鬧冕广,春花似錦偿洁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锨用。三九已至,卻和暖如春增拥,著一層夾襖步出監(jiān)牢的瞬間掌栅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評(píng)論 1 269
  • 我被黑心中介騙來泰國打工澄耍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人齐莲。 一個(gè)月前我還...
    沈念sama閱讀 47,876評(píng)論 2 370
  • 正文 我出身青樓铅搓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親多望。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氢烘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容