前言
iOS8开始,苹果引入了新的web控件WKWebView
替代UIWebView
,WKWebView
属于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的,但是并没有提供代理方法,需要自己添加监听。例如监听title
和estimatedProgress
。
[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 参考手册
提升代码质量最神圣的三部曲:模块设计(谋定而后动) -->无错编码(知止而有得) -->开发自测(防患于未然)
网友评论