objCとjsの通信実装--WebViewJavascriptBridge

7162 ワード

シーン
モバイル端末開発において、最も流行している開発モデルはhybmid開発であり、このようなnativeとh 5の混在の下で、いくつかの需要の中で十分な性能を保証することができるだけでなく、いくつかのリストの詳細な需要の下でh 5のスタイル制御を採用してコンテンツを豊富にすることができる.しかし、大規模な製品の開発では、常にフロントエンドの職責はh 5の作成だけでなく、基本的なビジネスロジックの実現も含まれています.例えば、h 5ページで現在のユーザーがいる都市を特定するなど、html 5仕様のGeolocationインタフェースを採用することができますが、nativeのローカルインタフェースを呼び出すのが一般的です.そのため、このような従来のシーンはjsとnative層の通信の問題に関連しており、これはハンドヘルド開発でよく遭遇し、ハンドヘルドは中間層windvane(jsBridge)を提供して通信を完了するが、windvaneの特殊性はオープンソースではないため、本稿ではWebViewJavascriptBridgeフレームワーク(iOS)の通信メカニズムの分析に重点を置いている.
突破口
 iOS下のh 5ページはwebViewビューにロードされ、webViewは比較的特殊なインタフェースを提供するstringByEvaluatingJavaScriptFromStringメソッドであり、js文字列を現在のwebviewで提供されているjsグローバルコンテキストでスクリプトを実行させるため、objCレイヤでstringByEvaluatingJavaScriptFromStringを呼び出し、h 5のjsに関する関数を実行する.戻り値としてjs側が提供する関連呼び出し関数配列を取得し、webview下のコンテキストで関数配列を実行し、最終的にobjC->jsの通信(呼び出し)を完了する.  jsがobjCを呼び出すのは特殊であるが、stringByEvaluatingJavaScriptFromString法を用いて基本通信を実現し、objC層がwebviewDelegateインタフェースに対して提供するwebView:shouldStartLoadWithRequest:navigationType法でjs層をキャプチャする呼び出しを行う.具体的には、非表示のiframeを作成し、srcを既定のschema形式で割り当て、objCレイヤのwebView:shouldStartLoadWithRequest:navigationTypeでschemaが正しいかどうかを判断し、正しい場合は関連スクリプトをロードして実行します.そうしないと実行しません.
ソース分析
objC層はjs層登録関数を呼び出す:
``` - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString)handlerName { NSMutableDictionary message = [NSMutableDictionary dictionary];
  if (data) {
      message[@"data"] = data;
  }

  if (responseCallback) {
      NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
      self.responseCallbacks[callbackId] = [responseCallback copy];
      message[@"callbackId"] = callbackId;
  }

  if (handlerName) {
      message[@"handlerName"] = handlerName;
  }
  [self _queueMessage:message];
}

```
  パラメータdataは、js層関数に渡すパラメータであり、NSString、NSDictionaryなどのオブジェクトであってもよく、responseCallbackはobjC層のコールバックであり、このコールバック関数の実行プロセスは、「jsレイヤ登録関数の実行が完了すると、responseIdのメッセージが返され、最後にobjCレイヤで対応するコールバック(ここのresponseCallback)がフェッチされ、実行されます」と簡単に説明し、handlerNameはjsレイヤ定義の関数名です.  ソースコードの中で_queueMessageメソッドは、h 5ページまたはjsリソースのロードが完了していないときにobjCレイヤwebviewでjs関数が呼び出されると、関連する操作(Messageフォーマットとして格納)がstartupMessageQueueに格納され、関連リソースのロードが完了するのを待つ(すなわち、webviewのwebViewDidFinishLoadライフサイクル関数でstartupMessageQueueに格納されたコマンド配列を実行し、実行が完了し、キューを空にする)jsレイヤ関数を呼び出す.そうでなければstartupMessageQueueが空の場合、js側に露出したwebViewJavascriptBridgeを直接実行します.handleMessageFromObjC関数は、呼び出された関数名と入力パラメータを取得し、objC層sendData:responseCallback:handlerNameで設定されたコールバック関数id--callbackIdを取得し、最終的にjs層登録関数を実行し、最終的にobjC層に「_doSend({responseId:callbackResponseId,responseData:responseData})」形式のメッセージを送信し、objCがメッセージを受信するまで待機する.responseIdを解析し、コールバック関数を実行します.
    //  objC   ,  ref
    function _dispatchMessageFromObjC(messageJSON) {
    setTimeout(function _timeoutDispatchMessageFromObjC() {
        var message = JSON.parse(messageJSON)
        var messageHandler
        var responseCallback

        // js  objC   ,objC    responseId   , js     
    if (message.responseId) {
        responseCallback = responseCallbacks[message.responseId]
        if (!responseCallback) { return; }
        responseCallback(message.responseData)
        delete responseCallbacks[message.responseId]
    } else {
                // objC  js ,     objC   
        if (message.callbackId) {
            var callbackResponseId = message.callbackId

                        //        ,      objC
            responseCallback = function(responseData) {
                _doSend({ responseId:callbackResponseId, responseData:responseData })
            }
        }
                
        var handler = WebViewJavascriptBridge._messageHandler
        if (message.handlerName) {
            handler = messageHandlers[message.handlerName]
        }
                
        try {
            handler(message.data, responseCallback)
        } catch(exception) {
            if (typeof console != 'undefined') {
                console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
            }
        }
       }
    })

 jsがobjC呼び出しをトリガーするのは_doSend関数は、主にiframeにschemaを設定することによって開始されます.
    var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'
    var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__'
    // js  objC,   webview  schema  
    //      ,   objC evaluateJs _fetchQueue()        
    function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime()
        responseCallbacks[callbackId] = responseCallback
        message['callbackId'] = callbackId
    }
    sendMessageQueue.push(message)
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
    }

