心辰·Dev

WKWebView 兼容 iOS7 实践

iOS8 新特性 WKWebView

WKWebView 相对于 UIWebView 有以下几点显著的提升:

  1. 使用 Nitro JavaScript 引擎,加载处理 JS 代码的速度明显提升四倍左右。
  2. 对 Web APP 加载的速度提升百分之20左右。
  3. 优化了 WebView 的内存使用。
  4. 开放了更多的 API.

这已经足以让我们考虑在支持 iOS8 设备的应用中使用 WKWebView.

和 UIWebView 共存

现阶段完全不兼容 iOS7 还为时过早,但是又想在项目中使用功能更多、效率更高的 WKWebView, 那么必须有一套方案来解决 WKWebView 和 UIWebView 的共存问题。也就是说,在 iOS7 及以下版本仍使用 UIWebView, 在 iOS8 以上使用 WKWebView. 上文提到两种 WebView 的 API 并不完全相同,在使用相同功能的接口时,要判断当前 WebView 是哪种实例才能分别调用各自的接口。这样代码中会有多处相似的逻辑判断。
针对这个问题,下文提出一种方案,封装一个统一的 WebView 来解决不同 iOS 版本的兼容问题。

第一步:定义抽象 Protocol

首先总结出经常要使用的一组属性和接口,定义一个抽象 protocol, 在 protocol 中统一定义这些属性或者接口,遵从该 protocol 的不同类再去具体实现这些接口,即可完成 API 的统一。对比 UIWebView 的属性,WKWebView 中没有 request 属性,而多出 URL 、 title 等属性,这些属性都可以统一定义到 protocol 中。对 API 的统一也是使用相同的思路。

第二步:使用 Category

用 Category 扩展两个 WebView, Category 遵从第一步定义的 protocol 并分别实现 protocol 中的方法。为了在 Category 中为类动态添加属性,要使用 Runtime 的 objc_getAssociatedObjectobjc_setAssociatedObject方法。例如为 WKWebView 添加 request 属性,要用到如下代码:

1
2
3
4
5
6
7
- (NSURLRequest *)request {
return objc_getAssociatedObject(self, @selector(request));
}

- (void)setRequest:(NSURLRequest *)request {
objc_setAssociatedObject(self, @selector(request), request, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

虽然添加了 request 属性,但它默认没有赋值,如果想在调用loadRequest时自动将 request 赋对应的值,那么还要使用 Method Swizzling 的方法换掉原生的loadRequest的实现,在loadRequest之前先进行赋值:

1
2
3
4
- (void)altLoadRequest:(NSURLRequest *)request {
[self setRequest: request];
[self altLoadRequest: request];
}

第三步:封装统一的 WebView

创建一个继承自 UIView 的 WebView, 取名为 BXWebView, BXWebView 中包含一个真正的 WebView 实例 contentView,在初始化首次创建 contentView 时,iOS8 以上实例化为 WKWebView, 否则就实例化为 UIWebView. 该实例还要遵从上文定义的 protocol 以达到统一 API 的目的。封装好的 BXWebView 类的实例应该是它其中 contenView (即 UIWebView 或 WKWebView 实例) 的代理。具体的类声明如下:

1
2
3
4
5
6
7
8
9
10
11
@interface BXWebView : UIView <UIWebViewDelegate, WKNavigationDelegate, WKUIDelegate>

// 真正的 WebView
@property (nonatomic) UIView <BXWebViewProtocol> *contentView;

// UIWebView 的代理
@property (nonatomic, assign) id<UIWebViewDelegate> oDelegate;
// WKWebView 的代理
@property (nonatomic, assign) id<WKNavigationDelegate, WKUIDelegate> nDelegate;

@end

在 BXWebView 类的实现中,实例化 contenView 时需要判断 iOS SDK 中是否有 WKWebView 这个类来分别实例化对应的 WebView. 如下代码所示:

1
2
3
4
5
if (NSClassFromString(@"WKWebView")) {
_contentView = [[WKWebView alloc] initWithFrame: self.bounds];
} else {
_contentView = [[UIWebView alloc] initWithFrame: self.bounds];
}

把 contenView 的代理设置为 self (即封装的 WebView )后,构成了 ViewController ->封装好的WebView -> UIWebView/WKWebView 两级代理的层次,触发代理方法的消息分级传递下去,处理方式如下:

1
2
3
4
5
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if ([self.oDelegate respondsToSelector:@selector(webViewDidFinishLoad:)]) {
[self.oDelegate webViewDidFinishLoad:webView];
}
}

此外还需要根据特定业务来封装一个 WebViewController 来处理业务逻辑。至此一套 WebView 的兼容框架就成型了。