Runtime Method Swizzling開發實例匯總

wxwj7573 8年前發布 | 5K 次閱讀 iOS開發 移動開發

什么是Method Swizzling,在iOS開發中它有什么作用?

簡單來說我們主要是使用Method Swizzling來把系統的方法交換為我們自己的方法,從而給系統方法添加一些我們想要的功能。已經有很多文章從各個角度解釋Method Swizzling的涵義甚至實現機制,該篇文章主要列舉Method Swizzling在開發中的一些現實用例。 希望閱讀文章的朋友們也可以提供一些文中尚未舉出的例子。

在列舉之前,我們可以將Method Swizzling功能封裝為類方法,作為NSObject的類別,這樣我們后續調用也會方便些。

#import
#import
@interface NSObject (Swizzling) 
 
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelectorbySwizzledSelector:(SEL)swizzledSelector; 
@end
#import "NSObject+Swizzling.h"
@implementationNSObject (Swizzling)
 
+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelectorbySwizzledSelector:(SEL)swizzledSelector{
    Class class = [self class];
    //原有方法
    MethodoriginalMethod = class_getInstanceMethod(class, originalSelector);
    //替換原有方法的新方法
    MethodswizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    //先嘗試給源SEL添加IMP,這里是為了避免源SEL沒有實現IMP的情況
    BOOL didAddMethod = class_addMethod(class,originalSelector,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {//添加成功:說明源SEL沒有實現IMP,將源SEL的IMP替換到交換SEL的IMP
        class_replaceMethod(class,swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {//添加失敗:說明源SEL已經有IMP,直接將兩個SEL的IMP交換即可
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end

實例一:替換ViewController生命周期方法

App跳轉到某具有網絡請求的界面時,為了用戶體驗效果常會添加加載欄或進度條來顯示當前請求情況或進度。這種界面都會存在這樣一個問題,在請求較慢時,用戶手動退出界面,這時候需要去除加載欄。

當然可以依次在每個界面的viewWillDisappear方法中添加去除方法,但如果類似的界面過多,一味的復制粘貼也不是方法。這時候就能體現Method Swizzling的作用了,我們可以替換系統的viewWillDisappear方法,使得每當執行該方法時即自動去除加載欄。

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementationUIViewController (Swizzling)
 
+ (void)load {
    static dispatch_once_tonceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)];
    });
}
 
- (void)sure_viewWillDisappear:(BOOL)animated {
    [self sure_viewWillDisappear:animated];
    [SVProgressHUDdismiss];
}

代碼如上,這樣就不用考慮界面是否移除加載欄的問題了。

實例二:解決獲取索引、添加、刪除元素越界崩潰問題

對于NSArray、NSDictionary、NSMutableArray、NSMutableDictionary不免會進行索引訪問、添加、刪除元素的操作,越界問題也是很常見,這時我們可以通過Method Swizzling解決這些問題,越界給予提示防止崩潰。

這里以NSMutableArray為例說明

#import "NSMutableArray+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementationNSMutableArray (Swizzling)
 
+ (void)load {
    static dispatch_once_tonceToken;
    dispatch_once(&onceToken, ^{
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObject:) bySwizzledSelector:@selector(safeRemoveObject:) ];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(addObject:) bySwizzledSelector:@selector(safeAddObject:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObjectAtIndex:) bySwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(insertObject:atIndex:) bySwizzledSelector:@selector(safeInsertObject:atIndex:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(objectAtIndex:) bySwizzledSelector:@selector(safeObjectAtIndex:)];
    });
}
- (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)anObjectatIndex:(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:anObjectatIndex: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 = self.count) {
        NSLog(@"%s index out of bound", __FUNCTION__);
        return;
    }
    [self safeRemoveObjectAtIndex:index];
}
@end

對應大家可以舉一反三,相應的實現添加、刪除等,以及NSArray、NSDictionary等操作,因代碼篇幅較大,這里就不一一書寫了。

這里沒有使用self來調用,而是使用objc_getClass(“__NSArrayM”)來調用的。因為NSMutableArray的真實類只能通過后者來獲取,而不能通過[self class]來獲取,而method swizzling只對真實的類起作用。這里就涉及到一個小知識點:類簇。補充以上對象對應類簇表。

實例三:防止按鈕重復暴力點擊

