Android中JSBridge的原理與實現

首先咱們來了解一下什麼是JSBridge和爲何要使用JSBridge?javascript

在開發中,爲了追求開發的效率以及移植的便利性,一些展現性強的頁面咱們會偏向於使用h5來完成,功能性強的頁面咱們會偏向於使用native來完成,而一旦使用了h5,爲了在h5中儘量的獲得native的體驗,咱們native層須要暴露一些方法給js調用,好比,彈Toast提醒,彈Dialog,分享等等,有時候甚至把h5的網絡請求放到native去完成。前端

JSBridge作得好的一個典型就是微信,微信給開發者提供了JSSDK,該SDK中暴露了不少微信native層的方法,好比支付,定位等。java

本文將對js和Native的通訊原理和實現方法的一些探討。web

實現JSBridge關鍵點的原理剖析

Android中的JSBridge是H5與Native通訊的橋樑,其做用是實現H5與Native間的雙向通訊。要實現H5與Native的雙向通訊,解決以下四個問題便可:面試

一、Java如何調用JavaScriptjson

二、JavaScript如何調用Java安全

三、方法參數以及回調如何處理性能優化

四、通訊協議的制定微信

下面從以上問題依次開始討論網絡

Java如何調用JavaScript

在WebView中,若是java要調用js的方法,是很是容易作到的,使用WebView.loadUrl(「javascript:function()」)便可,這樣,就作到了JSBridge的native層調用h5層的單向通訊

WebView.loadUrl("javascript:function()");

JavaScript如何調用Java

js調用Android的方法有如下四種:

一、WebView 的 andJavascriptInterface

二、WebViewClient.shouldOverrideUrlLoading()

三、WebChromeClient.onConsoleMessage()

四、WebChromeClient.onJsPrompt()、onJsAlert()、onJsConfirm()

咱們先對此四種方案進行一個詳細的描述,最後選擇一個方案便可。本文章中採用了第四種方案。

JavascriptInterface

JavascriptInterface是Android官方提供的js和Native通訊方案。其實現以下:

一、實現一個java類,供js調用

public class MyJavascriptInterface {
    @JavascriptInterface
    public void showToast(String toast) {             
       Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
    }
}

二、在webView中註冊這個類

webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");

三、在js中直接調用這個接口:

function showToast(text){
    window.javascriptInterface.showToast(text);
}

四、總結

大多數人都知道WebView存在一個漏洞,見WebView中接口隱患與手機掛馬利用,雖然該漏洞已經在Android 4.2上修復了(即便用@JavascriptInterface代替addJavascriptInterface),可是因爲兼容性和安全性問題,基本上咱們不會再利用Android系統爲咱們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現,因此咱們只能另闢蹊徑,去尋找既安全,又能實現兼容Android各個版本的方案。

WebViewClient.shouldOverrideUrlLoading()

這個方法是攔截全部webView的跳轉,頁面能夠構造一個特殊格式的Url跳轉,shouldOverrideUrlLoading攔截Url後判斷其格式,而後Native就能執行自身的邏輯了。

public class CustomWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
      if (isJsBridgeUrl(url)) {
        // JSbridge的處理邏輯
        return true;
      }
      return super.shouldOverrideUrlLoading(view, url);
    }
}

WebChromeClient.onConsoleMessage()

在js中執行console.log(), 會進入Android的WebChromeClient.consoleMessage()回調。

public class CustomWebChromeClient extends WebChromeClient {
  @Override
  public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
    super.onConsoleMessage(consoleMessage);
    String msg = consoleMessage.message();//Javascript輸入的Log內容
  }
}

WebChromeClient.onJsPrompt()

一、在WebView有一個方法,叫setWebChromeClient,能夠設置WebChromeClient對象,而這個對象中有三個方法,分別是onJsAlert,onJsConfirm,onJsPrompt,當js調用window對象的對應的方法,即window.alert,window.confirm,window.prompt,WebChromeClient對象中的三個方法對應的就會被觸發,那這三個方法到底要使用哪一個呢?

二、這三個方法的區別,能夠詳見w3c JavaScript 消息框 。

三、通常來講,咱們是不會使用onJsAlert的,爲何呢?由於js中alert使用的頻率仍是很是高的,一旦咱們佔用了這個通道,alert的正常使用就會受到影響,而confirm和prompt的使用頻率相對alert來講,則更低一點。

四、那麼究竟是選擇confirm仍是prompt呢,其實confirm的使用頻率也是不低的,好比你點一個連接下載一個文件,這時候若是須要彈出一個提示進行確認,點擊確認就會下載,點取消便不會下載,相似這種場景仍是不少的,所以不能佔用confirm。

五、而prompt則不同,在Android中,幾乎不會使用到這個方法,就是用,也會進行自定義,因此咱們徹底可使用這個方法。該方法就是彈出一個輸入框,而後讓你輸入,輸入完成後返回輸入框中的內容。所以,佔用prompt是再完美不過了。

public class CustomWebChromeClient extends WebChromeClient {
  @Override
  public boolean onJsPrompt() {
    super.onJsPrompt();
    ...
  }
}

myWebView.setWebChromClient(new CustomWebChromeClient());

方法參數以及回調處理

一、任何IPC通訊都涉及到參數序列化的問題,同理,Java與JavaScript之間只能傳遞基礎類型(包括基本類型和字符串),不包括其餘對象或者函數。因此能夠採用json格式來傳遞數據。

二、爲了實現異步返回結果,因此JavaScript與Java相互調用不能直接獲取返回值,只能經過回調的方式來獲取返回結果。

通訊協議的制定

要進行正常的通訊,通訊協議的制定是必不可少的。咱們回想一下熟悉的http請求url的組成部分。形如http://host:port/path?param=value, 咱們參考http,制定JSBridge的組成部分

