iOS安全防護(hù)之一:方法找不到

前言:在程序發(fā)版之后雁歌,會偶發(fā)地出現(xiàn)消息找不到而導(dǎo)致的Crash(unrecognized selector sent to class ),最知名地后臺返回null對象柱查。我們可能判斷不嚴(yán)謹(jǐn)廓俭,當(dāng)做字典或數(shù)組處理,由于OC的動(dòng)態(tài)性唉工,就會發(fā)生錯(cuò)誤研乒。誠然,良好地代碼邏輯可以避免這些問題淋硝。但是對于已經(jīng)非我們自己寫地代碼雹熬,或者其他地SDK引起地問題,讓我們防不勝防谣膳。所以橄唬,在Release下添加一個(gè)方法找不地防護(hù)還有很有作用地,至少保證不Crash参歹,還可以把錯(cuò)誤統(tǒng)計(jì)仰楚,方便后續(xù)地維護(hù)!

OC是消息機(jī)制犬庇,方法調(diào)用就是消息發(fā)送僧界,這個(gè)流程不清楚的同學(xué)可以看看我前面的文章。當(dāng)一個(gè)消息找不到時(shí)候就會進(jìn)行消息轉(zhuǎn)發(fā)臭挽。這時(shí)捂襟,有三次拯救地機(jī)會。

  1. 首先調(diào)用 +(BOOL)resolveInstanceMethod:(SEL)sel或者+ (BOOL)resolveClassMethod:(SEL)sel
    給我們一次動(dòng)態(tài)實(shí)現(xiàn)的機(jī)會,但是這個(gè)不合適欢峰,這樣會使類添加一個(gè)這個(gè)方法葬荷,我們也不知道方法地具體實(shí)現(xiàn)
  2. 調(diào)用-(id)forwardingTargetForSelector:(SEL)aSelector
    給我們一次轉(zhuǎn)發(fā)給其他對象,如果返回一個(gè)非nil.消息將會轉(zhuǎn)發(fā)給該對象.這個(gè)也不合適涨共,因?yàn)槲覀円膊恢酪l(fā)給誰處理
  3. 調(diào)用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法來獲取這個(gè)選擇子的方法簽名.然后在- (void)forwardInvocation:(NSInvocation *)anInvocation處理,這個(gè)就是我們想用的。就是要HOOK這兩個(gè)方法宠漩。

核心原理举反,利用Method-Swizzling達(dá)到HOOK這兩個(gè)動(dòng)態(tài)解析方法,為我所用扒吁。廢話不多說火鼻,上代碼,只所以寫在Load方法里面是因?yàn)閘oad方法會在啟動(dòng)之前自動(dòng)的調(diào)用雕崩。用dispatch_once防止有人手動(dòng)調(diào)用load方法魁索,防止再次交換就等于沒有交換。

+ (void)load   {
    
    static dispatch_once_t onceToken1;
    dispatch_once(&onceToken1, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(forwardInvocation:);
        SEL swizzledSelector = @selector(jessica_forwardInvocation:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
    
    static dispatch_once_t onceToken2;
    dispatch_once(&onceToken2, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(methodSignatureForSelector:);
        SEL swizzledSelector = @selector(jessica_methodSignatureForSelector:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
    
}

接下來就是一個(gè)有錯(cuò)誤的實(shí)現(xiàn)方法盼铁,原理非常簡單粗蔚,jessica_methodSignatureForSelector一定不能返回nil。所以當(dāng)他解析不了地是時(shí)候強(qiáng)行給他一個(gè)NSMethodSignature饶火,之所以這個(gè)寫是因?yàn)樘O果地編碼規(guī)則鹏控。jessica_forwardInvocation是能響應(yīng)就去處理,響應(yīng)不了就不處理趁窃。代碼如下

- (void)jessica_forwardInvocation:(NSInvocation *)anInvocation {
    
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self];
    }
}

- (NSMethodSignature *)jessica_methodSignatureForSelector:(SEL)aSelector {
    
    NSMethodSignature *methodSignature = [[self class] instanceMethodSignatureForSelector:aSelector];
    if (methodSignature == nil) {//這里是關(guān)鍵
        
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"@^v^c"];
    }
    return methodSignature;
}

