WKWebView笔记

作者: BoomLee | 来源:发表于2017-08-21 14:48 被阅读1640次

    前言

    iOS8开始,苹果引入了新的web控件WKWebView替代UIWebViewWKWebView属于WebKit框架,WebKit框架的API极为丰富,可以从WKWebView入手逐个了解。WebKit框架也在持续更新中,iOS9,iOS 10都引入了新的API,趋势就是赶紧废弃UIWebView使用WKWebView吧。本文是升级项目中的UIWebView的一些经验和遇到的坑,希望可以帮助到大家。

    1. WKWebView简介

    一个WKWebView用来展示可交互的网页内容,就像一个APP内的浏览器。你可以使用WKWebView在你的APP中嵌入网页内容。

    1.1.WKUserContentController

    这个属性非常重要,js->oc的交互全靠它。

    • 1.动态注入js,注入的既可以是js代码,也可以是一个js文件。
    WKUserScript *script = [[WKUserScript alloc] initWithSource:@"alert('哈哈');" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
    [controller addUserScript:script];
    
    • 2.JavaScript向WKWebView发送消息,通过识别不同的消息和消息的内容,可以执行不同的native操作。
    - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
    

    遵循WKScriptMessageHandler协议的对象可以在以下代理方法接收JavaScript发送的消息。

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
    

    1.2.customUserAgent

    @property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));
    

    用来自定义浏览器UserAgent,可惜的是9.0之后才可以使用,所以还是与UIWebView一样通过NSUserDefaults来设置:

    [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": newUserAgent}];
    

    1.3.属性支持kvo

    WKWebView的大部分属性是支持kvo的,但是并没有提供代理方法,需要自己添加监听。例如监听titleestimatedProgress

    [self.wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
    [self.wkWebView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
    

    这样就可以展示进度条,无需等待web完全加载完毕才显示标题

    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        if ([keyPath isEqualToString:@"estimatedProgress"]) {
            CGFloat progress = [[change valueForKey:NSKeyValueChangeNewKey] floatValue];
            if (progress >= 1) {
                [self.progressView setProgress:progress animated:NO];
                self.progressView.hidden = YES;
                [self.progressView setProgress:0 animated:NO];
            } else {
                self.progressView.hidden = NO;
                [self.progressView setProgress:progress animated:YES];
            }
        }
        if ([keyPath isEqualToString:@"title"] && !self.defaultTitle) {
            NSString *title = [change valueForKey:NSKeyValueChangeNewKey];
            if (title) {
                self.mTitleLabel.text = title;
            }
        }
    }
    

    记得删除监听

    - (void)dealloc{
        NSLog(@"%@",NSStringFromSelector(_cmd));
        [self.wkWebView removeObserver:self forKeyPath:@"estimatedProgress" context:nil];
        [self.wkWebView removeObserver:self forKeyPath:@"title" context:nil];
    }
    

    1.4.识别网页内容

    长按网页,会弹出一个UIActionSheet。例如长按一张图片会提示你保存图片。如果想在UIWebView实现这个功能只能自定义一个手势然后通过js获取网页内容再弹出UIActionSheet

    识别网页内容

    1.5.JS

    UIWebView调用jsstringByEvaluatingJavaScriptFromString是同步返回的,并没有提供状态信息。WebKit是异步block回调的,并带有状态信息。使用时要注意在页面销毁时恰好进行了回调在iOS8上会crash。

    - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
    

    2.一些坑

    2.1.cookie问题

    这应该是WebKit最大的坑,网上有好多文章介绍了原因和解决方法,我就不画蛇添足了,贴上个链接:cookie问题,我这里记录下解决方法。
    问题所在:WKWebView加载网页得到的Cookie会同步到NSHTTPCookieStorage中,但是WKWebView加载请求时,不会同步NSHTTPCookieStorage中已有的Cookie,所以导致Cookie丢失,web无法识别客户端身份。
    解决方法:将NSHTTPCookieStorage存储的Cookie设置为请求的allHTTPHeaderFields,通过注入js的方式将Cookie写入web中。

    2.2

    [5504:1981977] webViewWebContentProcessDidTerminate
    [5504:1982134] #WK: Unable to acquire assertion for process 0
    [5504:1981977] Could not signal service com.apple.WebKit.WebContent: 113: Could not find specified service
    

    模拟慢速网络时经常出现这种错误,进度加载一部分后退回到0。调试时发现是在创建NSMutableURLRequest是设置的超时时间过短导致的。解决方法是加大超时时间或者干脆不设置超时时间。

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.url] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.url]];
    

    2.3

    在iOS9 iPod上进行测试时,点进一个web页面没问题,但是返回的时候crash。错误信息如下

    2017-08-18 19:29:52.734 BluedInternational[11600:1646954] dealloc
    objc[11600]: Cannot form weak reference to instance (0x5225200) of class GJWebViewController. It is possible that this object was over-released, or is in the process of deallocation.
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BREAKPOINT (code=EXC_ARM_BREAKPOINT, subcode=0xdefe)
      * frame #0: 0x20bf2a44 libobjc.A.dylib`_objc_trap()
        frame #1: 0x20bf2aa8 libobjc.A.dylib`_objc_fatal(char const*, ...) + 72
        frame #2: 0x20c0c412 libobjc.A.dylib`weak_register_no_lock + 210
        frame #3: 0x20c0c7b8 libobjc.A.dylib`objc_storeWeak + 208
        frame #4: 0x25a8489a UIKit`-[UIScrollView setDelegate:] + 306
        frame #5: 0x283c6f30 WebKit`-[WKScrollView _updateDelegate] + 228
        frame #6: 0x283d09fe WebKit`-[WKWebView dealloc] + 266
        frame #7: 0x20c0d3a8 libobjc.A.dylib`(anonymous namespace)::AutoreleasePoolPage::pop(void*) + 388
        frame #8: 0x21366f88 CoreFoundation`_CFAutoreleasePoolPop + 16
        frame #9: 0x2141806e CoreFoundation`__CFRunLoopRun + 1582
        frame #10: 0x21367228 CoreFoundation`CFRunLoopRunSpecific + 520
        frame #11: 0x21367014 CoreFoundation`CFRunLoopRunInMode + 108
        frame #12: 0x22957ac8 GraphicsServices`GSEventRunModal + 160
        frame #13: 0x25a3b188 UIKit`UIApplicationMain + 144
        frame #14: 0x0007cf62 BluedInternational`main(argc=1, argv=0x0361bab8) at main.m:13
        frame #15: 0x2100f872 libdyld.dylib`start + 2
    (lldb) 
    

    分析发现,在WKWebView释放之后竟然还进行了scrollView代理的设置,而这个时候的self,也就是当前的控制器处于销毁当中,也就解释了上面log提到的or is in the process of deallocation.。所以加入你的WKWebView是懒加载的,不要在懒加载中设置代理,其次在dealloc中将代理置为nil。

    - (void)dealloc{
        NSLog(@"%@",NSStringFromSelector(_cmd));
        [self.wkWebView removeObserver:self forKeyPath:@"estimatedProgress" context:nil];
        [self.wkWebView removeObserver:self forKeyPath:@"title" context:nil];
        self.wkWebView.navigationDelegate = nil;
        self.wkWebView.UIDelegate = nil;
        self.wkWebView.scrollView.delegate = nil;
    }
    

    2.4

    在iOS10调用js时crash,以下是错误信息。这个并没有发现错在哪里,APP删除重新安装后就没复现过。如果有遇到同样问题的,请不吝赐教。

    //2017-08-18 11:17:00.576568 [8132:2457446] Could not signal service com.apple.WebKit.Networking: 113: Could not find specified service
    //(8132,0x1a8799c40) malloc: *** error for object 0x1700bf260: pointer being freed was not allocated
    //*** set a breakpoint in malloc_error_break to debug
    

    2.5

    上线一段时间后用户反馈有个URL加载失败:https://changba.com/s/9nAISuzODZd125S0d2HhOQ。错误信息如下:

    webView:didFailNavigation:withError:
    <WKNavigation: 0x10a4c1e80>,error:Error Domain=NSURLErrorDomain Code=-999 "(null)" UserInfo={NSErrorFailingURLStringKey=http://changba.com/s/9nAISuzODZd125S0d2HhOQ, NSErrorFailingURLKey=http://changba.com/s/9nAISuzODZd125S0d2HhOQ, _WKRecoveryAttempterErrorKey=<WKReloadFrameErrorRecoveryAttempter: 0x1c4a34900>}
    
    webView:didFailProvisionalNavigation:withError:
    <WKNavigation: 0x109e5f880>,error:Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={_WKRecoveryAttempterErrorKey=<WKReloadFrameErrorRecoveryAttempter: 0x1c463a4e0>, NSErrorFailingURLStringKey=changba://?ac=playuserwork&workid=976250206, NSErrorFailingURLKey=changba://?ac=playuserwork&workid=976250206, NSLocalizedDescription=unsupported URL, NSUnderlyingError=0x1c4841320 {Error Domain=kCFErrorDomainCFNetwork Code=-1002 "(null)"}}
    
    使用Safari打开虽然提示无效,但是可以正常显示内容: Safari打开

    那么看来是兼容无效URL的问题了。下面是每次重定向的URL信息:

    webView:decidePolicyForNavigationAction:decisionHandler:
    <WKNavigationAction: 0x11408e660; navigationType = -1; syntheticClickType = 0; position x = 0.00 y = 0.00 request = <NSMutableURLRequest: 0x1c4002650> { URL: https://changba.com/s/9nAISuzODZd125S0d2HhOQ }; sourceFrame = (null); targetFrame = <WKFrameInfo: 0x105ccad80; webView = 0x106181000; isMainFrame = YES; request = (null)>>
    webView:decidePolicyForNavigationAction:decisionHandler:
    <WKNavigationAction: 0x105d762f0; navigationType = -1; syntheticClickType = 0; position x = 0.00 y = 0.00 request = <NSMutableURLRequest: 0x1c0011230> { URL: http://changba.com/s/9nAISuzODZd125S0d2HhOQ }; sourceFrame = <WKFrameInfo: 0x105de47d0; webView = 0x106181000; isMainFrame = YES; request = (null)>; targetFrame = <WKFrameInfo: 0x105de47d0; webView = 0x106181000; isMainFrame = YES; request = (null)>>
    webView:decidePolicyForNavigationAction:decisionHandler:
    <WKNavigationAction: 0x105d762f0; navigationType = -1; syntheticClickType = 0; position x = 0.00 y = 0.00 request = <NSMutableURLRequest: 0x1c0009640> { URL: changba://?ac=playuserwork&workid=976250206 }; sourceFrame = <WKFrameInfo: 0x105d4e590; webView = 0x106181000; isMainFrame = YES; request = <NSMutableURLRequest: 0x1c40096d0> { URL: http://changba.com/s/9nAISuzODZd125S0d2HhOQ }>; targetFrame = <WKFrameInfo: 0x105d4e590; webView = 0x106181000; isMainFrame = YES; request = <NSMutableURLRequest: 0x1c40096d0> { URL: http://changba.com/s/9nAISuzODZd125S0d2HhOQ }>>
    

    发现最后一次的URL为:changba://?ac=playuserwork&workid=976250206。host和scheme丢失了!导致最后加载失败。解决方法是加个判断条件,如果满足那么就取消加载,显示已经加载出的页面即可:

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
        NSLog(@"%@\n%@",NSStringFromSelector(_cmd),navigationAction);
        if (navigationAction.request.URL.host == nil) {
            NSArray *schemeArr = @[@"mailto",@"tel"];
            if (![schemeArr containsObject:navigationAction.request.URL.scheme]) {
                decisionHandler(WKNavigationActionPolicyCancel);
                return;
            }
        }
    

    扩展

    1.cookie

    Cookie是网站为了识别终端身份,保存在终端本地的用户凭证信息。Cookie中的字段与意义由服务端进行定义。例如,当用户在某个网站进行了登录操作后,服务端会将Cookie信息返回给终端,终端会将这些信息进行保存,在下一次再次访问这个网站时,终端会将保存的Cookie信息一并发送到服务端,服务端根据Cookie信息是否有效来判断此用户是否可以自动登录

    1.1.NSHTTPCookie

    一个NSHTTPCookie实例代表一个单独的http cookie,以指定的字典来初始化。

    - (nullable instancetype)initWithProperties:(NSDictionary<NSHTTPCookiePropertyKey, id> *)properties;
    + (nullable NSHTTPCookie *)cookieWithProperties:(NSDictionary<NSHTTPCookiePropertyKey, id> *)properties;
    

    1.2.NSHTTPCookieStorage

    NSHTTPCookieStorage实现了一个单例对象来管理共享的cookie存储,客户端可以通过这个对象来增加,删除,获取当前的cookie,也可以解析和生成cookie相关的http头字段。

    - (void)setCookie:(NSHTTPCookie *)cookie;
    - (void)deleteCookie:(NSHTTPCookie *)cookie;
    - (nullable NSArray<NSHTTPCookie *> *)cookiesForURL:(NSURL *)URL;
    @property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;
    

    2.NSURLAuthenticationChallenge

    这个类代表一个鉴权查询消息。
    NSURLCredential代表一个鉴权凭证。

    3.MIME

    MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。
    MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。
    具体可以参考:MIME 参考手册



    提升代码质量最神圣的三部曲:模块设计(谋定而后动) -->无错编码(知止而有得) -->开发自测(防患于未然)

    相关文章

      网友评论

      • Hiker5:最近公司在做一个混合开发的项目,同一套JS代码,在wkwebView上的请求,总是请求失败,而安卓上是没事的,想请问一下有没有遇到过,或者有什么解决思路吗?
        BoomLee:@Hiker5 代码方便提供下吗

      本文标题:WKWebView笔记

      本文链接:https://www.haomeiwen.com/subject/zqyynttx.html