WKWebView笔记

前言

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 )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 *)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

WKWebView笔记_第1张图片
识别网页内容

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:
,error:Error Domain=NSURLErrorDomain Code=-999 "(null)" UserInfo={NSErrorFailingURLStringKey=http://changba.com/s/9nAISuzODZd125S0d2HhOQ, NSErrorFailingURLKey=http://changba.com/s/9nAISuzODZd125S0d2HhOQ, _WKRecoveryAttempterErrorKey=}

webView:didFailProvisionalNavigation:withError:
,error:Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={_WKRecoveryAttempterErrorKey=, NSErrorFailingURLStringKey=changba://?ac=playuserwork&workid=976250206, NSErrorFailingURLKey=changba://?ac=playuserwork&workid=976250206, NSLocalizedDescription=unsupported URL, NSUnderlyingError=0x1c4841320 {Error Domain=kCFErrorDomainCFNetwork Code=-1002 "(null)"}}

使用Safari打开虽然提示无效,但是可以正常显示内容:
WKWebView笔记_第2张图片
Safari打开

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

webView:decidePolicyForNavigationAction:decisionHandler:
 { URL: https://changba.com/s/9nAISuzODZd125S0d2HhOQ }; sourceFrame = (null); targetFrame = >
webView:decidePolicyForNavigationAction:decisionHandler:
 { URL: http://changba.com/s/9nAISuzODZd125S0d2HhOQ }; sourceFrame = ; targetFrame = >
webView:decidePolicyForNavigationAction:decisionHandler:
 { URL: changba://?ac=playuserwork&workid=976250206 }; sourceFrame =  { URL: http://changba.com/s/9nAISuzODZd125S0d2HhOQ }>; targetFrame =  { 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 *)properties;
+ (nullable NSHTTPCookie *)cookieWithProperties:(NSDictionary *)properties;

1.2.NSHTTPCookieStorage

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

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

2.NSURLAuthenticationChallenge

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

3.MIME

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



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

你可能感兴趣的:(WKWebView笔记)