隨手記Android JS與Native交互實踐

歡迎關注微信公衆號「隨手記技術團隊」,查看更多隨手記團隊的技術文章。轉載請註明出處
本文做者:譚海洋
原文連接:mp.weixin.qq.com/s/fKIyFhZC6…javascript

前言

在移動開發中,開發的需求和節奏都愈來愈快,而Native App在這種節奏中略顯笨拙,開發週期長、用戶升級慢、應用市場審覈時間長都深受開發者弊病。而這時候不少開發者都提出了Hybrid App的概念,這種開發模式有着迭代靈活、多端統1、開發週期短、快速上線等優點。可是Hybrid App也有其不足的地方,在性能很難到達Native App的水平,在訪問設備上的硬件時也不是那麼駕輕就熟。對於這些問題,如今已經有較多的解決方案,比較重的框架有Facebook的React Native,輕量級別也有ionic。若是是已經成熟的產品,Web頁面較多遷移比較困難,也可使用VasSonic來提高WebView體驗,而後經過JS調用Native。目前公司項目中因爲歷史緣由採用後者的方式來實現,可是在使用過程當中因爲沒有統一的管理,存在了通信方式多樣、調用混亂和安全性差等幾個問題。下文主要講述如何經過從新設計JS調用框架來解決以上問題。前端

Android WebView JS交互

首先介紹一下WebView中JS和Native相互調用的方式、相互之間的差別。java

Android調用JS

WebView調用JS有如下兩種方式:android

  • 經過WebView.loadUrl()
  • 經過WebView.evaluateJavascript()

在API 19以前是隻能經過WebView.loadUrl()進行調用JavaScript。在API 19的時候新提供了WebView.evaluateJavascript(),它的運行效率會比loadUrl()高,還能夠傳入一個回調對象,方便獲取Web端的回傳信息。web

webView.evaluateJavascript("fromAndroid()", new ValueCallback<String>() { 
 	 @Override
    public void onReceiveValue(String value) {
    		//do something
    }
});
複製代碼

JS調用Android代碼

JS調用Native代碼有如下三種方式:安全

  • 經過WebView.addJavascriptInterface()
  • 經過WebViewClient.shouldOverrideUrlLoading()
  • 經過WebChromeClient.onJsAlert()、onJsConfirm()、onJsPrompt()

WebView.addJavascriptInterface()是官方推薦的作法,在默認狀況下WebView是關閉了JavaScript的調用,須要調用WebSetting.setJavaScriptEnabled(true)來進行開啓。這個方法須要一個Object類型的JavaScript Interface,而後經過@JavascriptInterface來標註提供的JS調用的方法,下面是一個Google官方提供的例子:bash

public class AppJavaScriptProxy {

    private Activity activity = null;

    public AppJavaScriptProxy(Activity activity) {
        this.activity = activity;
    }

    @JavascriptInterface
    public void showMessage(String message) {

        Toast toast = Toast.makeText(this.activity.getApplicationContext(),
                message,
                Toast.LENGTH_SHORT);

        toast.show();
    }
}
複製代碼
webView.addJavascriptInterface(new AppJavaScriptProxy(this),「androidAppProxy」);
複製代碼
// JS代碼調用
if(typeof androidAppProxy !== "undefined"){
    androidAppProxy.showMessage("Message from JavaScript");
} else {
    alert("Running outside Android app");
}
複製代碼

這樣就能夠實現JS調用Android代碼,使用者只須要關注被JS調用方法的實現,對調用的過程是不可知的。使用的時候有幾個要注意的地方:微信

  1. 提供用於JS調用的方法必須爲public類型
  2. 在API 17及以上,提供用於JS調用的方法必需要添加註解@JavascriptInterface
  3. 這個方法不是在主線程中被調用的

WebViewClient.shouldOverrideUrlLoading()是經過攔截Url的方式來實現與JS的交互。shouldOverrideUrlLoading()返回true時,表明攔截此次請求,讓咱們本身處理。shouldOverrideUrlLoading()返回false時,表明不攔截此次請求,讓WebView去處理此次請求。網絡

WebChromeClient.onJsAlert()、onJsConfirm()、onJsPrompt()三種方式和WebViewClient.shouldOverrideUrlLoading()相似,都是經過攔截請求的方式達到交互功能。app