高高興興地集成到代碼里面,但是還沒等這個(gè)發(fā)揮作用急前,每次彈起鍵盤就會crash醒陆。如果我們只看左面地調(diào)用棧因?yàn)槭荱IKit框架,我們啥也看到裆针,這時(shí)候可以用LLDB指令刨摩,輸入bt指令,這樣地調(diào)用棧才能解決問題世吨,發(fā)現(xiàn)問題出現(xiàn)在這個(gè)UIKeyboardInputManagerClient身上澡刹,這個(gè)類調(diào)用methodSignatureForSelector這個(gè)方法,然說得到是一個(gè)nil耘婚。我猜想是這個(gè)類也做了消息轉(zhuǎn)發(fā)罢浇,一開始我地想法非常簡單,我判斷一下這個(gè)類沐祷,是這個(gè)類我就不給他做解析了嚷闭,讓他還是調(diào)用原來地方法,果然可行赖临,但是這樣地方法并不好胞锰,因?yàn)檫@樣地類可能還會有,可能系統(tǒng)升級也會多兢榨。所以不能寫死嗅榕,這時(shí)候runtime又有用了顺饮,我可以利用runtime,去查詢一下該類是否重寫過methodSignatureForSelector這個(gè)方法凌那,如果重寫過我就給你不去處理了兼雄。方便大家集成,我就上完整代碼了案怯!

.h文件

#import <Foundation/Foundation.h>

@interface NSObject (JessicaMessageForwarding_h)

//是否重寫了 methodSignatureForSelector
@property (assign, nonatomic) BOOL isOverriMethodSignatureForSelector;

//是否重寫了forwardInvocation
@property (assign, nonatomic) BOOL isOverriForwardInvocation;
@end

.m文件

#import "NSObject+JessicaMessageForwarding_h.h"
#import <objc/runtime.h>
@implementation NSObject (JessicaMessageForwarding_h)


