筆記-method-swizzling~那些年,一起遇過的坑

什么是method-swizzling躯畴?

method-swizzling俗稱黑魔法民鼓,在前幾篇文章中說過,在OC中調(diào)用一個(gè)方法蓬抄,其實(shí)就是向一個(gè)對(duì)象發(fā)送消息丰嘉,而查找消息的唯一依據(jù)是selector的名字,通過名字查找到IMP嚷缭。利用OC的動(dòng)態(tài)特性饮亏,可以實(shí)現(xiàn)在運(yùn)行時(shí)偷換selector對(duì)應(yīng)的方法實(shí)現(xiàn),達(dá)到方法實(shí)現(xiàn)交換的效果阅爽。

可以通過下圖去理解


image

在什么地方進(jìn)行方法交換路幸,為什么?

+load方法里付翁,原因有三:(后面文章里會(huì)具體分析這個(gè)+load方法)

  • 執(zhí)行比較早简肴,在main函數(shù)之前調(diào)用
  • 自動(dòng)執(zhí)行,不需要手動(dòng)執(zhí)行
  • 唯一性百侧,不用擔(dān)心被子類覆蓋

坑一:找不到真正的方法歸屬

數(shù)組越界砰识,是開發(fā)中最常見的一個(gè)錯(cuò)誤,看下面代碼

- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataArray = @[@"AA",@"BB",@"CC",@"DD"];
    NSLog(@"%@",self.dataArray[4]);
}

這段代碼執(zhí)行結(jié)果佣渴,相信我不用說了辫狼,奔潰,如下圖(其實(shí)都不用給圖的辛润。膨处。。)


image

其實(shí)可以利用這個(gè)黑魔法频蛔,去規(guī)避App的奔潰灵迫,同時(shí)可以打印出奔潰的類的所在代碼的行數(shù),下面就用這個(gè)機(jī)制晦溪,阻止它的奔潰,代碼如下

@implementation ZBRuntimeTool
+ (void)zb_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
    if (!cls) NSLog(@"傳入的交換類不能為空");
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}
@end

// 分類中
@implementation NSArray (ZB)
+ (void)load {
    [ZBRuntimeTool zb_methodSwizzlingWithClass:self oriSEL:@selector(objectAtIndex:) swizzledSEL:@selector(zb_objectAtIndex:)];
}

- (id)zb_objectAtIndex:(NSUInteger)index {
    if (index > self.count-1) {
        NSLog(@"取值越界了,請(qǐng)記錄 : %lu > %lu",(unsigned long)index,self.count-1);
        return nil;
    }
    return [self zb_objectAtIndex:index];
}
@end

搞定挣跋,是不是感覺很簡(jiǎn)單三圆,編譯運(yùn)行看打印效果,一頓操作猛如虎,結(jié)果發(fā)現(xiàn)運(yùn)行還是奔潰??舟肉,其實(shí)這就是在上面我為什么要給出奔潰的截圖修噪,細(xì)心的小伙伴可能已經(jīng)知道來原因,上面代碼的錯(cuò)誤有兩處:
第一就是方法的歸屬路媚,我們寫的是self黄琼,指的是NSArray,但是通過它報(bào)錯(cuò)的結(jié)果可以知道應(yīng)該是__NSArrayI整慎,它是一個(gè)族類脏款,這個(gè)一定要分清;
第二是我們交換的方法錯(cuò)誤裤园,同樣還是可以通過上面的截圖可以看得出來撤师,我們應(yīng)該交換objectAtIndexedSubscript:方法

最終代碼如下:

@implementation NSArray (ZB)
+ (void)load {
    [ZBRuntimeTool zb_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndexedSubscript:) swizzledSEL:@selector(zb_objectAtIndexedSubscript:)];
}

- (id)zb_objectAtIndexedSubscript:(NSUInteger)index {
    if (index > self.count-1) {
        NSLog(@"取值越界了,請(qǐng)記錄 : %lu > %lu",(unsigned long)index,self.count-1);
        return nil;
    }
    return [self zb_objectAtIndexedSubscript:index];
}
@end

這段代碼可以解決上面所遇到的問題,但是很明顯拧揽,這樣的代碼漏洞百出剃盾,一個(gè)優(yōu)秀的程序員不應(yīng)該寫到這里就停止的,后面篇章中淤袜,會(huì)持續(xù)優(yōu)化它痒谴。

坑二:可能會(huì)主動(dòng)調(diào)用load方法

還可以拿上面的例子來說事,下面代碼中的調(diào)用

- (void)viewDidLoad {
    [super viewDidLoad];
    self.dataArray = @[@"AA",@"BB",@"CC",@"DD"];
    [NSArray load];
    NSLog(@"%@",self.dataArray[4]);
}

