本文 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
还是更酷一些,原来的代码不用做任何修改,只要在工程中引入这两个文件就可以了。