最近公司项目搞优化,打算把客户端内的UIWebView 都替换成WKWebView。WKWebView 的优点多多,这里就不再赘述。因为还要兼容iOS7,所以这里主要说一下替换的过程以及踩过的坑。
兼容UIWebView
将客户端里所有的UIWebView 都替换成JCWebView,其内部自动判断使用哪个WebView。
@protocol JCWebViewDelegate <NSObject>
@optional
- (BOOL)webView:(JCWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(JCWebView *)webView;
- (void)webViewDidFinishLoad:(JCWebView *)webView;
- (void)webView:(JCWebView *)webView didFailLoadWithError:(NSError *)error;
///加载进度,用于进度条
- (void)webView:(JCWebView *)webView requestLoadEstimatedProgress:(double)estimatedProgress;
@end
@interface JCWebView : UIView
- (instancetype)initWithFrame:(CGRect)frame forceUseUIWebView:(BOOL)forceUseUIWebView;
@property (nonatomic, weak) id<JCWebViewDelegate> delegate;
/// 是否使用 UIWebView 默认是NO
@property (nonatomic, readonly) BOOL isUsedUIWebView;
/// 当前内部使用的webView
@property (nonatomic, readonly) id realWebView;
@property (nonatomic, readonly) double estimatedProgress;
@property (nonatomic, readonly, copy) NSString *title;
@property (nonatomic, readonly, weak) UIScrollView *scrollView;
@property (nonatomic, readonly, copy) NSURL *URL;
@property (nonatomic, readonly) NSURLRequest *request;
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
@property (nonatomic, readonly) BOOL canGoBack;
@property (nonatomic, readonly) BOOL canGoForward;
@property (nonatomic) BOOL scalesPageToFit;
- (id)loadRequest:(NSURLRequest *)request;
- (id)loadHTMLString:(NSString *)string baseURL:(NSURL *)baseURL;
- (id)goBack;
- (id)goForward;
- (id)reload;
- (id)reloadFromOrigin;
- (void)stopLoading;
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^)(id, NSError *))completionHandler;
- (id)stringByEvaluatingJavaScriptFromString:(NSString *)javaScriptString;
- (void)evaluateJavaScriptToAddCookie:(void(^)())completion;
@end
内部实现
-(void)initRealWebView{
Class wkWebView = NSClassFromString(@"WKWebView");
if(wkWebView && !self.isUsedUIWebView){
[self initWKWebView];
_isUsedUIWebView = NO;
}else{
[self initUIWebView];
_isUsedUIWebView = YES;
}
[self addSubview:self.realWebView];
}
-(void)initWKWebView{
WKWebViewConfiguration* configuration = [[NSClassFromString(@"WKWebViewConfiguration") alloc] init];
WKPreferences *preferences = [NSClassFromString(@"WKPreferences") new];
configuration.preferences = preferences;
configuration.allowsInlineMediaPlayback = YES;
//共享一个pool 用以cookies共享
configuration.processPool = [[WKWebViewPoolHandler sharedInstance] defaultPool];
WKUserContentController *userContentController = [NSClassFromString(@"WKUserContentController") new];
configuration.userContentController = userContentController;z
WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.bounds configuration:configuration];
webView.UIDelegate = self;
webView.navigationDelegate = self;
webView.backgroundColor = [UIColor whiteColor];
[webView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
[webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
_realWebView = webView;
}
- (id)loadRequest:(NSURLRequest *)request{
NSMutableURLRequest *newRequest = [request mutableCopy];
if(_isUsedUIWebView){
self.request = newRequest;
[(UIWebView*)self.realWebView loadRequest:newRequest];
return nil;
}else{
//重新添加Cookie WKWebView 不会带上cookie 需要同时在request上添加以及使用脚本添加
NSString *userAgent =[[NSUserDefaults standardUserDefaults] valueForKey:@"UserAgent"];
double systemVersion = [[[UIDevice currentDevice] systemVersion] doubleValue];
if (userAgent && userAgent.length > 0 && systemVersion >= 9) {
WKWebView *webView = (WKWebView*)self.realWebView;
webView.customUserAgent = userAgent;
}
[self injectCookies:newRequest];
self.request = newRequest;
return [(WKWebView*)self.realWebView loadRequest:newRequest];
}
}
JS 交互
1.WKUserContentController
通过WKUserContentController 来实现。先注册约定好的方法,然后再调用。
//注册方法名
[wkWebView.configuration.userContentController addScriptMessageHandler:handler name:@"sayHello"];
//dealloc 要移除
- (void)dealloc{
[userContentController removeScriptMessageHandlerForName:@"sayHello"];
}
- JS 调用Navtive
//js 端调用
window.webkit.messageHandlers.sayHello.postMessage("hi")
Native 端接收
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
NSLog(@"name:%@\n body:%@\n frameInfo:%@\n",message.name,message.body,message.frameInfo);
}
- Navtive 调用JS
[webView evaluateJavaScript:@"say()" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@",result);
}];
2.WebViewJavascriptBridge
我们客户端使用的是WebViewJavascriptBridge ,因为某些历史原因,使用方法和最新文档有出入,所以经过一番尝试,还是兼容了老的协议使用方法。H5页面开发只需要根据UserAgent 上特殊的标识符来判断客户端使用的是哪个WebView,来修改WebViewJavascriptBridge 创建方法,达到兼容目的
坑
1.请求Cookie
Cookie 是WKWebView 的一大短板
- UIWebView
不需要做额外的操作,WebView 内部发起的请求都会自动携带上NSHTTPCookieStorage 里所有的Cookie
- WKWebView
需要手动来添加Cookie,loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题
- (void)injectCookies:(NSMutableURLRequest *)request{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSString *validDomain = request.URL.host;
if (!cookies || cookies.count < 1) {
return;
}
NSMutableString *cookieString = [NSMutableString stringWithString:@""];
for (NSHTTPCookie *cookie in cookies) {
if (![validDomain hasSuffix:cookie.domain]) {
continue;
}
[cookieString appendString:[NSString stringWithFormat:@"%@=%@;", cookie.name, cookie.value]];
}
//删除最后一个“;”
if (cookieString.length > 0) {
[cookieString deleteCharactersInRange:NSMakeRange(cookieString.length - 1, 1)];
}
[request setValue:cookieString forHTTPHeaderField:@"Cookie"];
}
通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题
注意:document.cookie()无法跨域设置 cookie
- (void)addUserCookieScript:(NSURLRequest *)request{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
if (!cookies || cookies.count < 1) {
return;
}
NSMutableString *cookieScript = [NSMutableString stringWithString:@""];
for (NSHTTPCookie *cookie in cookies) {
[cookieScript appendString:[NSString stringWithFormat:@"document.cookie='%@';", [self javascriptStringWithCookie:cookie]]];
}
WKUserScript *script = [[WKUserScript alloc]initWithSource:cookieScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
WKWebView *wkWebView = (WKWebView*)self.realWebView;
[wkWebView.configuration.userContentController addUserScript:script];
}
- (NSString *)javascriptStringWithCookie:(NSHTTPCookie*)cookie {
NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@;",
cookie.name,
cookie.value,
cookie.domain,
cookie.path ?: @"/"];
if (cookie.secure) {
string = [string stringByAppendingString:@"secure=true"];
}
return string;
}
将Response 里HeaderFields 的Cookie 保存到本地,但是暂时还遇到过这种情况
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
if (cookies.count>0) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:response.URL mainDocumentURL:nil];
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
还有一种情况需要注意,就是如果是302 跳转,如果Response 里有Set-Cookie,下个页面请求头上将不会带上这个Cookie,这个暂时还没有找到解决办法。
2.多个WKWebView之间共享Cookie
WKProcessPool 这个类用来配置进程池,与网页视图的资源共享有关。WKProcessPool 类中没有暴露任何属性和方法,所以拿不到任何数据。 为多个WKWebView 配置为同一个WKProcessPool,会让多个WKWebView 之间共享数据,例如Cookie、用户凭证等。
WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc]init];
configuration.processPool = [[WKWebViewPoolHandler sharedInstance] defaultPool];
3.UIWebView 与WKWebView 之间共享Cookie
因为客户端使用的H5页面来登录,登录信息保存在Cookie里,所以需要把这部分的Cookie 保存到本地NSHTTPCookieStorage 里。最初尝试了各种办法,想通过document.cookie 来取出页面上的Cookie,但是拿不到Cookie的失效期,域名等,还是放弃了这种做法。最终还是选择了只有登录页面还是使用UIWebView,其他页面使用WKWebView,这样登录Cookie 能确保存到了本地。
4.清除缓存
WebKit框架采用其本身的缓存框架,iOS 9 之后可以用WKWebsiteDataStore 类来清除缓存。
NSSet *websiteDataTypes = [NSSet setWithArray:@[
//WKWebsiteDataTypeDiskCache,
//WKWebsiteDataTypeOfflineWebApplicationCache,
//WKWebsiteDataTypeMemoryCache,
//WKWebsiteDataTypeLocalStorage,
WKWebsiteDataTypeCookies,
WKWebsiteDataTypeSessionStorage,
//WKWebsiteDataTypeIndexedDBDatabases,
//WKWebsiteDataTypeWebSQLDatabases
]];
NSDate *dateFrom = [NSDate dateWithTimeIntervalSince1970:0];
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes modifiedSince:dateFrom completionHandler:^{}];
5.不响应JS 的alert()
需要实现runJavaScriptAlertPanelWithMessage 这个代理
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示"
message:message
preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"确定"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
completionHandler();
}]];
UIViewController *tpVCL = [self topViewController];
[tpVCL presentViewController:alertController animated:YES completion:^{}];
}
6.禁止了一些跳转
UIWebView
打开ituns.apple.com、跳转到appStore,、拨打电话,、唤起邮箱等一系列操作UIWebView 自己处理不了会自动交给UIApplication 来处理。WKWebView
上述事件WKWebView 不会自动交给UIApplication 来处理,除此之外,js端通过window.open() 打开新的网页的动作也被禁掉了
需要我们自己做处理
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
UIApplication *app = [UIApplication sharedApplication];
if ([url.scheme isEqualToString:@"tel"]){
if ([app canOpenURL:url]){
[app openURL:url];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
if ([url.absoluteString containsString:@"ituns.apple.com"]{
if ([app canOpenURL:url]){
[app openURL:url];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
7.NSURLProtocol
WKWebView 在独立于 App 进程之外的进程中执行网络请求,请求数据不经过主进程,因此,在 WKWebView 上直接使用 NSURLProtocol 无法拦截请求。网上也有让WKWebView 支持NSURLProtocol 的方法,但还没有研究过。
8.页面滚动速率
WKWebView 需要通过 scrollView delegate 调整滚动速率:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
scrollView.decelerationRate = UIScrollViewDecelerationRateNormal;
}
总结
暂时只遇到了这些坑,优化的时间还不是很长,其他问题还需要进一步测试来发现。总体来看WKWebView 相比于UIWebView 对性能的提升还是很明显的,但是缺点也很多。 希望苹果能进一步优化下WKWebView 的使用,WKWebView 应该迟早要替换掉UIWebView 的!