運(yùn)行結(jié)果和上面的結(jié)果一摸一樣铡羡,直接奔潰积蔚,而且報(bào)錯(cuò)信息都是一樣的,應(yīng)該能夠想到原因蓖墅,兩次的交換库倘,使得它們都指向了原來的IMP,所以為了防止這種多次調(diào)用的情況论矾,我們可以通過讓它只運(yùn)行一次來解決這個(gè)問題教翩,代碼如下:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [ZBRuntimeTool zb_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndexedSubscript:) swizzledSEL:@selector(zb_objectAtIndexedSubscript:)];
    });
}

對(duì)的,沒有錯(cuò)贪壳,利用單例的方式去解決上面的這個(gè)問題饱亿。

坑三:子類沒有實(shí)現(xiàn)父類的方法,子類交換了父類的方法

下看看下面代碼

// Person類
@interface ZBPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation ZBPerson
- (void)personInstanceMethod {
    NSLog(@"person對(duì)象方法:%s",__func__);
}
@end

// Student類
@interface ZBStudent : ZBPerson
- (void)helloWorld;
+ (void)helloWorld1;
                // 沒有實(shí)現(xiàn)父類ZBPerson的方法
@end

// 調(diào)用
- (void)viewDidLoad {
    [super viewDidLoad];
    ZBStudent *s = [[ZBStudent alloc] init];
    [s personInstanceMethod];

    ZBPerson *p = [[ZBPerson alloc] init];
    [p personInstanceMethod];
}

上面代碼中有兩個(gè)類闰靴,ZBPerson彪笼、ZBStudent,其中ZBStudent是繼承于ZBPerson蚂且,也沒有實(shí)現(xiàn)ZBPerson中的方法personInstanceMethod配猫。

下面進(jìn)行方法交換

@implementation ZBStudent (ZB)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [ZBRuntimeTool zb_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(zb_studentInstanceMethod)];
    });
}

- (void)zb_studentInstanceMethod{
    NSLog(@"ZBStudent分類添加的zb對(duì)象方法:%s",__func__);
    [self zb_studentInstanceMethod];
}
@end

如果ZBRuntimeTool類里面的方法交換還是和上面寫的一樣的話,運(yùn)行結(jié)果肯定是奔潰的杏死,這里就不過多描述這個(gè)錯(cuò)誤泵肄,下面是優(yōu)化過的代碼

+ (void)zb_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"傳入的交換類不能為空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    // 一般交換方法: 交換自己有的方法 -- 走下面 因?yàn)樽约河幸馕短砑臃椒ㄊ?    // 交換自己沒有實(shí)現(xiàn)的方法:
    //   首先第一步:會(huì)先嘗試給自己添加要交換的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再將父類的IMP給swizzle  personInstanceMethod(imp) -> swizzledSEL 
    //oriSEL:personInstanceMethod
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

這段代碼比之前的多了幾步操作捆交,第一步會(huì)先嘗試給自己添加要交換的方法,如果添加成功腐巢,說明本類中沒有實(shí)現(xiàn)這個(gè)方法品追,那么就直接添加一個(gè)swiMethodIMP,然后通過方法class_replaceMethod進(jìn)行替換冯丙;如果添加失敗肉瓦,說明類中存在了這個(gè)方法的IMP,那么可以直接利用method_exchangeImplementations方法進(jìn)行交換胃惜。

坑四:交換根本沒有實(shí)現(xiàn)的方法

顧名思義就是說交換的方法泞莉,不僅本類未實(shí)現(xiàn),其父類中也沒有實(shí)現(xiàn)蛹疯,同樣可以拿上面例子說起戒财,ZBStudent類中的方法helloWorld,在其父類以及本類中捺弦,都沒有實(shí)現(xiàn)饮寞。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [ZBRuntimeTool zb_betterMethodSwizzlingWithClass:self oriSEL:@selector(helloWorld) swizzledSEL:@selector(zb_studentInstanceMethod)];
    });
}

- (void)zb_studentInstanceMethod{
    NSLog(@"ZBStudent分類添加的zb對(duì)象方法:%s",__func__);
    [self zb_studentInstanceMethod];
}

運(yùn)行結(jié)果如下:


image

可以看出,進(jìn)入了遞歸列吼。

思考一下幽崩,這里為什么會(huì)進(jìn)入遞歸的死循環(huán)呢?

