WK 與 JS 的那些事 WKWebView使用

 

蘋果在iOS 8中推出了 WKWebView,這是一個高性能的 web 框架,相較於 UIWebView 來講,有巨大提高。本文將針對 WKWebView 進行簡單介紹,而後介紹下如何和 JS 進行愉快的交互。還望各位大佬不吝賜教。git

本文分爲兩大部分github

  1. WKWebView 簡單介紹
  2. JS 交互

1 WKWebView

就目前移動開發趨勢來講,不少 APP 都會嵌套一些 H5 的應用。H5 有一些 Native 沒法比擬的優點,例如:更新快,不用發版,隨時上線等等。然而在 iOS 中, UIWebView 是及其難用的。隨着 iOS 8 的推出,Apple 重構了 UIWebView,因而 WKWebView 橫空出世。web

1.1 WKWebView VS UIWebView

根據官方文檔,咱們來簡單對比一下 UIWebView 和 WKWebView,看看這兩個到底有什麼區別算法

  WKWebView UIWebView
內存佔用 大 且有內存泄漏
加載速度
與 JS 交互 難 (可與 JSCore 配合)
幀率 60FPS 掉幀

從文檔來看,兩者區別仍是很明顯的,但到底區別有多大的,咱們用數聽說話。打開京東,網易,新浪這三個網站,從打開時間和佔用內存上來比較一下,看誰能勝出。該測試在 2015款 MBP 上打開,使用 Xcode 9 GM 版,在 iPhone 8 Plus 上運行json

使用 WKWebView 和 UIWebView 打開 京東 網易 新浪 三個網站所耗費的時長

使用 WKWebView 和 UIWebView 打開 京東 網易 新浪 三個網站所耗費的時長數組

使用 WKWebView 和 UIWebView 打開 京東 網易 新浪 三個網站所耗費的內存

使用 WKWebView 和 UIWebView 打開 京東 網易 新浪 三個網站所耗費的內存安全

在內存測試中發現,UIWebView 佔用內存很不穩定,在打開新浪的網站時,最高內存能飆升到 200m 後來慢慢回落到 160m 左右,但會上下波動。但 WKWebView 上就沒有這個問題。經過上述對比,不難看出,WKWebVeiw 要優於 UIWebView。網絡

1.2 如何使用 WKWebView

得益於蘋果 API 的高度封裝,咱們使用 WKWebView 及其簡單app

- (WKWebView *)wkWebView {
    if (!_wkWebView) {
        
        _wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:[WKWebViewConfiguration new]]; //1. 
        NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.jd.com"]]; //2.
        [_wkWebView loadRequest:request]; //3. 
    }
    return _wkWebView;
}
  1. 初始化一個 WKWebView,咱們須要傳一個 WKWebViewConfiguration 對象,來對 WKWebView 進行配置。
  2. 構造一個請求。
  3. 加載這個請求。

只須要這三步,咱們就可使用一個高性能的 web 框架。是否是很贊!!!
關於 WKWebView 如何使用,這裏就不作過多的詳細介紹了,網上這種文章太多了,你們能夠自行翻閱。接下來咱們說如何與 JS 交互。框架

2. JS 交互

WebVeiw 與 JS 交互是一個很古老的問題,如何與 JS 交互是一個 WebVeiw 必須具有的能力,在 UIWebView 時代,咱們能夠經過攔截 URL 的方式來進行交互,也能夠經過 WebViewJavascriptBridge 來進行交互,還能夠配合 JSCore 來進行交互。可是在 WKWebView 時代,因爲它是在一個單獨的進程中運行,咱們沒法獲取到 JSContext,因此咱們沒法使用 JSCore 這個強大的框架來進行交互,那咱們怎麼辦呢,且聽我一一道來。

2.1 Native 調用 JS

還記的上邊說的 WKWebViewConfiguration 麼,在這個類裏邊,有一個屬性

@property (nonatomic, strong) WKUserContentController *userContentController;

Native 和 H5 交互基本全靠這個對象, 在 WKWebVeiw 中,咱們使用咱們有兩種方式來調用 JS,

  1. 使用 WKUserScript
  2. 直接調用 JS 字符串

2.1.1 使用 WKUserScript

要想使用 WKUserScript,首先,咱們要構造一個 WKUserScript 對象,構造方法及其簡單,咱們使用下邊代碼來建立一個 WKUserScript 對象。