總結:這三種方式實際上能夠概括成兩種:JavascriptInterface和攔截請求,二者之間各有好壞。

  • JavascriptInterface是系統提供的方式,在效率和可靠性上確定是優於後者的,並且之後會一直持續維護和優化。缺點在於擴展性和管理方面不太強,在Android 4.2如下存在漏洞,須要移除掉系統提供的一些接口,並當心處理提供的接口。
  • 攔截請求的方式優勢是便於管理和擴展,能夠按照自身的業務進行設計,方便應對複雜的邏輯,並且能夠在安全性上有所保證。缺點主要是官方對這種方式不提供支持,之後高版本有須要遷移整個邏輯的可能性。還有就是效率不高,H5快速調用多個請求時會有丟失的可能。

JS調用框架設計

爲了解決前言中提到的通信方式多樣、調用混亂和安全性差等幾個問題,須要從新設計JS調用框架,將整個流程從WebView中剝離出來,達到低耦合的目的。綜合考慮後,決定沿用項目中以前的解決方式,經過攔截WebView請求的方式來實現。攔截性的方式在設計框架以前還須要考慮到通信協議的問題。

協議設計

如上圖所示,經過設計通信協議達到多端統一通信。協議上面能夠參考現有的通信協議,或者根據項目需求和前端設計一套通用協議。這裏推薦一種簡單的現有的協議:統一資源標誌符。

jsbridge://method1?a=123&b=345&jsCall=jsMethod1" 複製代碼

該種標識容許用戶對網絡中(通常指萬維網)的資源經過特定的協議進行交互操做,在這裏不用徹底使用,只使用了其中的三個字段。scheme定義爲jsbridge,用於區分別的網絡請求。authority用來定義JS須要訪問的方法。後面的query用來傳參數,若是須要客戶端回調信息給前端,就能夠加個參數jsCall=jsMethod1,而後客戶端處理完後就能夠經過WebView進行回調。

WebView.loadUrl("javascript:jsMethod1(result=1)")
複製代碼

這樣就定義了一種簡單的交互方式,能讓JS和Native擁有基礎的交互能力。若是須要傳文件,能夠經過將文件流轉成Base64而後在通信,固然若是文件太大,這種方式會有內存方面的風險。這裏還有另一種方式,攔截WebView的資源請求,將文件以流的形式進行通信:

