ios_UI对移动开发有帮助程序员

升级到 WKWebView 以及遇到的坑

2017-05-03  本文已影响2152人  郑嘉成_

JCWebView

最近公司项目搞优化,打算把客户端内的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 端调用
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);
 }
[webView evaluateJavaScript:@"say()" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
    NSLog(@"%@",result);
}];

2.WebViewJavascriptBridge

GitHub

我们客户端使用的是WebViewJavascriptBridge ,因为某些历史原因,使用方法和最新文档有出入,所以经过一番尝试,还是兼容了老的协议使用方法。H5页面开发只需要根据UserAgent 上特殊的标识符来判断客户端使用的是哪个WebView,来修改WebViewJavascriptBridge 创建方法,达到兼容目的

1.请求Cookie

Cookie 是WKWebView 的一大短板

不需要做额外的操作,WebView 内部发起的请求都会自动携带上NSHTTPCookieStorage 里所有的Cookie

需要手动来添加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.禁止了一些跳转

需要我们自己做处理

-(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 的!

JCWebView

上一篇下一篇

猜你喜欢

热点阅读