【瞎搞iOS开发07】Runtime Method Swizzling 方法转换实践小结

的Markdown内部跳转太别扭,建议看原始版本

#import 


  • Method Swizzling 的实现
    • 交换实例方法
    • 交换类方法
  • Method Swizzling 的应用
    • 打印当前显示的UIViewController
    • 打印字典中的中文
    • 打印数组中的中文 (按需使用)
    • 防止MutableArray插入nil、越界导致崩溃(慎用)
    • 防止MutableDictionary 传入nil导致崩溃(慎用)
    • 防止重复点击按钮Button、监听某些系统自带按钮的点击事件。
    • 拦截系统自带的导航栏'返回'按钮的Pop事件
    • 防止重复点击按钮Button、拦截某些系统自带按钮的点击事件


Method Swizzling 的实现

对于Runtime Swizzling,这篇文章值得一看:《Objective-C的方法替换》,俺目前只实现了对一个类的2个“方法”进行Method Swizzling,对不同的类进行Method Swizzling出现了奇怪的问题,已放弃研究。
Method Swizzling尽量少用,这是把利剑,用好了能省很多事,用不好就可能成为猪队友。


交换实例方法

/**
 【Method Swizzling-慎用】用于替换同一类的2个[实例]方法。建议放在+(void)load方法配合DispatchOnce一起使用

 @param originalSEL 被替换的SEL
 @param objectSEL   用于替换的自定义SEL
 @param objectClass 进行Method Swizzling的Class
 */
void JK_ExchangeInstanceMethod(SEL originalSEL, SEL objectSEL, Class objectClass);



void JK_ExchangeInstanceMethod(SEL originalSEL, SEL objectSEL, Class objectClass) {
    Method originalMethod = class_getInstanceMethod(objectClass, originalSEL);
    Method replaceMethod = class_getInstanceMethod(objectClass, objectSEL);
    
    // 判断是否实现方法
    if (originalMethod == NULL || replaceMethod == NULL) {
        NSLog(@"\n.\tWarning! JK_ExchangeInstanceMethod 失败!  [%@及其SuperClasses] 均未实现方法 [%@]\n.",objectClass,originalMethod == NULL ? NSStringFromSelector(originalSEL) : NSStringFromSelector(objectSEL));
        return;
    }
    
    // 将replaceMethod实现添加到objectClass中,并且将originalSEL指向新添加的replaceMethod的IMP。
    BOOL add = class_addMethod(objectClass, originalSEL, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
    
    if (add) {
        // 添加成功,再将objectSEL指向原有的originalMethod的IMP,实现交换
        // 当前类或者父类没有实现originalSEL会执行这一步
        class_replaceMethod(objectClass, objectSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        
    } else {
        // 已经实现customMethod,对systemMethod和customMethod的实现指针IMP进行交换
        method_exchangeImplementations(originalMethod, replaceMethod);
    }
}

用作测试的类继承关系如:ViewController -> BaseViewController -> UIViewController
用作测试的originalSEL:@selector(viewWillAppear:)
进行以下操作:

  1. ViewController进行ExchangeInstanceMethodobjectSEL:@selector(edf_viewWillAppear:),而且ViewController和父类BaseViewController都【没有实现viewWillAppear】,
    add = YES;

  2. UIViewController基类进行ExchangeInstanceMethodobjectSEL:@selector(jk_viewWillAppear:),
    add = NO。

  3. 在操作2的基础上,如果在任何手动创建的类.m的(+ load)方法中执行操作1【不实现viewWillAppear:】,
    add = YES,对于ViewController只会执行@selector(edf_viewWillAppear:),不会执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:)标记点1

  4. 在操作2的基础上,如果在任何手动创建的类.m除【(+ load)以外】的方法中执行操作1【不实现viewWillAppear:】,
    add = YES,对于ViewController既执行@selector(edf_viewWillAppear:)又执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:)。

  5. 在操作2的基础上,如果在任何手动创建的类.m的(+ load)方法中执行操作1,但是ViewController或者父类BaseViewController正常【实现了viewWillAppear:方法】,
    add = NO,对于ViewController既执行@selector(edf_viewWillAppear:)又执行@selector(jk_viewWillAppear:),其他控制器则会执行@selector(jk_viewWillAppear:),和操作4结果差不多。

