前言
iOS8開始,蘋果引入了新的web控件WKWebView替代UIWebView,WKWebView屬于WebKit框架,WebKit框架的API極為豐富,可以從WKWebView入手逐個了解。WebKit框架也在持續(xù)更新中,iOS9,iOS 10都引入了新的API,趨勢就是趕緊廢棄UIWebView使用WKWebView吧。本文是升級項目中的UIWebView的一些經(jīng)驗和遇到的坑,希望可以幫助到大家。
1. WKWebView簡介
一個WKWebView用來展示可交互的網(wǎng)頁內(nèi)容,就像一個APP內(nèi)的瀏覽器。你可以使用WKWebView在你的APP中嵌入網(wǎng)頁內(nèi)容。
1.1.WKUserContentController
這個屬性非常重要,js->oc的交互全靠它。
- 1.動態(tài)注入js,注入的既可以是js代碼,也可以是一個js文件。
WKUserScript *script = [[WKUserScript alloc] initWithSource:@"alert('哈哈');" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
[controller addUserScript:script];
- 2.JavaScript向
WKWebView發(fā)送消息,通過識別不同的消息和消息的內(nèi)容,可以執(zhí)行不同的native操作。
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
遵循WKScriptMessageHandler協(xié)議的對象可以在以下代理方法接收J(rèn)avaScript發(fā)送的消息。
- (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來設(shè)置:
[[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": newUserAgent}];
1.3.屬性支持kvo
WKWebView的大部分屬性是支持kvo的,但是并沒有提供代理方法,需要自己添加監(jiān)聽。例如監(jiān)聽title和estimatedProgress。
[self.wkWebView addObserver:self forKeyPath:@"estimatedProgress" options:NSKeyValueObservingOptionNew context:nil];
[self.wkWebView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionNew context:nil];
這樣就可以展示進度條,無需等待web完全加載完畢才顯示標(biāo)題
- (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;
}
}
}
記得刪除監(jiān)聽
- (void)dealloc{
NSLog(@"%@",NSStringFromSelector(_cmd));
[self.wkWebView removeObserver:self forKeyPath:@"estimatedProgress" context:nil];
[self.wkWebView removeObserver:self forKeyPath:@"title" context:nil];
}
1.4.識別網(wǎng)頁內(nèi)容
長按網(wǎng)頁,會彈出一個UIActionSheet。例如長按一張圖片會提示你保存圖片。如果想在UIWebView實現(xiàn)這個功能只能自定義一個手勢然后通過js獲取網(wǎng)頁內(nèi)容再彈出UIActionSheet。

1.5.JS
UIWebView調(diào)用jsstringByEvaluatingJavaScriptFromString是同步返回的,并沒有提供狀態(tài)信息。WebKit是異步block回調(diào)的,并帶有狀態(tài)信息。使用時要注意在頁面銷毀時恰好進行了回調(diào)在iOS8上會crash。
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
2.一些坑
2.1.cookie問題
這應(yīng)該是WebKit最大的坑,網(wǎng)上有好多文章介紹了原因和解決方法,我就不畫蛇添足了,貼上個鏈接:cookie問題,我這里記錄下解決方法。
問題所在:WKWebView加載網(wǎng)頁得到的Cookie會同步到NSHTTPCookieStorage中,但是WKWebView加載請求時,不會同步NSHTTPCookieStorage中已有的Cookie,所以導(dǎo)致Cookie丟失,web無法識別客戶端身份。
解決方法:將NSHTTPCookieStorage存儲的Cookie設(shè)置為請求的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
模擬慢速網(wǎng)絡(luò)時經(jīng)常出現(xiàn)這種錯誤,進度加載一部分后退回到0。調(diào)試時發(fā)現(xiàn)是在創(chuàng)建NSMutableURLRequest是設(shè)置的超時時間過短導(dǎo)致的。解決方法是加大超時時間或者干脆不設(shè)置超時時間。
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)
分析發(fā)現(xiàn),在WKWebView釋放之后竟然還進行了scrollView代理的設(shè)置,而這個時候的self,也就是當(dāng)前的控制器處于銷毀當(dāng)中,也就解釋了上面log提到的or is in the process of deallocation.。所以加入你的WKWebView是懶加載的,不要在懶加載中設(shè)置代理,其次在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調(diào)用js時crash,以下是錯誤信息。這個并沒有發(fā)現(xiàn)錯在哪里,APP刪除重新安裝后就沒復(fù)現(xiàn)過。如果有遇到同樣問題的,請不吝賜教。
//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加載失?。?a target="_blank" rel="nofollow">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打開雖然提示無效,但是可以正常顯示內(nèi)容:
那么看來是兼容無效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 }>>
發(fā)現(xiàn)最后一次的URL為:changba://?ac=playuserwork&workid=976250206。host和scheme丟失了!導(dǎo)致最后加載失敗。解決方法是加個判斷條件,如果滿足那么就取消加載,顯示已經(jīng)加載出的頁面即可:
- (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是網(wǎng)站為了識別終端身份,保存在終端本地的用戶憑證信息。Cookie中的字段與意義由服務(wù)端進行定義。例如,當(dāng)用戶在某個網(wǎng)站進行了登錄操作后,服務(wù)端會將Cookie信息返回給終端,終端會將這些信息進行保存,在下一次再次訪問這個網(wǎng)站時,終端會將保存的Cookie信息一并發(fā)送到服務(wù)端,服務(wù)端根據(jù)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實現(xiàn)了一個單例對象來管理共享的cookie存儲,客戶端可以通過這個對象來增加,刪除,獲取當(dāng)前的cookie,也可以解析和生成cookie相關(guān)的http頭字段。
- (void)setCookie:(NSHTTPCookie *)cookie;
- (void)deleteCookie:(NSHTTPCookie *)cookie;
- (nullable NSArray<NSHTTPCookie *> *)cookiesForURL:(NSURL *)URL;
@property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;
2.NSURLAuthenticationChallenge
這個類代表一個鑒權(quán)查詢消息。
NSURLCredential代表一個鑒權(quán)憑證。
3.MIME
MIME (Multipurpose Internet Mail Extensions) 是描述消息內(nèi)容類型的因特網(wǎng)標(biāo)準(zhǔn)。
MIME 消息能包含文本、圖像、音頻、視頻以及其他應(yīng)用程序?qū)S玫臄?shù)據(jù)。
具體可以參考:MIME 參考手冊
提升代碼質(zhì)量最神圣的三部曲:模塊設(shè)計(謀定而后動) -->無錯編碼(知止而有得) -->開發(fā)自測(防患于未然)