これによりschemaのフォーマットは「wvjbscheme://WVJB_QUEUE_MESSAGE」と、objCは、webviewのプロキシメソッドでwebView:shouldStartLoadWithRequest:navigationTypeがschema形式をリスニングし、jsのコマンドを実行するか否かを判断する.
jsレイヤobjCレイヤ登録関数の呼び出し
前節で説明したように、webView:shouldStartLoadWithRequest:navigationTypeでschemaフォーマットをリスニングし、jsレイヤの関数呼び出しからメッセージが来たかどうかを判断します.
    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:   (UIWebViewNavigationType)navigationType {
      if (webView != _webView) { return YES; }
      NSURL *url = [request URL];
      __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
      if ([_base isCorrectProcotocolScheme:url]) {
          if ([_base isCorrectHost:url]) {
              NSString *messageQueueString = [self _evaluateJavascript:@"WebViewJavascriptBridge._fetchQueue();"];
              [_base flushMessageQueue:messageQueueString];
          } else {
              [_base logUnkownMessage:url];
          }
          return NO;
      } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
          return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
      } else {
          return YES;
      }
    }

 メッセージがjsレイヤから来た場合、WebviewコンテキストでWebViewJavascriptBridge._を実行します.FetchQueue()関数.
    function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue)
    sendMessageQueue = []
    return messageQueueString
    }

  _FetchQueueは、js側の呼び出しコマンドキューmessageQueue Stringを返し、objC層でflushMessageQueue:messageQueue Stringメソッドを実行し、呼び出しコマンド配列をシーケンス化してobjC層定義の関数を実行します.この呼び出しのプロセスは、前節のobjCがjs層定義の関数を呼び出したのと同様で、objC層定義の関数が実行された後、js層にメッセージをトリガーします.メッセージフォーマットは、前節「jsがobjCに送信するメッセージフォーマットは、{responseId:callbackResponseId,responseData:responseData}」のように、jsレイヤがメッセージを受信するとjsレイヤのコールバック関数を実行します.したがって,objCとjsがどのように通信するかにかかわらず,最も重要なのはstringByEvaluatingJavaScriptFromString法であり,objCとjsの通信の基盤を構築し,「objCは直接この方法でjs関数を呼び出すことができ,jsはobjCにこの方法でjsの呼び出しキューを取得させ,objC層でコマンドを実行させることもできる」.
まとめ
  上記で述べたのは単なる通信メカニズムであり、具体的な実装の詳細には、js側で通信コンポーネントの初期化イベントをどのように受信するか、objCレイヤでjs定義関数をいつ呼び出すべきか、objC送信メッセージに特殊文字をシーケンス化するかなど、多くの注意が必要であるが、通信のメカニズムは、本明細書で少し知ることができる.