升级到 WKWebView 以及遇到的坑

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 调用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

GitHub

我们客户端使用的是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 的!

JCWebView

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容