// source 就是咱們要調用的 JS 函數或者咱們要執行的 JS 代碼
// injectionTime 這個參數咱們須要指定一個時間,在何時把咱們在這段 JS 注入到 WebVeiw 中,它是一個枚舉值,WKUserScriptInjectionTimeAtDocumentStart 或者 WKUserScriptInjectionTimeAtDocumentEnd
// MainFrameOnly 由於在 JS 中,一個頁面可能有多個 frame,這個參數指定咱們的 JS 代碼是否只在 mainFrame 中生效
- initWithSource:injectionTime:forMainFrameOnly:

至此,咱們已經構建了一個 WKUserScript,而後呢,咱們要作的就是要把它添加進來

- addUserScript:

至此使用 WKUserScript 調用 JS 完成。

2.1.2 直接調用 JS 字符串

在 WKWebView 中,咱們也能夠直接執行 JS 字符串

- (void)evaluateJavaScript: completionHandler:

咱們經過調用這個方法來執行 JS 字符串,而後在 completionHandler 中拿到執行這段 JS 代碼後的返回值。

至此,Native 調用 JS 完成。是否是簡單到懼怕

2.2 JS 調用 Native

在 WK 這套框架下,JS 調用 Native 簡直簡單到喪心病狂。還記的上邊那個 WKUserContentController,咱們也是要經過它來進行,而你所須要作的,只須要三步,須要三步,三步。

  1. 向 JS 注入一個字符串
[_webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeMethod"];

咱們向 JS 注入了一個方法,叫作 nativeMethod

  1. JS 調用 Native
window.webkit.messageHandlers.nativeMethod.postMessage(value);

一句話調用,咱們就能夠在 Native 中接收到 value

  1. 接收 JS 調用

上邊咱們調用 addScriptMessageHandler:name 的時候,咱們要遵照 WKScriptMessageHandler 協議,而後實現這個協議。

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
 NSString * name = message.name // 就是上邊注入到 JS 的哪一個名字,在這裏是 nativeMethod
 id param = message.body // 就是 JS 調用 Native 時,傳過來的 value
 // TODO: do your stuff
}

完了,Native 調用 JS 就這麼簡單,是否是喪心病狂,簡直簡單到不能再簡單了。

可是,你覺得這麼就完了麼,上邊寫的這些東西在網上隨便一搜都有一大片,從新再寫一遍,貌似意義不是很大啊,怎麼也得來點稍微不同的東西吧。

2.3 JS 調用 Native 後的回調

舉一個很常見的例子,假設咱們有這麼一個需求,個人 JS 要調用 Native 發一個網絡請求,Native 執行完了,把請求數據回傳給 JS。
很簡單的一個需求,來,想一想怎麼執行。

2.3.1 postMessage 的坑

可能很快就想到了,postMessage 的時候,直接把這個方法傳過去不就好了。一開始我也是這麼作的。

const person = {
        firstName: "John",
        lastName: "Doe",
        age: 50,
        eyeColor: "blue",
    };
    document.getElementById("li1").onclick = function (nativeValue) {
        person.callBack = function () {
            console.log("native call");
        }
        window.webkit.messageHandlers.nativeMethod.postMessage(person);
    };

首先構造一個 person,而後咱們給 person 增長一個 callBack 屬性,而後傳進去,運行程序。打開 Safari 選擇 開發->模擬器,打開調試界面,而後咱們點擊查看控制檯。


而後你會發現,報錯了,爲何呢,這一切都是由於 postMessag 這個方法。
打開 postMessage文檔 ,你會發現,

message
將要發送到其餘 window的數據。它將會被結構化克隆算法序列化。這意味着你能夠不受什麼限制的將數據對象安全的傳送給目標窗口而無需本身序列化

這個 message 須要支持 結構化克隆算法 。很遺憾,這個算法目前不支持傳遞 FunctionError,它只支持一下幾種類型

對象類型 注意
全部的原始類型 除了symbols
Boolean 對象
String 對象
Date  
RegExp lastIndex 字段不會被保留。
Blob  
File  
FileList  
ArrayBuffer  
ArrayBufferView 這基本上意味着全部的 類型化數組 ,好比 Int32Array 等等。
ImageData  
Array  
Object 僅包括普通對象 (好比對象字面量 )
Map  
Set  

說好的不受限制呢

15088520633631.jpg

15088520633631.jpg

2.3.2 function 轉爲 字符串

那既然它不支持傳一個 Function ,那咱們就得另闢蹊徑了,String 總支持吧,咱們把一個方法轉爲字符串,而後傳到 Native,而後 Native 執行這個字符串。貌似可行的,咱們來試一下。