jsbridge://className:callbackAddress/methodName?jsonObj

// className: 表示java的類名
// callbackAddress: js回調的標識
// methodName: java中的方法名
// jsonObj: 接口數據

具體實踐

調用流程:

一、在js中,能夠採用以下方法調用java方法

var JSBridge = {
  call: function(className, method, params, callback) {
    var uri = 'jsbridge://' + className + ':' + callback + '/' + method + '?' + params;
    window.prompt(uri, "");
  }
}

// 下面會調用java中的 bridge.showToast方法
JSBridge.call('bridge', 'showToast', {'msg':'Hello JSBridge'}, function(res) {
          alert(JSON.stringify(res))
      });

二、在java中, 能夠以下實現:

// 進入prompt回調
  public class JSBridgeWebChromeClient extends WebChromeClient {      
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {            
      result.confirm(JSBridge.callJava(view, message));
      return true;
    }
  }

  // 調用java邏輯
  public class JSBridge {
    ...
    public static String callJava(WebView webView, String uriString) {
        String methodName = "";
        String className = "";
        String param = "{}";
        String port = "";
        if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
            Uri uri = Uri.parse(uriString);
            className = uri.getHost();
            param = uri.getQuery();
            port = uri.getPort() + "";
            String path = uri.getPath();
            if (!TextUtils.isEmpty(path)) {
                methodName = path.replace("/", "");
            }
        }

        // 基於上面的className、methodName和port path調用對應類的方法
        if (exposedMethods.containsKey(className)) {
            HashMap<String, Method> methodHashMap = exposedMethods.get(className);

            if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
                Method method = methodHashMap.get(methodName);
                if (method != null) {
                    try {
                        method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return null;
    }
}

  // 直接進入showToast函數的實現
  public static void showToast(WebView webView, JSONObject param, final Callback callback) {
    String message = param.optString("msg");
    Toast.makeText(webView.getContext(), message, Toast.LENGTH_SHORT).show();
    if (null != callback) {
        try {
            JSONObject object = new JSONObject();
            object.put("key", "value");
            object.put("key1", "value1");
            callback.apply(getJSONObject(0, "ok", object));
        } catch (Exception e) {
            e.printStackTrace();
        }
     }
  }

  // 上述程序的callback.apply方法實現以下: 即經過webView.loadUrl實現java調用js的方法
  public class Callback {
    private static Handler mHandler = new Handler(Looper.getMainLooper());
    private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
    private String mPort;
    private WeakReference<WebView> mWebViewRef;

    public Callback(WebView view, String port) {
        mWebViewRef = new WeakReference<>(view);
        mPort = port;
    }

    public void apply(JSONObject jsonObject) {
        final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
        if (mWebViewRef != null && mWebViewRef.get() != null) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mWebViewRef.get().loadUrl(execJs);
                }
            });

        }

    }
}

安全性及其它

JSBridge類管理暴露給前端方法,前端調用的方法應該在此類中註冊纔可以使用。register的實現是從Map中查找key是否存在,不存在則反射取得對應class中的全部方法,具體方法是在BridgeImpl中定義的,方法包括三個參數分別爲WebView、JSONObject、CallBack。若是知足條件,則將全部知足條件的方法put到map中。

private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();
public static void register(String exposedName, Class<? extends IBridge> clazz) {
        if (!exposedMethods.containsKey(exposedName)) {
            try {
                exposedMethods.put(exposedName, getAllMethod(clazz));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

JSBridge類中的callJava方法就是將js傳遞過來的URL解析,根據將要調用的類名從剛剛創建的Map中找出,根據方法名調用具體的方法,並將解析出的三個參數傳遞進去。

public static String callJava(WebView webView, String uriString) {
        String methodName = "";
        String className = "";
        String param = "{}";
        String port = "";
        if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
            Uri uri = Uri.parse(uriString);
            className = uri.getHost();
            param = uri.getQuery();
            port = uri.getPort() + "";
            String path = uri.getPath();
            if (!TextUtils.isEmpty(path)) {
                methodName = path.replace("/", "");
            }
        }


        if (exposedMethods.containsKey(className)) {
            HashMap<String, Method> methodHashMap = exposedMethods.get(className);

            if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
                Method method = methodHashMap.get(methodName);
                if (method != null) {
                    try {
                        method.invoke(null, webView, new JSONObject(param), new Callback(webView, port));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return null;
    }

CallBack類是用來回調js中回調方法的Java對應類。Java層處理好的返回結果是經過CallBack類來實現的。在這個回調類中傳遞的參數是JSONObject(返回結果)、WebView和port,port應與js傳遞過來的port相對應。

private static Handler mHandler = new Handler(Looper.getMainLooper());
    private static final String CALLBACK_JS_FORMAT = "javascript:JSBridge.onFinish('%s', %s);";
    private String mPort;
    private WeakReference<WebView> mWebViewRef;

    public Callback(WebView view, String port) {
        mWebViewRef = new WeakReference<>(view);
        mPort = port;
    }
    public void apply(JSONObject jsonObject) {
        final String execJs = String.format(CALLBACK_JS_FORMAT, mPort, String.valueOf(jsonObject));
        if (mWebViewRef != null && mWebViewRef.get() != null) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mWebViewRef.get().loadUrl(execJs);
                }
            });
        }
    }

在java層的JSBridge中註冊方法,例如

JSBridge.register("bridge", BridgeImpl.class);
更多資料分享歡迎Android工程師朋友們加入安卓開發技術進階互助:856328774免費提供安卓開發架構的資料(包括Fultter、高級UI、性能優化、架構師課程、 NDK、Kotlin、混合式開發(ReactNative+Weex)和一線互聯網公司關於Android面試的題目彙總。
相關文章
相關標籤/搜索