JSBridge深度剖析

概述

做過混合開發的人都知道Ionic和PhoneGap之類的框架,這些框架在web基礎上包裝一層Native,然後通過Bridge技術的js調用本地的庫。

在講JSBridge技術之前,我們來看一下傳統的實現方式。

Android端

Native調JS

native調用js比較簡單,只要遵循:”javascript: 方法名(‘參數,需要轉爲字符串’)”的規則即可。

在4.4之前,調用的方式:


 
  1. // mWebView = new WebView(this);

  2. mWebView.loadUrl("javascript: 方法名('參數,需要轉爲字符串')");

  3.  
  4. //ui線程中運行

  5. runOnUiThread(new Runnable() {

  6. @Override

  7. public void run() {

  8. mWebView.loadUrl("javascript: 方法名('參數,需要轉爲字符串')");

  9. Toast.makeText(Activity名.this, "調用方法...", Toast.LENGTH_SHORT).show();

  10. }

  11. });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

4.4以後(包括4.4),使用以下方式:


 
  1. mWebView.evaluateJavascript("javascript: 方法名('參數,需要轉爲字符串')", new ValueCallback() {

  2. @Override

  3. public void onReceiveValue(String value) {

  4. //這裏的value即爲對應JS方法的返回值

  5. }

  6. });

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

說明:

  • 4.4之前Native通過loadUrl來調用JS方法,只能讓某個JS方法執行,但是無法獲取該方法的返回值
  • 4.4之後,通過evaluateJavascript異步調用JS方法,並且能在onReceiveValue中拿到返回值
  • 不適合傳輸大量數據(大量數據建議用接口方式獲取)
  • mWebView.loadUrl(“javascript: 方法名(‘參數,需要轉爲字符串’)”);函數需在UI線程運行,因爲mWebView爲UI控件

JS調Native

Js調用Native需要對WebView設置@JavascriptInterface註解,這裏有個漏洞,後面會給大家說明。要想js能夠Native,需要對WebView設置以下屬性。


 
  1. WebSettings webSettings = mWebView.getSettings();

  2. //Android容器允許JS腳本

  3. webSettings.setJavaScriptEnabled(true);

  4. //Android容器設置僑連對象

  5. mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");

  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

這裏我們看到了getJSBridge(),Native中通過addJavascriptInterface添加暴露出來的JS橋對象,然後再該對象內部聲明對應的API方法。


 
  1. private Object getJSBridge(){

  2. Object insertObj = new Object(){

  3. @JavascriptInterface

  4. public String foo(){

  5. return "foo";

  6. }

  7.  
  8. @JavascriptInterface

  9. public String foo2(final String param){

  10. return "foo2:" + param;

  11. }

  12.  
  13. };

  14. return insertObj;

  15. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

那麼Html怎麼調用Native的方法呢?


 
  1. //調用方法一

  2. window.JSBridge.foo(); //返回:'foo'

  3. //調用方法二

  4. window.JSBridge.foo2('test');//返回:'foo2:test'

  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

說明:

  • 在Android4.2以上(api17後),暴露的api要加上註解@JavascriptInterface,否則會找不到方法。
  • 在api17以前,addJavascriptInterface有風險,hacker可以通過反編譯獲取Native註冊的Js對象,然後在頁面通過反射Java的內置 靜態類,獲取一些敏感的信息和破壞
  • JS調用Native暴露的api,並且能得到相應返回值

注:說到WebView中接口隱患的問題,這裏大家可以參考WebViw漏洞利用,不過Android發展到現在,這個漏洞基本沒有了。

iOS端

Native調JS

Native調用js的方法比較簡單,Native通過stringByEvaluatingJavaScriptFromString調用Html綁定在window上的函數。不過應注意Oc和Swift的寫法。


 
  1. //Swift

  2. webview.stringByEvaluatingJavaScriptFromString("方法名(參數)")

  3. //OC

  4. [webView stringByEvaluatingJavaScriptFromString:@"方法名(參數);"];

  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

說明:

  • Native調用JS方法時,能拿到JS方法的返回值
  • 不適合傳輸大量數據(大量數據建議用接口方式獲取)

JS調Native

Native中通過引入官方提供的JavaScriptCore庫(iOS7以上),然後可以將api綁定到JSContext上(然後Html中JS默認通過window.top.*可調用)。

引入官方的庫文件

#import <JavaScriptCore/JavaScriptCore.h>
  • 1
  • 1

Native註冊api函數(OC)


 
  1. -(void)webViewDidFinishLoad:(UIWebView *)webView{

  2. [self hideProgress];

  3. [self setJSInterface];

  4. }

  5.  
  6. -(void)setJSInterface{

  7.  
  8. JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

  9.  
  10. // 註冊名爲foo的api方法

  11. context[@"foo"] = ^() {

  12.  
  13. //獲取參數

  14. NSArray *args = [JSContext currentArguments];

  15. NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];

  16. //做一些自己的邏輯

  17. //返回一個值 'foo:'+title

  18. return [NSString stringWithFormat:@"foo:%@", title];

  19. };

  20. }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Html中JS調用Native方法

