WKWebView 的使用和踩過的坑

iOS8之後,蘋果推出了WebKit這個框架,用來替換原有的UIWebView,新的控件優點多多,不一一敘述。由於一直在適配iOS7,就沒有去替換,現在仍掉了iOS7,以爲很簡單的就替換過來了,然而在替換的過程中,卻遇到了很多坑。還有一點就是原來寫過一篇文章 Objective-C與JavaScript交互的那些事以爲年代久遠的UIWebView已經作古,可這篇文章現在依然有一定的閱讀量。所以在決定在續一篇此文,以引導大家轉向WKWebView,並指出自己踩過的坑,讓大家少走彎路。

此篇文章的邏輯圖


此篇文章的邏輯圖

WKWebView使用

WKWebView簡單介紹

使用及注意點

WKWebView只能用代碼創建,而且自身就支持了右滑返回手勢allowsBackForwardNavigationGestures和加載進度estimatedProgress等一些UIWebView不具備卻非常好用的屬性。在創建的時候,指定初始化方法中要求傳入一個WKWebViewConfiguration對象,一般我們使用默認配置就好,但是有些地方是要根據自己的情況去做更改。比如,配置中的allowsInlineMediaPlayback這個屬性,默認爲NO,如果不做更改,網頁中內嵌的視頻就無法正常播放。

更改User-Agent

有時我們需要在User-Agent添加一些額外的信息,這時就要更改默認的User-Agent在使用UIWebView的時候,可用如下代碼(在使用UIWebView之前執行)全局更改User-Agent

// 獲取默認User-Agent
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];

// 給User-Agent添加額外的信息
NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];

// 設置global User-Agent
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];

以上代碼是全局更改User-Agent,也就是說,App內所有的Web請求的User-Agent都被修改。替換爲WKWebView後更改全局User-Agent可以繼續使用上面的一段代碼,或者改爲用WKWebView獲取默認的User-Agent,代碼如下:

self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero];

// 獲取默認User-Agent
[self.wkWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
    NSString *oldAgent = result;

    // 給User-Agent添加額外的信息
    NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];

    // 設置global User-Agent
    NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newAgent, @"UserAgent", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
}];

對比發現,這兩種方法並沒有本質的區別,一點小區別在於一個是用UIWebView獲取的默認User-Agent,一個是用WKWebView獲取的默認User-Agent。上面方法的缺點也是很明顯的,就是App內所有Web請求的User-Agent全部被修改。

iOS9WKWebView提供了一個非常便捷的屬性去更改User-Agent,就是customUserAgent屬性。這樣使用起來不僅方便,也不會全局更改User-Agent,可惜的是iOS9纔有,如果適配iOS8,還是要使用上面的方法。

WKWebView的相關的代理方法

WKWebView的相關的代理方法分別在WKNavigationDelegateWKUIDelegate以及WKScriptMessageHandler這個與JavaScript交互相關的代理方法。

  • WKNavigationDelegate: 此代理方法中除了原有的UIWebView的四個代理方法,還增加了其他的一些方法,具體可參考我下面給出的Demo
  • WKUIDelegate: 此代理方法在使用中最好實現,否則遇到網頁alert的時候,如果此代理方法沒有實現,則不會出現彈框提示。
  • WKScriptMessageHandler: 此代理方法就是和JavaScript交互相關,具體介紹參考下面的專門講解。

WKWebView使用過程中的坑

WKWebView下面添加自定義View

因爲我們有個需求是在網頁下面在添加一個View,用來展示此鏈接內容的相關評論。在使用UIWebView的時候,做法非常簡單粗暴,在UIWebViewScrollView後面添加一個自定義View,然後根據View的高度,在改變一下scrollViewcontentSize屬性。以爲WKWebView也可以這樣簡單粗暴的去搞一下,結果卻並不是這樣。

首先改變WKWebViewscrollViewcontentSize屬性,系統會在下一次幀率刷新的時候,再給你改變回原有的,這樣這條路就行不通了。我馬上想到了另一個辦法,改變scrollViewcontentInset這個系統倒不會在變化回原來的,自以爲完事大吉。後來過了兩天,發現有些頁面的部分區域的點擊事件無法響應,百思不得其解,最後想到可能是設置的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];
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。如果你有解決辦法,也歡迎私信我,感激不盡。

WKWebView加載POST請求

非常感謝@e231e1ff5f8b的指出,原來POST請求這兒還有一個坑。自己項目中並沒有這塊需求,也就沒有發現。加載POST請求的時候,會丟失HTTPBody。解決辦法是在網頁上開一個JavaScript方法,在請求POST的時候去調用JavaScript這個方法,從而完成POST請求。調用JavaScript方法參考下面交互這一章節。

WKWebView和JavaScript交互

WKWebViewJavaScript交互,在WKUserContentController.h這個頭文件中- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;這個方法的註釋中已經明確給出了交互辦法。使用起來倒是非常的簡單。創建WKWebView的時候添加交互對象,並讓交互對象實現WKScriptMessageHandler中的唯一的一個代理方法。具體的方式參考Demo中的使用。

// 添加交互對象
[config.userContentController addScriptMessageHandler:(id)self.ocjsHelper name:@"timefor"];

/** 此點後來更新,如果不移除交互對象,則導致交互對象內存常駐(2016.12.17) */
// VC銷燬時,移除交互對象
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"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中
}];

總結

此文主要介紹了WKWebView使用中的注意點,一般也都是常用的,還有緩存等一些不是太常用的就沒有具體介紹。如果在其他方面遇到問題,也歡迎你私信我共同探討進步。WKWebView確實比UIWebView有些地方好用不少,但是一些bug至今也沒解決,權限挑戰是在iOS9解決的,POST請求則至今沒有解決,而改變contentInset導致的點擊事件不準確,同樣是沒有解決。這些問題讓開發者使用起來,有諸多不便啊。
此文的Demo地址:WKWebViewDemo 如果此文對你有所幫助,請給個star吧。

更新

  • 2016.12.17
    // VC銷燬時,移除交互對象;如果不移除交互對象,則導致交互對象內存常駐而引起內存泄漏
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"timefor"];

參考

發佈了60 篇原創文章 · 獲贊 17 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章