初步得出的结论是 当前类或者父类没有实现originalSEL时(UIViewController排除在外),add = YES,并且执行class_addMethod前后的class_getInstanceMethod(objectClass, originalSEL)的指针地址不一样。
add = NO的时候,class_addMethod前后的class_getInstanceMethod(objectClass, originalSEL)的指针地址一样。

个人对Swizzling结果的理解是对2个SEL所关联的2个IMP进行了调换
交换前:调用SEL1,会执行IMP_1对应的代码

SEL1 ---> IMP_1
SEL2 ---> IMP_2

交换后:调用SEL2,会执行IMP_1对应的代码

SEL1 ---> IMP_2
SEL2 ---> IMP_1

建议进行Swizzling的objectClass是拥有originalSEL的最顶层父类,先从顶层父类开始选,比如UIViewController,而不是底层的子类,比如普通的控制器类。


交换类方法

/**
 【Method Swizzling-慎用】用于替换同一类的2个[类]方法。建议放在+(void)load方法使用
 
 @param originalSEL 被替换的SEL
 @param objectSEL   用于替换的自定义SEL
 @param objectClass 进行Method Swizzling的Class
 */
void JK_ExchangeClassMethod(SEL originalSEL, SEL objectSEL, Class objectClass);



void JK_ExchangeClassMethod(SEL originalSEL, SEL objectSEL, Class objectClass) {
    Method originalMethod = class_getClassMethod(objectClass, originalSEL);
    Method replaceMethod = class_getClassMethod(objectClass, objectSEL);
    
    
    if (originalMethod == NULL || replaceMethod == NULL) {
        NSLog(@"\n.\tWarning! JK_ExchangeClassMethod 失败!  [%@及其SuperClasses] 均未实现方法 [%@]\n.",objectClass,originalMethod == NULL ? NSStringFromSelector(originalSEL) : NSStringFromSelector(objectSEL));
        return;
    }
    
    /// 交换实例方法的写法在这失效了,所以直接进行了method_exchangeImplementations,待研究
    method_exchangeImplementations(originalMethod, replaceMethod);
}


Method Swizzling 的应用

  • 如果子类的[+ load]中调用[super load],父类的[+ load]就会被调用2次,所以加上dispatch_once以防重复Swizzling,安全性更高。
  • 有些类使用Method Swizzling是为了方便DEBUG调试,对于发布版本是多余的操作,所以加上#ifdef DEBUG进行判断。
  • 可能会存在多个UIViewCOntroller分类针对viewWillAppear进行Method Swizzling,不过不用担心,经测试转换后的ObjectSEL都会被调用,当然也要注意特殊情况:标记点1

打印当前显示的控制器ViewController

@implementation UIViewController (Swizzling)

+ (void)load {
#ifdef DEBUG
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JK_ExchangeInstanceMethod(@selector(viewWillAppear:), @selector(jk_viewWillAppear:), self);
    });
#endif
}


- (void)jk_viewWillAppear:(BOOL)animated{
    [self jk_viewWillAppear:animated];
    
    NSString * className = NSStringFromClass([self class]);
    if (![className hasPrefix:@"UI"] && ![className hasPrefix:@"_"]) {
        NSLog(@"即将显示:%@   备注:%@",self.class,self.view.accessibilityIdentifier);
    }
}

@end

UIImage优先使用无缓存加载

加载UIImage有2种方式:

  • 方式1:[UIImage imageNamed...]
  • 方式2:[UIImage imageWithContentsOfFile...]

方式1会不断增加缓存,直到APP进程被杀才会释放,适用于频繁使用的图片,存放在Assets.xcassets中图片必须[UIImage imageNamed...]
方式2不会被缓存,Image对象释放即可释放内存,适用于不怎么用的图片,而且存在于[NSBundle mainBundle]中,不能存放在Assets.xcassets中。

@implementation UIImage (Swizzling)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JK_ExchangeClassMethod(@selector(imageNamed:), @selector(jk_imageNamed:), self);
    });
}


/**
 优先无缓存加载图片,imageWithContentsOfFile
 */