webView.setWebViewClient(new WebViewClient(){
	 @Override
     public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
         	return new WebResourceResponse("image/jpeg", "UTF-8", new FileInputStream(new File("xxxx");
     }
}
複製代碼

框架設計

在設計中,主要考慮瞭如下幾點:

  1. 安全性: 防止第三方網站獲取用戶私密信息和通信被第三方截取信息。(域名白名單、數據加密)
  2. 易用性: 設計框架都須要考慮易用性,方便實用。如Android的JavascriptInterface方式,使用者只用關注被調用方法的實現。(參考Android的JavascriptInterface方式)
  3. 可移植性: 如今Android系統突飛猛進,每一個版本都有較大改動和優化,若是出現更好的方案或者特性時,要方便遷移整個JsBridge方案。(設計中要職責分明)
  4. 擴展性: 方便業務邏輯擴展。(添加中間件)

經過分析整個通信的流程,結合項目中的須要,大致抽象出通信流程中的五個角色:

  1. JsBridge: 整個Js框架的管理,提供對外的接口,鏈接Processor和JsProvider。
  2. JsCall: 抽象一次請求,包含一次請求的內容和環境,擁有回調信息給前端的接口。
  3. IProcessor: 協議的抽象體。因爲項目緣由須要多套協議兼容,因此抽象出協議,負責請求的分類和解析。
  4. JsProvider: Js方法的提供者,自己是Object類型,方便現有代碼遷移,並且和JavascriptInterface方式一致,也方便之後遷移。
  5. @JsMethod: Js方法的一個註解,相似@JavascriptInterface。

這樣的模式和系統提供的JavascriptInterface方式基本一致,可是咱們能夠作的事情比JavascriptInterface方式更多,並且整個系統解耦清晰,可是這個結構實際上還缺少較多的東西,沒法達到設計的目標,整個流程中缺少擴展性,沒有攔截和二次處理機制。

能夠在執行JsMethod以前添加一個攔截器,加強擴展性。

安全性方面也能夠經過添加攔截器的方式來實現,將JS請求攔截在執行JsMethod以前,而每一個JsMethod的安全級別能夠經過擴展註解參數來標註。例以下面代碼,添加permission字段來標示方法的安全級別。

@JsMethod(permission = "high")
 public void requestInfo(IJsCall jsCall) {
 		// do something
 }
複製代碼

框架骨架搭建好了後,還須要一些優化性的設計:

  1. 日誌系統:添加日誌開關,打印關鍵性的日誌。
  2. 線程轉換:因爲WebViewClient.shouldOverrideUrlLoading是在主線程裏面執行,能夠參考Android作法,將JS方法都放到其它線程去作,不影響頁面流暢度。
  3. 異常機制:將框架中發生的異常統一管理後,拋出給框架調用者。
  4. ... (結合業務設計)

###實現效果

最後,框架大致設計完畢,實現都是比較簡單的。如今來看看使用的時候,首先是JS發起一個請求:

var iframe = document.createElement('iframe');
  iframe.setAttribute('src', 'jsbridge://method1?a=123&b=345');
  document.body.appendChild(iframe);
  iframe.parentNode.removeChild(iframe);
  iframe = null;
複製代碼

客戶端只須要簡單的對WebView的請求作攔截。

@Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url) {
    	boolean handle = super.shouldOverrideUrlLoading(webView, url);
    	if (!handle) {
			handle = JSBridge.parse(activity, webView, url);
		}
		return handle;
	}
複製代碼

建立一個解析當前協議的對象,這個是之後均可以複用的:

public class JsProcessor implements IProcessor {

    public static final int TYPE_PROCESSOR = 1;

    /**
     * 協議編號
     * @return
     */
    @Override
    public int getType() {
        return TYPE_PROCESSOR;
    }

    /**
     * 判斷請求是否是屬於這個協議
     * @param url
     * @return
     */
    @Override
    public boolean isProtocol(String url) {
        return !TextUtils.isEmpty(url) && url.startsWith("jsbridge");
    }

    /**
     * 解析協議
     * @param context
     * @param webView
     * @param url
     * @param webViewContext WebView的環境
     * @return
     */
    @Override
    public IJsCall parse(Context context, WebView webView, final String url, Object webViewContext) {
        return new IJsCall<RequestBean, ResponseBean>() {

            private String mMethodName;

            @Override
            public void callback(ResponseBean data, WebView webView) {
                JSBridge.callback(data, webView);
            }

            @Override
            public String url() {
                return null;
            }

            @Override
            public RequestBean parseData() {
                if (TextUtils.isEmpty(url)) {
                    return null;
                }
                Uri uri = Uri.parse(url);
                String methodName = uri.getPath();
                methodName = methodName.replace("/", "");
                mMethodName = methodName;
                return new RequsetBean(url);
            }

            @Override
            public String method() {
                return mMethodName;
            }
        };
    }
}
複製代碼

建立一個提供JS方法的對象,在對外提供的方法上加入註解@JsMethod,並標註調用該方法的協議編號、方法名稱和權限級別,方法中所須要的信息都經過IJsCall獲取,處理完成後,經過IJsCall回調信息給JS。

public class JsProvider {
    
    @JsMethod(processorType = JsProcessor.TYPE_PROCESSOR, name = "method1", permission = "high")
    public void method1(IJsCall jsCall) {
        // do anything
        // ...
        // ...
        // ...
        jsCall.callback("xxxx");
    }
}
複製代碼

以上就完成了一次JS和Native的通信。整個通信的細節不對外開放,使用者只用關注方法的開發,方法的信息經過註解來承載,解析註解時能夠經過編譯時生成代碼來提升效率。白名單和數據加密直接經過攔截器來實現。整個系統完美的解決了以前項目中問題,並且也方便之後的業務發展。

總結

Hybrid App是之後的趨勢,JS和Native之間業務邏輯也會愈來愈重,因此項目中這塊的設計也很是重要,須要不斷的根據業務來調整,保證其穩定性的同時,又有很強的擴展能力。

參考連接

www.jianshu.com/p/93cea79a2…

zh.wikipedia.org/zh-hans/%E7…

相關文章
相關標籤/搜索