WKWebView 使用和坑

iOS8以後,蘋果推出了新框架Wekkit,提供了替換UIWebView的組件WKWebView。各種UIWebView的問題沒有了,速度更快了,佔用內存少了,一句話,WKWebView是App內部加載網頁的最佳選擇!

先看下 WKWebView的特性:

  1. 在性能、穩定性、功能方面有很大提升(最直觀的體現就是加載網頁是佔用的內存,模擬器加載百度與開源中國網站時,WKWebView佔用23M,而UIWebView佔用85M);
  2. 允許JavaScript的Nitro庫加載並使用(UIWebView中限制);
  3. 支持了更多的HTML5特性;
  4. 高達60fps的滾動刷新率以及內置手勢;
  5. 將UIWebViewDelegate與UIWebView重構成了14類與3個協議(查看蘋果官方文檔);  這些協議沒點進去看, 如果需要就進去找吧,這個坑估計以後也不會填了 .

然後從以下幾個方面說下WKWebView的基本用法:

  1. 加載網頁
  2. 加載的狀態回調
  3. 新的WKUIDelegate協議
  4. 動態加載並運行JS代碼
  5. webView 執行JS代碼
  6. JS調用App註冊過的方法

一、加載網頁

加載網頁或HTML代碼的方式與UIWebView相同,代碼示例如下:

 WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
 NSUrl * url = [NSURL URLWithString:@"http://www.baidu.com"];
[webView loadRequest:[NSURLRequest requestWithURL:url]];
[self.view addSubview:webView];

二、加載的狀態回調 (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;
// 在發送請求之前,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;


三、新的WKUIDelegate協議

這個協議主要用於WKWebView處理web界面的三種提示框(警告框、確認框、輸入框),下面是警告框的例子:

/**
 *  web界面中有彈出alert時調用,對應不同的alert
    不同點在最後的block回調的參數,  可以無參數,Bool,和NSString 三種返回給JS.
   當然 記得給delegate 和遵守 WKUIDelegate 協議啊 .
 */

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"確認" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
    
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    //    DLOG(@"msg = %@ frmae = %@",message,frame);
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"確認" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"完成" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];

}


​

 

四、動態加載並運行JS代碼

用於在客戶端內部加入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><imgea src='http://www.nsu.edu.cn/v/2014v3/img/background/3.jpg' />"baseURL:nil];
[self.view addSubview:_webView];

五、webView 執行JS代碼

用戶調用用JS寫過的代碼,一般是h5的小夥伴開發好的方法, iOS調用:

    NSString *jsFounction = [NSString stringWithFormat:@"iosToH5('%@')", @"原生調h5"];
    [self.webView evaluateJavaScript:jsFounction completionHandler:^(id object, NSError * _Nullable error) {
        NSLog(@"obj:%@---error:%@", object, error);
    }];

六、JS調用App註冊過的方法

WKWebView裏面註冊供JS調用的方法,是通過WKUserContentController類下面的方法:

- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;

scriptMessageHandler是代理回調,JS調用name方法後,OC會調用scriptMessageHandler指定的對象。

JS在調用OC註冊方法的時候要用下面的方式:


window.webkit.messageHandlers.<name>.postMessage(<messageBody>)

注意,name(方法名)是放在中間的,messageBody只能是一個對象,如果要傳多個值,需要封裝成數組,或者字典。整個示例如下:

//OC註冊供JS調用的方法
[[_webView configuration].userContentController addScriptMessageHandler:self name:@"closeMe"];

//OC在JS調用方法做的處理
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    NSLog(@"JS 調用了 %@ 方法,傳回參數 %@",message.name,message.body);
}

// h5的代碼中 , JS這樣調用
    window.webkit.messageHandlers.closeMe.postMessage(null);


如果你在viewController的dealloc打個斷點,會發現self沒有釋放!經過一番查找,找到了這一句,猜測就是userContentController強引用了self,
[[_webView configuration].userContentController addScriptMessageHandler:self name:@"closeMe"];

找到之後就好說了, 官方還提供了remove的方法, 在Nav pop的時候調用,果然沒有了循環引用,
[userContent removeScriptMessageHandlerForName:@"closeMe"];

方法是有了,但是感覺寫起來有點麻煩,如果只有一種交互還可以忍,但是交互多了,這麼寫太累了,而且,通過代理的方式進行的回調,userContentController: didReceiveScriptMessage: 寫起來也不是很舒服,所有就想辦法封裝成block回調.這裏不貼代碼了 , 附上git地址 ,需要的話直接拖過去文件使用就行了,  https://github.com/guochaoshun/WKWebViewWithH5 

 

 

由於是ipad加載webView ,需要考慮轉屏的問題, 所以頁面縮放也得做.

UIWebView 有屬性 scalespageToFit,設置爲:YES,可以自動對頁面進行縮放以適應屏幕
那麼,WKWebView怎麼做可以實現自動縮放網頁比例 ? 

還是代理,還是協議...  

_wkWebView.scrollView.delegate = self; //   注意這裏也有一個坑,如果這麼寫了的話, 需要在  dealloc (或者其他不用的敵法)中 把 代理置nil

//     self.webView.scrollView.delegate = nil ;  否則會崩潰
之前有引用到WKWebview,爲使用方便將WKWebview設爲了成員變量,然後又設置了該成員變量的scrollview的屬性的代理爲當前視圖控制器,然後就出現了問題,每次push時候從新創建時候總會訪問之前的內存,然後報錯說訪問了一塊已經釋放掉的內存,pop出棧的時候會崩潰,這樣一直找不到問題的存在,後來才知道強引用了scrollview,代理釋放不掉,所以會報錯,解決辦法,在dealloc函數或者viewwillappear等函數中將代理設爲nil就解決了
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
    return nil;

 

 

 

坑一:獲取不到WKWebView的高度

獲取方法:在WKWebView加載成功的代理方法裏獲取WKWebView的UIScrollView的contentSize

- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
    self.webViewContentHeight = self.webView.scrollView.contentSize.height;
}

運行後,發現獲取不到contentSize, 打印結果顯示(width = 0, height =0).

解決辦法:使用KVO監聽WKWebView的contentSize


-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (!self.webView.isLoading) {
        if([keyPath isEqualToString:@"scrollView.contentSize"])
        {
            self.webViewContentHeight = self.webView.scrollView.contentSize.height;
            CGRect frame = self.webView.frame;
            frame.size.height = self.webViewContentHeight;
            self.webView.frame = frame;
            [self.webView sizeToFit];
        }
    }
}

 

Bingo! 完美取到WKWebView的height.

坑二:移除KVO的keypath時,程序crash。

- (void)dealloc {
    [self.webView removeObserver:self forKeyPath:@"scrollView.contentSize" context:nil];
}

這種情況通常出現在對同一個keypath進行兩次remove,如父類中有一個kvo, 父類在dealloc的時候remove一次,子類dealloc的時候又remove一詞。看到這裏想必大家都已經知道解決思路了吧?那就是區分父類和子類的KVO,回頭看一下發現addObserver和removeObserver中都有一個context參數,沒錯,這個參數就可以用來標記我們自己添加的KVO。

[self.webView addObserver:self forKeyPath:@"scrollView.contentSize" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:@"DJWebKitContext"];

- (void)dealloc {
    [self.webView removeObserver:self forKeyPath:@"scrollView.contentSize" context:@"DJWebKitContext"];
}

運行一下,退出頁面的時候果然不會crash了

 

 

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