JS 代碼

document.getElementById("li1").onclick = function () {

        person.callBack = function (nativeValue) {
            console.log("native call");
        }.toString();
        window.webkit.messageHandlers.nativeMethod.postMessage(person);
    };

OC 代碼

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    if ([message.name isEqualToString:@"nativeMethod"]) {
        NSLog(@"body:%@, ", message.body);
        NSDictionary *dict = @{@"key1": @"value1",
                               @"key2": @"value2"
                               }; // 構造回傳 js 數據
        id data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
        NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // 轉爲 json 字符串
        [_webView evaluateJavaScript:[NSString stringWithFormat:@"(%@)(%@)", message.body[@"callBack"], jsonString] completionHandler:^(id _Nullable jsData, NSError * _Nullable error) {
            
        }];        
    }
}

果真不出咱們所料,咱們能夠直接獲得這個 Native 傳遞給 JS 的值。
可是,這個做用域會不會變化呢,咱們在來改一下 JS 代碼

document.getElementById("li1").onclick = function () {

    var arg1 = 100;
    var arg2 = 200;
    person.callBack = function (nativeValue) {
        console.log(nativeValue);
        console.log(arg1 + arg2);
    }.toString();
    window.webkit.messageHandlers.nativeMethod.postMessage(person);
};

你們猜能不能打印出來 300,咱們來試一下。

完蛋,找不到 arg1。。。。

怎麼回事呢?

咱們把一個 function 轉換成 字符串以後,傳給 Native,Native 在執行的時候,他的做用域已經變了,變成了 window,這個時候,window 下是沒有 arg1 和 arg2 的,因此咱們找不到。

若是咱們這麼作的話,確實是能夠實現上述的需求的,可是,這樣做用域就改變了,全部的變量都要定義爲全局變量,函數要改成全局函數,以遍可以在回調中獲取正確的變量。

這確實是一個可行的方法,但有沒有更好的方法呢?H5 原本寫的好好的,匿名函數寫的 6 的飛起,幹嗎都要改爲全局變量,全局函數,要是這麼寫,我都很差意思給 H5 提需求讓人家改。

我就想,能不能像 UIWebView 同樣使用 JSCore,可是使用 JSCore 的話,咱們要獲取 JSContext,而 WKWebView 是運行在一個單獨的進程中,咱們是不可能進行應用間的通訊的(目前我沒發現,若是有的話,還請多多指教)。我就想,要不去扒一扒 WebKit 的源碼,看看會有什麼發現。

2.3.3 改下源碼 ?

而後我就找啊找,終於找到了關鍵的方法

virtual void didPostMessage(WebKit::WebPageProxy& page, WebKit::WebFrameProxy& frame, const WebKit::SecurityOriginData& securityOriginData, WebCore::SerializedScriptValue& serializedScriptValue)
{
   @autoreleasepool {
       RetainPtr<WKFrameInfo> frameInfo = wrapper(API::FrameInfo::create(frame, securityOriginData.securityOrigin()));
    
       ASSERT(isUIThread());
       static JSContext* context = [[JSContext alloc] init]; //1. 建立一個 JSContext
    
       JSValueRef valueRef = serializedScriptValue.deserialize([context JSGlobalContextRef], 0);
       JSValue *value = [JSValue valueWithJSValueRef:valueRef inContext:context];
       id body = value.toObject; // 把 JS 的類型轉爲 OC 類型
    
       auto message = adoptNS([[WKScriptMessage alloc] _initWithBody:body webView:fromWebPageProxy(page) frameInfo:frameInfo.get() name:m_name.get()]); // 構造 message
  
       [m_handler userContentController:m_controller.get() didReceiveScriptMessage:message.get()]; // 調用代理對象,傳遞 message
   }
}

看到這裏,我想,能不能把這個 JSContext 漏出來,這樣的話,說不定還能想 UIWebView 和 JSCore 同樣。可是轉念一想,WKWebView 從 iOS 8 就出現了,如今到 iOS 11 了,難道都沒想過如何解決回調這個問題麼?難道蘋果那幫開發都沒發現麼?怎麼辦,這不科學啊。

2.3.4 我有一個同窗

其實,咱們一開始就想錯了。一直在想,如何把這個方法傳過來,其實縱使能把一個 function 傳過來,咱們也沒有辦法去執行,由於咱們能執行的只有一個字符串,而這個字符串執行後做用域確定是會變的。因此,歸根到底,這是 H5 的工做,咱們作不了,想要支持回調,讓 H5 本身去研究。我敢保證,你若是這麼去給 H5 說,他追出去三條街,也要把砍你。