分析Method-Swizzling的原理就是進(jìn)行消息IMP的交換寞钥,執(zhí)行上面load方法后慌申,先會(huì)把方法helloWorldIMP指向zb_studentInstanceMethod(IMP),然后把zb_studentInstanceMethod方法的IMP指向helloWorld(IMP)理郑。注意了蹄溉,這里的helloWorld(IMP)為空,意思是方法zb_studentInstanceMethodIMP并沒有改變成功您炉,還是指向了自己的IMP柒爵,和方法helloWorld一樣。所以會(huì)一直調(diào)用方法zb_studentInstanceMethod赚爵,進(jìn)入了死循環(huán)的遞歸棉胀。

優(yōu)化代碼:

+ (void)zb_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    if (!cls) NSLog(@"傳入的交換類不能為空");
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    if (!oriMethod) {
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        // IMP指向了一個(gè)空的block方法(空IMP)
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}

運(yùn)行結(jié)果:


image

牛逼哦 老鐵們!<较ァ唁奢!

坑五:類方法--類方法存在元類中

其實(shí)類方法的method-swizzling和對(duì)象方法基本類似,但是有一個(gè)很大的區(qū)別窝剖,就是類方法存在元類中麻掸,代碼如下

@implementation ZBStudent (ZB)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [ZBRuntimeTool zb_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloWorld1) swizzledSEL:@selector(zb_studentInstanceMethod1)];
    });
}

+ (void)zb_studentInstanceMethod1{
    NSLog(@"ZBStudent分類添加的zb對(duì)象方法:%s",__func__);
    [[self class] zb_studentInstanceMethod1];
}
@end

+ (void)zb_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"傳入的交換類不能為空");
    Method swiCLassMethod = class_getClassMethod([cls class], swizzledSEL);
    Method oriClassMethod = class_getClassMethod([cls class], oriSEL);
    if (!oriClassMethod) {
        class_addMethod(object_getClass([cls class]), oriSEL, method_getImplementation(swiCLassMethod), method_getTypeEncoding(swiCLassMethod));
        method_setImplementation(swiCLassMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }

    BOOL didAddMethod = class_addMethod(object_getClass([cls class]), oriSEL, method_getImplementation(swiCLassMethod), method_getTypeEncoding(swiCLassMethod));
    if (didAddMethod) {
        class_replaceMethod(object_getClass([cls class]), swizzledSEL, method_getImplementation(oriClassMethod), method_getTypeEncoding(oriClassMethod));
    }else {
        method_exchangeImplementations(oriClassMethod, swiCLassMethod);
    }
}

代碼執(zhí)行結(jié)果:

image

嗯,上面代碼中多處用到object_getClass([cls class])赐纱,實(shí)際上這個(gè)就是元類论笔,關(guān)于類方法的一些IMP的交換都是在元類中進(jìn)行的采郎,因?yàn)?strong>類方法存在元類中千所。

以上總結(jié)了一些關(guān)于Method-Swizzling的坑狂魔,當(dāng)然肯定不止這幾個(gè)。在日常開發(fā)中還是慎用淫痰,不過真的很強(qiáng)大最楷。上面的幾種模式下的代碼,值得細(xì)細(xì)閱讀待错,可以增加對(duì)Method-Swizzling的理解籽孙,如有錯(cuò)誤,希望指出火俄,謝謝

如果對(duì)此很感興趣犯建,可以了解一下AOP切面設(shè)計(jì)的源碼實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瓜客,隨后出現(xiàn)的幾起案子适瓦,更是在濱河造成了極大的恐慌,老刑警劉巖谱仪,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玻熙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡疯攒,警方通過查閱死者的電腦和手機(jī)嗦随,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來敬尺,“玉大人枚尼,你說我怎么就攤上這事∩巴蹋” “怎么了署恍?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)呜舒。 經(jīng)常有香客問我锭汛,道長(zhǎng),這世上最難降的妖魔是什么袭蝗? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任唤殴,我火速辦了婚禮,結(jié)果婚禮上到腥,老公的妹妹穿的比我還像新娘朵逝。我一直安慰自己,他們只是感情好乡范,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布配名。 她就那樣靜靜地躺著啤咽,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渠脉。 梳的紋絲不亂的頭發(fā)上宇整,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音芋膘,去河邊找鬼鳞青。 笑死,一個(gè)胖子當(dāng)著我的面吹牛为朋,可吹牛的內(nèi)容都是我干的臂拓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼习寸,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼胶惰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起霞溪,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤孵滞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后威鹿,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剃斧,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年忽你,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了幼东。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡科雳,死狀恐怖根蟹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情糟秘,我是刑警寧澤简逮,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站尿赚,受9級(jí)特大地震影響散庶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凌净,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一悲龟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧冰寻,春花似錦须教、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乐疆。三九已至,卻和暖如春贬养,著一層夾襖步出監(jiān)牢的瞬間挤土,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工煤蚌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留耕挨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓尉桩,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親贪庙。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜘犁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354