+ (UIImage *)jk_imageNamed:(NSString *)name{
    UIImage * image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:[name hasSuffix:@"jpg"] ? nil : @"png"]];
    if (!image) {
        image = [self jk_imageNamed:name];
    }
    
    if (image == nil) {
        NSLog(@"\nWarning! 图片加载失败!  imageName:%@",name);
    }
    return image;
}

@end


打印字典NSDictionary中的中文

打印NSDictionary中的中文,针对网络请求到的数据

@implementation NSDictionary (Swizzling)

+ (void)load {
#ifdef DEBUG
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            JK_ExchangeInstanceMethod(@selector(descriptionWithLocale:), @selector(jk_descriptionWithLocale:), self);
        });
#endif
}


- (NSString *)jk_descriptionWithLocale:(id)locale {
    if (self == nil || self.allKeys.count == 0) {
        return [self jk_descriptionWithLocale:locale];
    } else {
        @try {
            NSError * error = nil;
            NSData * data = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&error];
            if (error) {
                return [self jk_descriptionWithLocale:locale];
            } else {
                return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            }
        } @catch (NSException *exception) {
            return [self jk_descriptionWithLocale:locale];
        }
    }
}

@end

打印数组NSArray中的中文(按需使用)

打印NSArray中的中文,针对网络请求到的数据,一般Json都会用字典包裹,所以可以不用或者注释下面的代码,毕竟数组一般都是存Model,Json转换不了,加了@try之后发生异常时会被断点捕获。(正常情况下转换NSJSONSerialization不支持的类型,会直接Crash

@implementation NSArray (Swizzling)

+ (void)load {
#ifdef DEBUG
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            JK_ExchangeInstanceMethod(@selector(descriptionWithLocale:), @selector(jk_descriptionWithLocale:), self);
        });
#endif
}


- (NSString *)jk_descriptionWithLocale:(id)locale {
    if (self == nil || self.count == 0) {
        return [self jk_descriptionWithLocale:locale];
    } else {
        @try {
            NSError * error = nil;
            NSData * data = [NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:&error];
            if (error) {
                return [self jk_descriptionWithLocale:locale];
            } else {
                return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            }
        } @catch (NSException *exception) {
            return [self jk_descriptionWithLocale:locale];
        }
    }
}

@end

防止NSMutableArray 插入nil、数组越界导致崩溃

慎用可能会变相的造成数据异常

@implementation NSArray (SafeSwizzling)
static const char * kArrayClass = "__NSArrayI";

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JK_ExchangeInstanceMethod(@selector(objectAtIndex:), @selector(jk_objectAtIndexI:), objc_getClass(kArrayClass));
    });
}

- (id)jk_objectAtIndexI:(NSUInteger)index {
    if (index < self.count) {
        return [self jk_objectAtIndexI:index];
    } else {
        NSLog(@"数组查询越界,return 。 --[NSArray objectAtIndex:]-- index=%zd   array.count=%zd",index,self.count);
        return [NSNull null];
    }
}
@end


@implementation NSMutableArray (SafeSwizzling)
static const char * kMutArrayClass = "__NSArrayM";

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JK_ExchangeInstanceMethod(@selector(insertObject:atIndex:), @selector(jk_insertObject:atIndex:), objc_getClass(kMutArrayClass));
        JK_ExchangeInstanceMethod(@selector(objectAtIndex:), @selector(jk_objectAtIndexM:), objc_getClass(kMutArrayClass));
    });
}

- (void)jk_insertObject:(id)anObject atIndex:(NSUInteger)index {
    if (index > self.count) {
        NSLog(@"数组插值越界 --[NSMutableArray insertObject: atIndex:]-- object=%@   index=%zd      array.count=%zd",anObject,index,self.count);
    } else if (anObject != nil) {
        [self jk_insertObject:anObject atIndex:index];
    } else {
        NSLog(@"传入空值Nil --[NSMutableArray insertObject: atIndex:]-- object=%@   index=%zd",anObject,index);
    }
}

- (id)jk_objectAtIndexM:(NSUInteger)index {
    if (index < self.count) {
        return [self jk_objectAtIndexM:index];
    } else {
        NSLog(@"数组查询越界,return 。 --[NSMutableArray objectAtIndex:]-- index=%zd   array.count=%zd",index,self.count);
        return [NSNull null];
    }
}

@end

防止MutableDictionary 传入nil导致崩溃