咱們要先幫 H5 解決這個問題,咱們才能去推進 H5 解決這個問題。

然而,我有一個同窗,一個作 H5 的同窗,@勵志成爲網紅的網黃,在我苦苦思索不能解決的時候,我給他說了個人問題。而後咱們就這個問題和見解進行了深刻的探討和交流。在達成了某些不可描述的交易以後,咱們終於找到了一種解決辦法。

他說,能夠用 BroadcastChannel 來解決這個問題。

BroadcastChannel API 容許同一原始域和用戶代理下的全部窗口,iFrames等進行交互。也就是說,若是用戶打開了同一個網站的的兩個標籤窗口,若是網站內容發生了變化,那麼兩個窗口會同時獲得更新通知。

而後進行了一波研究以後,發現 API 不支持。有興趣的能夠研究這個 API

而後,咱們繼續進行交易,好在,此次交易,取得了重大成功。
有一天,他在看 Vue 的源碼時,發現了這麼一個類 MessageChannel ,看起來能夠解決這個問題。

官方文檔上這麼說

Channel Messaging API的MessageChannel接口容許咱們建立一個新的消息通道,並經過它的兩個MessagePort屬性發送數據

它有兩個端口,port1 和 port2,這兩個端口能夠互相發消息,能夠互相監聽,這樣的話,咱們是否是能夠另闢蹊徑來解決這個問題呢,咱們來看下代碼。

JS 代碼

document.getElementById("li1").onclick = function () {
    const  arg1 = 100;
    const  arg2 = 200;
    _postMessage(person, 'nativeMethod').then((val) => {
      // 6.
      console.log(val);
      console.log(arg1 + arg2);
    })
};
    
function _postMessage(val, name){
   var channel = new MessageChannel(); // 建立一個 MessageChannel
   window.nativeCallBack = function(nativeValue) {
     // 3. 
     channel.port1.postMessage(nativeValue) 
   };
   // 1.
   window.webkit.messageHandlers[name].postMessage(val); 
   return new Promise((resolve, reject) => {
     channel.port2.onmessage = function(e){ 
         // 4
         var data = e.data;
         // 5.
         resolve(data); 
         channel = null;
         window.nativeCallBack = null;
     }
   })
}

咱們封裝了一個 _postMessage 方法,在這個方法中咱們,返回了一個 Promise 對象,其實 JS 調用 Native 是一個異步操做,JS 調用客戶端,等待客戶端執行完畢,執行完畢後,告訴 JS,JS 在執行接下來的操做。

OC 代碼

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    
    if ([message.name isEqualToString:@"nativeMethod"]) {
       NSLog(@"body:%@, ", message.body);
       NSDictionary *dict = @{
           @"key1": @"value1",
           @"key2": @"value2"
       }; // 構造回傳 js 數據
       id data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
       NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // 轉爲 json 字符串        
    
       // 2
       [_webView evaluateJavaScript:[NSString stringWithFormat:@"%@(%@)", @"nativeCallBack", jsonString] completionHandler:^(id _Nullable jsData, NSError * _Nullable error) {
           
       }];
       
    }
}

在 OC 代碼中,咱們構造一個 JSON ,而後執行 JS nativeCallBack(jsonString) ,把構造的 JSON 傳給 JS。

注意上邊代碼的註釋,咱們來一步一步看,發生了什麼。

  1. 把值傳給 Native。
  2. Native 接受到以後,調用 JS 的 nativeCallBack 方法。
  3. 接收到 Native 調用以後,channel 的 port1 把 Native 的值轉出去。
  4. channel 的 port2 接收到 port1 發送的值以後,在 prot2 的 onmessage 方法中接收。
  5. 執行 Promise 的 then,並把 data 傳過去。
  6. then 接收到調用,執行裏邊的代碼。

那到底能不能執行呢,咱們運行一下試試

哈哈哈,果真和咱們預料的同樣,我只想說一句,

總結

上邊囉嗦了這麼多,其實很簡單,利用 MessageChannel 端口轉發功能來解決做用域改變的問題,JS 不用傳遞方法給 Native,Native 直接調用一個統一的全局方法就行。交互簡單方便。

做者:XcodeMen 連接:http://www.jianshu.com/p/c9ceb6a824e2 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。

相關文章
相關標籤/搜索