程序中大量按鈕沒有做連續響應的校驗,連續點擊出現了很多不必要的問題,例如發表帖子操作,用戶手快點擊多次,就會導致同一帖子發布多次。

#import
//默認時間間隔
#define defaultInterval 1
@interface UIButton (Swizzling)
//點擊間隔
@property (nonatomic, assign) NSTimeIntervaltimeInterval;
//用于設置單個按鈕不需要被hook
@property (nonatomic, assign) BOOL isIgnore;
@end
#import "UIButton+Swizzling.h"
#import "NSObject+Swizzling.h"
 
@implementationUIButton (Swizzling)
 
+ (void)load {
    static dispatch_once_tonceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)];
    });
}
 
- (NSTimeInterval)timeInterval{
    return [objc_getAssociatedObject(self, _cmd) doubleValue];
}
- (void)setTimeInterval:(NSTimeInterval)timeInterval{
    objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 
}
//當按鈕點擊事件sendAction 時將會執行sure_SendAction
- (void)sure_SendAction:(SEL)actionto:(id)targetforEvent:(UIEvent *)event{
    if (self.isIgnore) {
        //不需要被hook
        [self sure_SendAction:actionto:targetforEvent:event];
        return;
    }
    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:nilafterDelay:self.timeInterval];
        }
    }
    //此處 methodA和methodB方法IMP互換了,實際上執行 sendAction;所以不會死循環
    self.isIgnoreEvent = YES;
    [self sure_SendAction:actionto:targetforEvent:event];
}
//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)setIsIgnore:(BOOL)isIgnore{
    // 注意BOOL類型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯,否則set方法會賦值出錯
    objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isIgnore{
    //_cmd == @select(isIgnore); 和set方法里一致
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}
- (void)resetState{
    [self setIsIgnoreEvent:NO];
}
@end

實例四:全局更換控件初始效果

以UILabel為例,在項目比較成熟的基礎上,應用中需要引入新的字體,需要更換所有Label的默認字體,但是同時,對于一些特殊設置了字體的label又不需要更換。乍看起來,這個問題確實十分棘手,首先項目比較大,一個一個設置所有使用到的label的font工作量是巨大的,并且在許多動態展示的界面中,可能會漏掉一些label,產生bug。其次,項目中的label來源并不唯一,有用代碼創建的,有xib和storyBoard中的,這也將浪費很大的精力。這時Method Swizzling可以解決此問題,避免繁瑣的操作。

#import "UILabel+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementationUILabel (Swizzling)
 
+ (void)load {
    static dispatch_once_tonceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(init) bySwizzledSelector:@selector(sure_Init)];
        [self methodSwizzlingWithOriginalSelector:@selector(initWithFrame:) bySwizzledSelector:@selector(sure_InitWithFrame:)];
        [self methodSwizzlingWithOriginalSelector:@selector(awakeFromNib) bySwizzledSelector:@selector(sure_AwakeFromNib)];
    });
}
- (instancetype)sure_Init{
    id__self = [self sure_Init];
    UIFont * font = [UIFontfontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}
-(instancetype)sure_InitWithFrame:(CGRect)rect{
    id__self = [self sure_InitWithFrame:rect];
    UIFont * font = [UIFontfontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}
-(void)sure_AwakeFromNib{
    [self sure_AwakeFromNib];
    UIFont * font = [UIFontfontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
}
@end

這一實例個人認為使用率可能不高,對于產品的設計這些點都是已經確定好的,更改的幾率很低。況且我們也可以使用appearance來進行統一設置。

實例五:App熱修復

因為AppStore上線審核時間較長,且如果在線上版本出現bug修復起來也是很困難,這時App熱修復就可以解決此問題。熱修復即在不更改線上版本的前提下,對線上版本進行更新甚至添加模塊。國內比較好的熱修復技術:JSPatch。JSPatch能做到通過JS調用和改寫OC方法最根本的原因是Objective-C是動態語言,OC上所有方法的調用/類的生成都通過Objective-C Runtime在運行時進行,我們可以通過類名/方法名反射得到相應的類和方法,進而替換出現bug的方法或者添加方法等。bang的博客上有詳細的描述有興趣可以參考,這里就不贅述了。

暫時寫到這里,部分內容來源于網絡,后續還會更新。

最后,還是希望看過的朋友們可以提供一些自己開發中的實例加以補充。

 

來自:http://ios.jobbole.com/90809/

 

 本文由用戶 wxwj7573 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!