runtime method swizzling

前言

在我學(xué)習(xí)runtime的method swizzling特性之前玩祟,有很多同事或者朋友經(jīng)常在我耳邊說起swizzling特性沐悦,一個(gè)個(gè)在我面前說這個(gè)東西千萬不能用,會(huì)引起很多問題的臂拓。但是距境,在我學(xué)習(xí)完這一節(jié)的知識(shí)后,我終于明白其所以然祭阀。

學(xué)習(xí)完swizzling特性后鹉戚,我很喜歡她鲜戒。她就像一把雙刃劍,用好了可以帶你飛抹凳,亂用則會(huì)反傷遏餐。但是,我更相信她的強(qiáng)大赢底,更相信自己夠能駕馭她失都!一起來學(xué)習(xí)吧!

Method Swizzling

試想一下幸冻,蘋果的源碼是閉源的粹庞,我們只有類名和類的屬性、方法等聲明洽损,卻看不到實(shí)現(xiàn)庞溜,這時(shí)候我們?nèi)粝敫淖兤渲幸粋€(gè)方法的實(shí)現(xiàn),有哪些方案呢碑定?筆者想到的有以下幾種方案:

  1. 繼承于這個(gè)類流码,然后通過重寫方法(很常用,比如基類控制器延刘,可以在視圖加載完成時(shí)做一些公共的配置等)
  2. 通過類別重寫方法漫试,暴力搶先(此法太暴力,盡量不要這么做)
  3. swizzling(本文特講內(nèi)容)

Swizzling原理

在Objective-C中調(diào)用一個(gè)方法访娶,其實(shí)是向一個(gè)對(duì)象發(fā)送消息商虐,而查找消息的唯一依據(jù)是selector的名字。所以崖疤,我們可以利用Objective-C的runtime機(jī)制,實(shí)現(xiàn)在運(yùn)行時(shí)交換selector對(duì)應(yīng)的方法實(shí)現(xiàn)以達(dá)到我們的目的典勇。

每個(gè)類都有一個(gè)方法列表劫哼,存放著selector的名字和方法實(shí)現(xiàn)的映射關(guān)系。IMP有點(diǎn)類似函數(shù)指針割笙,指向具體的Method實(shí)現(xiàn)权烧。

我們先看看SEL與IMP之間的關(guān)系圖:

20130718230259187.png

從上圖可以看出來,每一個(gè)SEL與一個(gè)IMP一一對(duì)應(yīng)伤溉,正常情況下通過SEL可以查找到對(duì)應(yīng)消息的IMP實(shí)現(xiàn)般码。

但是,現(xiàn)在我們要做的就是把鏈接線解開乱顾,然后連到我們自定義的函數(shù)的IMP上板祝。當(dāng)然,交換了兩個(gè)SEL的IMP走净,還是可以再次交換回來了券时。交換后變成這樣的孤里,如下圖:

20130718230430859.png

從圖中可以看出,我們通過swizzling特性橘洞,將selectorC的方法實(shí)現(xiàn)IMPc與selectorN的方法實(shí)現(xiàn)IMPn交換了捌袜,當(dāng)我們調(diào)用selectorC,也就是給對(duì)象發(fā)送selectorC消息時(shí)炸枣,所查找到的對(duì)應(yīng)的方法實(shí)現(xiàn)就是IMPn而不是IMPc了虏等。

在+load方法中交換

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)用徐紧。

不要在+initialize中交換

+initialize是類第一次初始化時(shí)才會(huì)被調(diào)用,因?yàn)檫@個(gè)類有可能一直都沒有使用到炭懊,因此這個(gè)類可能永遠(yuǎn)不會(huì)被調(diào)用并级。

類文件雖然在工程中,但是如果沒有任何地方調(diào)用過侮腹,那么是不會(huì)調(diào)用+initialize方法的嘲碧。

使用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ī)范中。

通用交換IMP寫法

網(wǎng)上有很多的版本狸棍,但是有很多是不全面的身害,考慮的范圍不夠全面。下面我們來寫一個(gè)通用的寫法草戈,現(xiàn)在擴(kuò)展到NSObject中塌鸯,因?yàn)镹SObject是根類,這樣其它類都可以使用了:

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

雖然method_exchangeImplementations函數(shù)的本質(zhì)也是這么寫法饥努,但是它內(nèi)部做了線程安全處理。

當(dāng)然八回,我們也可以寫成C語言函數(shù)酷愧,而不是歸屬于類的方法:

// C語言版
void swizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector) {
  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);
  }
}

簡(jiǎn)單使用swizzling

最簡(jiǎn)單的方法實(shí)現(xiàn)交換如下:

Method originalMethod = class_getInstanceMethod([NSArray class], @selector(lastObject));

Method newMedthod = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"gg_lastObject"));

method_exchangeImplementations(originalMethod, newMedthod);