window.top.foo('test');
  • 1
  • 1

說明:

  • iOS7纔出現這種方式,在這之前,js無法直接調用Native,只能通過JSBridge方式簡介調用
  • JS能調用到已經暴露的api,並且能得到相應返回值
  • iOS原生本身是無法被JS調用的,但是通過引入官方提供的第三方”JavaScriptCore”,即可開放api給JS調用

JSBridge

什麼是JSBridge

JSBridge:聽其取名就是js和Native之前的橋樑,而實際上JSBridge確實是JS和Native之前的一種通信方式。簡單的說,JSBridge就是定義Native和JS的通信,Native只通過一個固定的橋對象調用JS,JS也只通過固定的橋對象調用Native。JSBridge另一個叫法及大家熟知的Hybrid app技術。

這裏寫圖片描述

流程:H5->通過某種方式觸發一個url->Native捕獲到url,進行分析->原生做處理->Native調用H5的JSBridge對象傳遞迴調。

我們前面講過了原生的WebView/UIWebView控件已經能夠和Js實現數據通信了,那爲什麼還要JSBridge呢? 
其實使用JSBridge有很多方面的考慮:

  • Android4.2以下,addJavascriptInterface方式有安全漏掉
  • iOS7以下,JS無法調用Native
  • url scheme交互方式是一套現有的成熟方案,可以完美兼容各種版本,對以前老版本技術的兼容。

url scheme

url scheme是一種類似於url的鏈接,是爲了方便app直接互相調用設計的。具體來講如果是系統的url scheme,則打開系統應用,否則找看是否有app註冊這種scheme,打開對應app。 
注:這種scheme必須原生app註冊後纔會生效。

而在我們實際的開發中,app不會註冊對應的scheme,而是由前端頁面通過某種方式觸發scheme(如用iframe.src),然後Native用某種方法捕獲對應的url觸發事件,然後拿到當前的觸發url,根據定義好的協議,分析當前觸發了那種方法。

JSBridge技術實現

要實現JSBridge,我們需要按以下步驟分析:

  • 第一步:設計出一個Native與JS交互的全局橋對象
  • 第二步:JS如何調用Native
  • 第三步:Native如何得知api被調用
  • 第四步:分析url-參數和回調的格式
  • 第五步:Native如何調用JS
  • 第六步:H5中api方法的註冊以及格式

JSBridge的完整流程可總結爲: 
這裏寫圖片描述

設計Native與JS交互的全局橋對象

我們規定,JS和Native之間的通信必須通過一個H5全局對象JSbridge來實現。該對象有如下特點: 
該對象名爲”JSBridge”,是H5頁面中全局對象window的一個屬性,形如:

var JSBridge = window.JSBridge || (window.JSBridge = {});
  • 1
  • 1

該對象有如下方法:

  • registerHandler( String,Function )H5調用註冊本地JS方法,註冊後Native可通過JSBridge調用。調用後會將方法註冊到本地變量messageHandlers 中。
  • callHandler( String,JSON,Function )H5調用 調用原生開放的api,調用後實際上還是本地通過url scheme觸發。調用時會將回調id存放到本地變量responseCallbacks中
  • _handleMessageFromNative( JSON )Native調用 原生調用H5頁面註冊的方法,或者通知H5頁面執行回調方法

這裏寫圖片描述

JS調用Native

我們定義好了全局橋對象,可以通過它的callHandler方法來調用原生的api。

callHandler函數內部實現過程

在執行callHandler時,內部經歷了以下步驟:

  1. 判斷是否有回調函數,如果有,生成一個回調函數id,並將id和對應回調添加進入回調函數集合responseCallbacks中。
  2. 通過特定的參數轉換方法,將傳入的數據,方法名一起,拼接成一個url scheme

 
  1. //url scheme的格式如

  2. //基本有用信息就是後面的callbackId,handlerName與data

  3. //原生捕獲到這個scheme後會進行分析

  4. var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data

  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4
  1. 使用內部早就創建好的一個隱藏iframe來觸發scheme

 
  1. //創建隱藏iframe過程

  2. var messagingIframe = document.createElement('iframe');

  3. messagingIframe.style.display = 'none';

  4. document.documentElement.appendChild(messagingIframe);

  5.  
  6. //觸發scheme

  7. messagingIframe.src = uri;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注:正常來說是可以通過window.location.href達到發起網絡請求的效果的,但是有一個很嚴重的問題,就是如果我們連續多次修改window.location.href的值,在Native層只能接收到最後一次請求,前面的請求都會被忽略掉。所以JS端發起網絡請求的時候,需要使用iframe,這樣就可以避免這個問題。

