一、WKWebView簡介
UIWebView自iOS2就有,WKWebView從iOS8才有,毫無疑問WKWebView將逐步取代笨重的UIWebView。通過簡單的測試即可發現UIWebView占用過多內存,且內存峰值更是夸張。WKWebView網頁加載速度也有提升,但是并不像內存那樣提升那么多。下面列舉一些其它的優勢:
1、更多的支持HTML5的特性
2、官方宣稱的高達60fps的滾動刷新率以及內置手勢
3、Safari相同的JavaScript引擎,且允許JavaScript的Nitro庫加載并使用(UIWebView中限制);
4、將UIWebViewDelegate與UIWebView拆分成了14類與3個協議(官方文檔說明)
5、占用更少的內存,在性能、穩定性、功能方面有很大提升(最直觀的體現就是加載網頁是占用的內存,模擬器加載百度與開源中國網站時,WKWebView占用23M,而UIWebView占用85M);
另外用的比較多的,增加加載進度屬性:estimatedProgress
二、WKWebView初始化
1. 首先需要引入WebKit庫
import <WebKit/WebKit.h>
2. 初始化方法分為以下兩種
// 默認初始化
- (instancetype)initWithFrame:(CGRect)frame;
// 根據對webview的相關配置,進行初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
3. 加載網頁與HTML代碼的方式與UIWebView相同,代碼如下:
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];
[self.view addSubview:webView];
三、屬性介紹
WKBackForwardList:之前訪問過的 web頁面的列表,可以通過后退和前進動作來訪問到。
WKBackForwardListItem: webview中后退列表里的某一個網頁。
WKFrameInfo: 包含一個網頁的布局信息。
WKNavigation: 包含一個網頁的加載進度信息。
WKNavigationAction:包含可能讓網頁導航變化的信息,用于判斷是否做出導航變化。
WKNavigationResponse:包含可能讓網頁導航變化的返回內容信息,用于判斷是否做出導航變化。
WKPreferences: 概括一個 webview 的偏好設置。
WKProcessPool: 表示一個 web 內容加載池。
WKUserContentController: 提供使用 JavaScript post 信息和注射 script 的方法。
WKScriptMessage: 包含網頁發出的信息。
WKUserScript:表示可以被網頁接受的用戶腳本。
WKWebViewConfiguration: 初始化 webview 的設置。
WKWindowFeatures: 指定加載新網頁時的窗口屬性。
WKNavigationDelegate: 提供了追蹤主窗口網頁加載過程和判斷主窗口和子窗口是否進行頁面加載新頁面的相關方法。
WKScriptMessageHandler: 提供從網頁中收消息的回調方法。
WKUIDelegate: 提供用原生控件顯示網頁的方法回調。
四、WKWebView代理
WKWebView有兩個delegate,WKUIDelegate 和 WKNavigationDelegate。WKNavigationDelegate主要處理一些跳轉、加載處理操作,WKUIDelegate主要處理JS腳本,確認框,警告框等。因此WKNavigationDelegate更加常用。
1. WKNavigationDelegate
該代理提供的方法,可以用來追蹤加載過程(頁面開始加載、加載完成、加載失敗)、決定是否執行跳轉。
#pragma mark - WKNavigationDelegate
// 頁面開始加載時調用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{
}
// 當內容開始返回時調用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation{
}
// 頁面加載完成之后調用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
}
// 頁面加載失敗時調用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation{
}
頁面跳轉的代理方法有三種,分為(收到跳轉與決定是否跳轉兩種)
// 接收到服務器跳轉請求之后調用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
}
// 在收到響應后,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSLog(@"%@",navigationResponse.response.URL.absoluteString);
//允許跳轉
decisionHandler(WKNavigationResponsePolicyAllow);
//不允許跳轉
//decisionHandler(WKNavigationResponsePolicyCancel);
}
// 在發送請求之前,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
NSLog(@"%@",navigationAction.request.URL.absoluteString);
//允許跳轉
decisionHandler(WKNavigationActionPolicyAllow);
//不允許跳轉
//decisionHandler(WKNavigationActionPolicyCancel);
}
2. WKUIDelegate
#pragma mark - WKUIDelegate
// 創建一個新的WebView
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures{
return [[WKWebView alloc]init];
}
// 輸入框
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler{
completionHandler(@"http");
}
// 確認框
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler{
completionHandler(YES);
}
// 警告框
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
NSLog(@"%@",message);
completionHandler();
}
3. WKScriptMessageHandler
這個協議中包含一個必須實現的方法,這個方法是提高App與web端交互的關鍵,它可以直接將接收到的JS腳本轉為OC或Swift對象。(當然,在UIWebView也可以通過“曲線救國”的方式與web進行交互,著名的Cordova框架就是這種機制)
// 從web界面中接收到一個腳本時調用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
4、WKWebView加載JS
// 圖片縮放的js代碼
NSString *js = @"var count = document.images.length;for (var i = 0; i < count; i++) {var image = document.images[i];image.style.width=320;};window.alert('找到' + count + '張圖');";
// 根據JS字符串初始化WKUserScript對象
WKUserScript *script = [[WKUserScript alloc] initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
// 根據生成的WKUserScript對象,初始化WKWebViewConfiguration
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
[config.userContentController addUserScript:script];
_webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
[_webView loadHTMLString:@"<head></head>"baseURL:nil];
[self.view addSubview:_webView];
五、WKWebView使用過程中的坑
1、WKWebView下面添加自定義View
因為我們有個需求是在網頁下面在添加一個View,用來展示此鏈接內容的相關評論。在使用UIWebView的時候,做法非常簡單粗暴,在UIWebView的ScrollView后面添加一個自定義View,然后根據View的高度,在改變一下scrollView的contentSize屬性。以為WKWebView也可以這樣簡單粗暴的去搞一下,結果卻并不是這樣。
首先改變WKWebView的scrollView的contentSize屬性,系統會在下一次幀率刷新的時候,再給你改變回原有的,這樣這條路就行不通了。我馬上想到了另一個辦法,改變scrollView的contentInset這個系統倒不會在變化回原來的,自以為完事大吉。后來過了兩天,發現有些頁面的部分區域的點擊事件無法響應,百思不得其解,最后想到可能是設置的contentInset對其有了影響,事實上正是如此。查來查去,最后找到了一個解決辦法是,就是當頁面加載完成時,在網頁下面拼一個空白的div,高度就是你添加的View的高度,讓網頁多出一個空白區域,自定義的View就添加在這個空白的區域上面。這樣就完美解決了此問題。具體可參考Demo所寫,核心代碼如下:
self.addView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, addViewHeight)];
self.addView.backgroundColor = [UIColor redColor];
[self.webView.scrollView addSubview:self.addView];
NSString *js = [NSString stringWithFormat:@"\
var appendDiv = document.getElementById(\"AppAppendDIV\");\
if (appendDiv) {\
appendDiv.style.height = %@+\"px\";\
} else {\
var appendDiv = document.createElement(\"div\");\
appendDiv.setAttribute(\"id\",\"AppAppendDIV\");\
appendDiv.style.width=%@+\"px\";\
appendDiv.style.height=%@+\"px\";\
document.body.appendChild(appendDiv);\
}\
", @(addViewHeight), @(self.webView.scrollView.contentSize.width), @(addViewHeight)];
[self.webView evaluateJavaScript:js completionHandler:nil];
2、WKWebView加載HTTPS的鏈接
HTTPS已經越來越被重視,前面我也寫過一系列的HTTPS的相關文章HTTPS從原理到應用(四):iOS中HTTPS實際使用當加載一些HTTPS的頁面的時候,如果此網站使用的根證書已經內置到了手機中這些HTTPS的鏈接可以正常的通過驗證并正常加載。但是如果使用的證書(一般為自建證書)的根證書并沒有內置到手機中,這時是鏈接是無法正常加載的,必須要做一個權限認證。開始在UIWebView的時候,是把請求存儲下來然后使用NSURLConnection去重新發起請求,然后走NSURLConnection的權限認證通道,認證通過后,在使用UIWebView去加載這個請求。
在WKWebView中,WKNavigationDelegate中提供了一個權限認證的代理方法,這是權限認證更為便捷。代理方法如下:
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([challenge previousFailureCount] == 0) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
} else {
completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
}
}
這個方法比原來UIWebView的認證簡單的多。但是使用中卻發現了一個很蛋疼的問題,iOS8系統下,自建證書的HTTPS鏈接,不調用此代理方法。查來查去,原來是一個bug,在iOS9中已經修復,這明顯就是不管iOS8的情況了,而且此方法也沒有標記在iOS9中使用,這點讓我感到有點失望。這樣我就又想到了換回原來UIWebView的權限認證方式,但是試來試去,發現也不能使用了。所以關于自建證書的HTTPS鏈接在iOS8下面使用WKWebView加載,我沒有找到很好的辦法去解決此問題。這樣我不得已有些鏈接換回了HTTP,或者在iOS8下面在換回UIWebView。如果你有解決辦法,也歡迎私信我,感激不盡。
3、WKWebView和JavaScript交互
WKWebView和JavaScript交互,在WKUserContentController.h這個頭文件中- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;這個方法的注釋中已經明確給出了交互辦法。使用起來倒是非常的簡單。創建WKWebView的時候添加交互對象,并讓交互對象實現WKScriptMessageHandler中的唯一的一個代理方法。具體的方式參考Demo中的使用。
// 添加交互對象
[config.userContentController addScriptMessageHandler:(id)self.ocjsHelper name:@"timefor"];
// 代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
JavaScript調用Objective-C的時候,使用window.webkit.messageHandlers.timefor.postMessage({code: '0001', functionName: 'getdevideId'}); Objective-C自動對交互參數包裝成了WKScriptMessage對象,其屬性body則為傳送過來的參數,name為添加交互對象的時候設置的名字,以此名字可以過濾掉不屬于自己的交互方法。其中body可以為NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull。
而Objective-C在回調JavaScript的時候,不能像我原來在 Objective-C與JavaScript交互的那些事這篇文章中寫的那樣,JavaScript傳過來一個匿名函數,Objective-C這邊直接調用一下就完事。WKWebView沒有辦法傳過來一個匿名函數,所以回調方式,要么執行一段JavaScript代碼,或者就是調用JavaScript那邊的一個全局函數。一般是采用后者,至于Web端雖說暴露了一個全局函數,同樣可以把這一點代碼處理的很優雅。Objective-C傳給JavaScript的參數,可以為Number, String, and Object。參考如下:
// 數字
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", number];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 字符串
NSString *js = [NSString stringWithFormat:@"globalCallback(\'%@\')", string];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 對象
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", @{@"name" : @"timefor"}];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 帶返回值的JS函數
[self.webView evaluateJavaScript:@"globalCallback()" completionHandler:^(id result, NSError * _Nullable error) {
// 接受返回的參數,result中
}];