// NSArray提供了這樣的實(shí)現(xiàn)
- (id)gg_lastObject {
  if (self.count == 0) {
    NSLog(@"%s 數(shù)組為空溶浴,直接返回nil", __FUNCTION__);
    
    return nil;
  }
  
  return [self gg_lastObject];
}

看到gg_lastObject這個(gè)方法遞歸調(diào)用自己了嗎?為什么不是調(diào)用return [self lastObject]管引?因?yàn)槲覀兘粨Q了方法的實(shí)現(xiàn)戳葵,那么系統(tǒng)在調(diào)用lastObject方法是,找的是gg_lastObject方法的實(shí)現(xiàn)汉匙,而手動(dòng)調(diào)用gg_lastObject方法時(shí),會(huì)調(diào)用lastObject方法的實(shí)現(xiàn)生蚁。不清楚噩翠?回到前面看一看那個(gè)交換IMP的圖吧!

我們通過使用swizzling只是為了添加個(gè)打影钔丁伤锚?當(dāng)然不是,我們還可以做很多事的志衣。比如屯援,上面我們還做了防崩潰處理猛们。

NSMutableArray擴(kuò)展交換處理崩潰

還記得那些調(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(gg_safeRemoveObject:)];
    
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(addObject:)
     withSwizzledSelector:@selector(gg_safeAddObject:)];
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(removeObjectAtIndex:)
                            withSwizzledSelector:@selector(gg_safeRemoveObjectAtIndex:)];
 
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(insertObject:atIndex:)
     withSwizzledSelector:@selector(gg_insertObject:atIndex:)];
    
    [objc_getClass("__NSPlaceholderArray") swizzleSelector:@selector(initWithObjects:count:) withSwizzledSelector:@selector(gg_initWithObjects:count:)];
    
    [objc_getClass("__NSArrayM") swizzleSelector:@selector(objectAtIndex:) withSwizzledSelector:@selector(gg_objectAtIndex:)];
  });
}

- (instancetype)gg_initWithObjects:(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 gg_initWithObjects:newObjects count:index];
  }

  return [self gg_initWithObjects:objects count:cnt];
}


- (void)gg_safeAddObject:(id)obj {
  if (obj == nil) {
    NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
  } else {
    [self gg_safeAddObject:obj];
  }
}

- (void)gg_safeRemoveObject:(id)obj {
  if (obj == nil) {
    NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
    return;
  }
  
  [self gg_safeRemoveObject:obj];
}

- (void)gg_insertObject:(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 gg_insertObject:anObject atIndex:index];
  }
}

- (id)gg_objectAtIndex:(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 gg_objectAtIndex:index];
}

- (void)gg_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 gg_safeRemoveObjectAtIndex:index];
}

@end

然后恶导,我們測(cè)試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);
  
// 測(cè)試數(shù)組中有數(shù)組
NSArray *array2 = @[@[@"12323", @"nsdf", nilStr], @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];

哈哈惨寿,都不崩潰了邦泄,而且還打印出崩潰原因。是不是很神奇缤沦?如果充分利用這種特性虎韵,是不是可以給我們帶來很多便利之處?

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

參考源代碼:https://github.com/CoderJackyHuang/RuntimeDemo

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末企量,一起剝皮案震驚了整個(gè)濱河市测萎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌届巩,老刑警劉巖硅瞧,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異恕汇,居然都是意外死亡腕唧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門瘾英,熙熙樓的掌柜王于貴愁眉苦臉地迎上來枣接,“玉大人,你說我怎么就攤上這事缺谴〉蹋” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)膀曾。 經(jīng)常有香客問我县爬,道長(zhǎng),這世上最難降的妖魔是什么添谊? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任财喳,我火速辦了婚禮,結(jié)果婚禮上碉钠,老公的妹妹穿的比我還像新娘纲缓。我一直安慰自己,他們只是感情好喊废,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布祝高。 她就那樣靜靜地躺著,像睡著了一般污筷。 火紅的嫁衣襯著肌膚如雪工闺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天瓣蛀,我揣著相機(jī)與錄音陆蟆,去河邊找鬼。 笑死惋增,一個(gè)胖子當(dāng)著我的面吹牛叠殷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播诈皿,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼林束,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了稽亏?” 一聲冷哼從身側(cè)響起壶冒,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎截歉,沒想到半個(gè)月后胖腾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瘪松,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年咸作,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宵睦。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡性宏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出状飞,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布诬辈,位于F島的核電站酵使,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏焙糟。R本人自食惡果不足惜口渔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望穿撮。 院中可真熱鬧缺脉,春花似錦、人聲如沸悦穿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栗柒。三九已至礁扮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瞬沦,已是汗流浹背太伊。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留逛钻,地道東北人僚焦。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像曙痘,于是被迫代替她去往敵國(guó)和親芳悲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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