iOS Method Swizzling 的一个实际应用

2017-05-23  本文已影响0人  舌尖上的大胖

本文 Method Swizzling 部分参考了 《iOS黑魔法-Method Swizzling》

一、问题

最近在维护公司一个久远的项目时,发现当时使用了 UIWebView 展示 HTML 页面,为了解决 JavaScript 中 alert 和 confirm 样式不能自定义的问题,所以通过实现以下方法,

@interface UIWebView (JSConfirmAlert)

- (void)webView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(id)frame;
- (BOOL)webView:(UIWebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(id)frame;

@end

来拦截 alert 和 confirm ,并通过 UIAlertView 重新实现。

但是原来的代码在实现上有些问题,这部分功能又是做成了 framework 集成到工程里面的,而且由于项目几经易手,framework 部分的源码已经没有了。由于原来是通过给 UIWebView 写 Category 来实现的功能,所以即便不引用 .h 文件,只要集成了 framework,这部分代码就会生效。(这一点是刚发现的,原来一直以为要引用了 .h 才会生效。)

二、解决方案

无奈之下,就考虑用 Method Swizzling 来改写之前的方法了。

// UIWebView+SwizzlingAlertAndConfirm.h

#import <Foundation/Foundation.h>

@interface UIWebView (SwizzlingAlertAndConfirm)

@end

// UIWebView+SwizzlingAlertAndConfirm.m

#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#import "UIWebView+SwizzlingAlertAndConfirm.h"

@implementation UIWebView (SwizzlingAlertAndConfirm)

+ (void)exchangeMethod:(SEL)fromSelector toMethod:(SEL)toSelector {

    // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。
    Method fromMethod = class_getInstanceMethod([self class], fromSelector);
    Method toMethod = class_getInstanceMethod([self class], toSelector);

    /**
     *  我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
     *  而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
     *  所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
     */
    if (!class_addMethod([self class], fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
        method_exchangeImplementations(fromMethod, toMethod);
    }

}


+ (void)load {
    [super load];

    [self exchangeMethod:@selector(webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:)
                toMethod:@selector(swizzlingWebView:runJavaScriptAlertPanelWithMessage:initiatedByFrame:)];

    [self exchangeMethod:@selector(webView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:)
                toMethod:@selector(swizzlingWebView:runJavaScriptConfirmPanelWithMessage:initiatedByFrame:)];

    [self exchangeMethod:@selector(alertView:clickedButtonAtIndex:)
                toMethod:@selector(swizzlingAlertView:clickedButtonAtIndex:)];

}



- (void)swizzlingWebView:(UIWebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(id)frame {
    UIAlertView* customAlert = [[UIAlertView alloc] initWithTitle:@"助手提示" message:message delegate:nil cancelButtonTitle:@"确定bbb" otherButtonTitles:nil];
    [customAlert show];
}

static BOOL diagStat = NO;
static NSInteger bIdx = -1;

- (BOOL)swizzlingWebView:(UIWebView *)sender runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(id)frame {
    UIAlertView *confirmDiag = [[UIAlertView alloc] initWithTitle:@"助手提示"
                                                          message:message
                                                         delegate:self
                                                cancelButtonTitle:@"取消13"
                                                otherButtonTitles:@"确定13", nil];

    [confirmDiag show];
    bIdx = -1;

    while (bIdx==-1) {
        //[NSThread sleepForTimeInterval:0.2];
        [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1f]];
    }
    if (bIdx == 0){//取消;
        diagStat = NO;
    }
    else if (bIdx == 1) {//确定;
        diagStat = YES;
    }
    return diagStat;
}

- (void)swizzlingAlertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    bIdx = buttonIndex;
}


@end

工程引用这两个文件后,问题解决。

三、讨论
1、Method Swizzling 的封装

这里对 Method Swizzling 做了个简单的封装,不过只是为了写着方便随便整了下。真正在项目中我们肯定会在很多地方用到 Method Swizzling,而且在使用这个特性时有很多需要注意的地方。我们可以将 Method Swizzling 封装起来,也可以使用一些比较成熟的第三方。在这里我推荐Github上星最多的一个第三方——JRSwizzle

2、改进

都折腾完之后才发现,就这个项目本身的问题而言,其实用不到 Method Swizzling,只要写个 UIWebView 的子类,在子类中重新实现这几个方法,然后直接使用子类就好了。当时没想起来。不过使用子类化的方法,还是需要修改原来代码中对 UIWebView 引用的那部分代码。而使用 Method Swizzling 还是更酷一些,原来的代码不用做任何修改,只要在工程中引入这两个文件就可以了。

上一篇下一篇

猜你喜欢

热点阅读