+ (void)load   {
    
    static dispatch_once_t onceToken1;
    dispatch_once(&onceToken1, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(forwardInvocation:);
        SEL swizzledSelector = @selector(jessica_forwardInvocation:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
    
    static dispatch_once_t onceToken2;
    dispatch_once(&onceToken2, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(methodSignatureForSelector:);
        SEL swizzledSelector = @selector(jessica_methodSignatureForSelector:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
    
}

#pragma mark - Method Swizzling

- (void)jessica_forwardInvocation:(NSInvocation *)anInvocation {
    
    if (self.isOverriForwardInvocation) {
        return [self jessica_forwardInvocation:anInvocation];
    }
    
    if ([self respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self];
    }
}

- (NSMethodSignature *)jessica_methodSignatureForSelector:(SEL)aSelector {

    if (self.isOverriMethodSignatureForSelector) {
        return [self jessica_methodSignatureForSelector:aSelector];
    }
    
    NSMethodSignature *methodSignature = [[self class] instanceMethodSignatureForSelector:aSelector];
    if (methodSignature == nil) {
 #warning 諸如UIKeyboardInputManagerClient 這個(gè)類自己重寫了 methodSignatureForSelector方法, 就得遵循自己地方法
        
        self.isOverriMethodSignatureForSelector = NO;
        self.isOverriForwardInvocation = NO;
        
        for (NSString *methodStr in [self getAllMethodArray]) {
            if ([methodStr isEqualToString:@"methodSignatureForSelector:"]) {
                self.isOverriMethodSignatureForSelector = YES;
            }
            if ([methodStr isEqualToString:@"forwardInvocation:"]) {
                self.isOverriForwardInvocation = YES;
            }
        }
        
        if (self.isOverriMethodSignatureForSelector) {
           return [self jessica_methodSignatureForSelector:aSelector];
        }
        
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"@^v^c"];
    }
    return methodSignature;
}


-(NSArray *)getAllMethodArray{
    u_int count;
    NSMutableArray *arrayM = [NSMutableArray array];
    
    Method *mothList_f = class_copyMethodList([self class],&count) ;
    for (int i = 0; i < count; i++) {
        Method temp_f = mothList_f[i];
        
        SEL name_f = method_getName(temp_f);
        const char * name_s = sel_getName(name_f);
        [arrayM addObject:[NSString stringWithUTF8String:name_s]];
        
    }
    free(mothList_f);
    
    return arrayM.copy;
}

-(BOOL)isOverriMethodSignatureForSelector{
    NSNumber* vale = objc_getAssociatedObject(self, @selector(isOverriMethodSignatureForSelector));
    return [vale boolValue];
}

-(BOOL)isOverriForwardInvocation{
    NSNumber* vale = objc_getAssociatedObject(self, @selector(isOverriForwardInvocation));
    return [vale boolValue];
}

-(void)setIsOverriMethodSignatureForSelector:(BOOL)vale{
    objc_setAssociatedObject(self, @selector(isOverriMethodSignatureForSelector), [NSNumber numberWithBool:vale], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(void)setIsOverriForwardInvocation:(BOOL)vale{
    objc_setAssociatedObject(self, @selector(isOverriForwardInvocation), [NSNumber numberWithBool:vale], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

建議大家集成之前多測試幾個(gè)頁面君旦,沒問題最好只在release下生效,如果在您地代碼里面不兼容嘲碱,發(fā)生異常金砍。請與我聯(lián)系,我會盡我所能地去完善麦锯。如果涉及到runime不懂地地方可以看看我原來地帖子恕稠,也可以與我交流,感謝您地閱讀扶欣。
補(bǔ)充:之前我說的可以錯(cuò)誤統(tǒng)計(jì)鹅巍,就是在 methodSignature = [NSMethodSignature signatureWithObjCTypes:"@vc"]; 這個(gè)代碼地時(shí)候添加一些保存上傳地邏輯,把類名料祠,方法名骆捧,當(dāng)時(shí)地調(diào)用堆棧上傳就好。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末髓绽,一起剝皮案震驚了整個(gè)濱河市敛苇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌顺呕,老刑警劉巖枫攀,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異株茶,居然都是意外死亡来涨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進(jìn)店門启盛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蹦掐,“玉大人,你說我怎么就攤上這事僵闯◇源常” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵棍厂,是天一觀的道長颗味。 經(jīng)常有香客問我,道長牺弹,這世上最難降的妖魔是什么浦马? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任时呀,我火速辦了婚禮,結(jié)果婚禮上晶默,老公的妹妹穿的比我還像新娘谨娜。我一直安慰自己,他們只是感情好磺陡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布趴梢。 她就那樣靜靜地躺著,像睡著了一般币他。 火紅的嫁衣襯著肌膚如雪坞靶。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天蝴悉,我揣著相機(jī)與錄音彰阴,去河邊找鬼。 笑死拍冠,一個(gè)胖子當(dāng)著我的面吹牛尿这,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庆杜,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼射众,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了晃财?” 一聲冷哼從身側(cè)響起叨橱,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拓劝,沒想到半個(gè)月后雏逾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嘉裤,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡郑临,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屑宠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厢洞。...
    茶點(diǎn)故事閱讀 38,605評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖典奉,靈堂內(nèi)的尸體忽然破棺而出躺翻,到底是詐尸還是另有隱情,我是刑警寧澤卫玖,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布公你,位于F島的核電站,受9級特大地震影響假瞬,放射性物質(zhì)發(fā)生泄漏陕靠。R本人自食惡果不足惜迂尝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望剪芥。 院中可真熱鬧垄开,春花似錦、人聲如沸税肪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽益兄。三九已至锻梳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間偏塞,已是汗流浹背唱蒸。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灸叼,地道東北人神汹。 一個(gè)月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像古今,于是被迫代替她去往敵國和親屁魏。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評論 2 348

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