Native通知api被調用

上一步,我們已經成功在H5頁面中觸發scheme,那麼Native如何捕獲scheme被觸發呢?

根據系統不同,Android和iOS分別有自己的處理方式。

Android

在Android中(WebViewClient裏),通過shouldoverrideurlloading可以捕獲到url scheme的觸發。


 
  1. public boolean shouldOverrideUrlLoading(WebView view, String url){

  2. //如果返回false,則WebView處理鏈接url,如果返回true,代表WebView根據程序來執行url

  3. return true;

  4. }

  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

iOS

iOS中,UIWebView有個特性:在UIWebView內發起的所有網絡請求,都可以通過delegate函數在Native層得到通知。這樣,我們可以在webview中捕獲url scheme的觸發(原理是利用 shouldStartLoadWithRequest)


 
  1. - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

  2. NSURL *url = [request URL];

  3.  
  4. NSString *requestString = [[request URL] absoluteString];

  5. //獲取利潤url scheme後自行進行處理

  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

分析url-參數和回調的格式

在前面的步驟中,Native已經接收到了JS調用的方法,那麼接下來,原生就應該按照定義好的數據格式來解析數據了,Native接收到Url後,可以按照這種格式將回調參數id、api名、參數提取出來,然後按如下步驟進行。

  1. 根據api名,在本地找尋對應的api方法,並且記錄該方法執行完後的回調函數id
  2. 根據提取出來的參數,根據定義好的參數進行轉化
  3. 原生本地執行對應的api功能方法
  4. 功能執行完畢後,找到這次api調用對應的回調函數id,然後連同需要傳遞的參數信息,組裝成一個JSON格式的參數
  5. 通過JSBridge通知H5頁面回調

Native調用JS

到了這一步,就該Native通過JSBridge調用H5的JS方法或者通知H5進行回調了。其中的messageJSON數據格式根據兩種不同的類型。

JSBridge._handleMessageFromNative(messageJSON);     
  • 1
  • 1

Native通知H5頁面進行回調: 
數據格式爲: Native通知H5回調的JSON格式。 
Native主動調用H5方法: 
Native主動調用H5方法時,數據格式是:{handlerName:api名,data:數據,callbackId:回調id}:

  • handlerName String型 需要調用的,h5中開放的api的名稱
  • data JSON型 需要傳遞的數據,固定爲JSON格式(因爲我們固定H5中註冊的方法接收的第一個參數必須是JSON,第二個是回調函數)
  • callbackId String型 原生生成的回調函數id,h5執行完畢後通過url scheme通知原生api成功執行,並傳遞參數

H5中api方法的註冊以及格式

前面有提到Native主動調用H5中註冊的api方法,那麼h5中怎麼註冊供原生調用的api方法呢?


 
  1. JSBridge.registerHandler('testH5Func',function(data,callback){

  2. alert('測試函數接收到數據:'+JSON.stringify(data));

  3. callback&&callback('測試回傳數據...');

  4. });

  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

如上代碼,其中第一個data即原生傳過來的數據,第二個callback是內部封裝過一次的,執行callback後會觸發url scheme,通知原生獲取回調信息.

完善JSBridge方案

github上有一個開源項目,它裏面的JSBridge做法在iOS上進一步優化了,所以參考他的做法,這裏進一步進行了完善。地址marcuswestin/WebViewJavascriptBridge

JSBridge對象圖解: 
這裏寫圖片描述

JSBridge實現完整流程: 
這裏寫圖片描述

總結

那麼我們在實際的開發中,如何針對Android和iOS的不同情況,統一出一種完整的方案。 
這裏寫圖片描述

另類實現:不採用url scheme方式

前面提到的JSBridge都是基於url scheme的,但其實如果不考慮Android4.2以下,iOS7以下,其實也可以用另一套方案的。

  • Native調用JS的方法不變
  • JS調用Native是不再通過觸發url scheme,而是採用自帶的交互 
    具體來講: 
    Android中,原生通過 addJavascriptInterface開放一個統一的api給JS調用,然後將觸發url scheme步驟變爲調用這個api,其餘步驟不變。

OS中,原生通過JavaScriptCore裏面的方法來註冊一個統一api,其餘和Android中一樣。

 

轉自:https://blog.csdn.net/zuggs_/article/details/80649669

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章