慎用可能会变相的造成数据异常

@implementation NSMutableDictionary (SafeSwizzling)
static const char * kMutDictClass = "__NSDictionaryM";
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JK_ExchangeInstanceMethod(@selector(setObject:forKey:), @selector(jk_setObject:forKey:), objc_getClass(kMutDictClass));
    });
}
- (void)jk_setObject:(id)anObject forKey:(id)aKey {
    if (anObject && aKey) {
        [self jk_setObject:anObject forKey:aKey];
    } else {
        NSLog(@"传入空值Nil --[NSMutableDictionary setObject: forKey:]-- object=%@   key=%@",anObject,aKey);
    }
}
@end

拦截系统自带的导航栏'返回'按钮的Pop事件

参考UIViewController-BackButtonHandler

分类.h

/**
 需要拦截导航栏上系统自带的‘返回’按钮事件,就实现此协议方法
 */
@protocol JKViewControllerPopActionHandler 

@optional
- (BOOL)jk_navigationControllerShouldPopOnBackButton;

@end


/**
 所有控制器都遵守JKViewControllerPopActionHandler协议
 */
@interface UIViewController (PopActionHandler)
@end



/**
 用Runtime Method Swizzling拦截@selector(navigationBar:shouldPopItem:)
 */
@interface UINavigationController (PopActionHandler)
@end

分类.m

@implementation UIViewController (PopActionHandler)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JK_ExchangeInstanceMethod(@selector(viewDidAppear:), @selector(jk_viewDidAppear:), self);
        JK_ExchangeInstanceMethod(@selector(viewDidDisappear:), @selector(jk_viewDidDisappear:), self);
    });
}


- (void)jk_viewDidAppear:(BOOL)animated {
    [self jk_viewDidAppear:animated];
    if ([self respondsToSelector:@selector(jk_navigationControllerShouldPopOnBackButton)]) {
        /// 拦截Pop事件就关闭侧滑返回
        self.navigationController.interactivePopGestureRecognizer.enabled = NO;
    }
}

- (void)jk_viewDidDisappear:(BOOL)animated {
    [self jk_viewDidDisappear:animated];
    self.navigationController.interactivePopGestureRecognizer.enabled = YES;
}
@end



@implementation UINavigationController (PopActionHandler)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        JK_ExchangeInstanceMethod(@selector(navigationBar:shouldPopItem:), @selector(jk_navigationBar:shouldPopItem:), self);
    });
}



- (BOOL)jk_navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
    if([self.viewControllers count] < [navigationBar.items count]) {
        return YES;
    }
    UIViewController* topVC = [self topViewController];
    BOOL enablePop = YES;
    if ([topVC respondsToSelector:@selector(jk_navigationControllerShouldPopOnBackButton)]) {
        enablePop = [topVC jk_navigationControllerShouldPopOnBackButton];
    }
    
    if (enablePop == YES) {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self popViewControllerAnimated:YES];
        });
    }
    return NO;
}


@end

防止重复点击按钮Button、拦截某些系统自带按钮的点击事件

代码就不贴了,搜一下有一堆,随便推荐一篇文章iOS 解决button重复点击问题。
我的建议是有需要才做处理重复点击,创建的Button默认不处理重复点击,文章中是拦截处理了所有UIControl的点击事件,会对某些系统按钮也会拦截点,所以要对某些类做特殊处理,下面是我碰到过的。

// 拍照的控制器
if ([target isKindOfClass:NSClassFromString(@"PLImagePickerCameraView")]
        // 网页视频播放器
        || [target isKindOfClass:NSClassFromString(@"AVFullScreenPlaybackControlsViewController")]
        // iOS10 拍照
        || [target isKindOfClass:NSClassFromString(@"CAMViewfinderViewController")]
        || [target isKindOfClass:[UIBarButtonItem class]]) {
        // 系统拍照按钮/视频播放器按钮单独处理,其他需要快速点击的设置quickTapEnable = YES
        [self jk_sendAction:action to:target forEvent:event];
        return;
    }

参考文献

  • UIViewController-BackButtonHandler
  • iOS 解决button重复点击问题。

你可能感兴趣的:(【瞎搞iOS开发07】Runtime Method Swizzling 方法转换实践小结)