隨手記Android JS與Native互動實踐

隨手記技術團隊發表於2018-01-03

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

前言

在移動開發中,開發的需求和節奏都越來越快,而Native App在這種節奏中略顯笨拙,開發週期長、使用者升級慢、應用市場稽核時間長都深受開發者弊病。而這時候很多開發者都提出了Hybrid App的概念,這種開發模式有著迭代靈活、多端統一、開發週期短、快速上線等優勢。但是Hybrid App也有其不足的地方,在效能很難到達Native App的水平,在訪問裝置上的硬體時也不是那麼得心應手。對於這些問題,現在已經有較多的解決方案,比較重的框架有Facebook的React Native,輕量級別也有ionic。如果是已經成熟的產品,Web頁面較多遷移比較困難,也可以使用VasSonic來提升WebView體驗,然後通過JS呼叫Native。目前公司專案中由於歷史原因採用後者的方式來實現,但是在使用過程中由於沒有統一的管理,存在了通訊方式多樣、呼叫混亂和安全性差等幾個問題。下文主要講述如何通過重新設計JS呼叫框架來解決以上問題。

Android WebView JS互動

首先介紹一下WebView中JS和Native相互呼叫的方式、相互之間的差異。

Android呼叫JS

WebView呼叫JS有以下兩種方式:

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

在API 19之前是隻能通過WebView.loadUrl()進行呼叫JavaScript。在API 19的時候新提供了WebView.evaluateJavascript(),它的執行效率會比loadUrl()高,還可以傳入一個回撥物件,方便獲取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官方提供的例子:

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()類似,都是通過攔截請求的方式達到互動功能。

總結:這三種方式實際上可以歸納成兩種:JavascriptInterface和攔截請求,兩者之間各有好壞。

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

JS呼叫框架設計

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

協議設計

隨手記Android JS與Native互動實踐

如上圖所示,通過設計通訊協議達到多端統一通訊。協議上面可以參考現有的通訊協議,或者根據專案需求和前端設計一套通用協議。這裡推薦一種簡單的現有的協議:統一資源標誌符。

隨手記Android JS與Native互動實踐

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。

隨手記Android JS與Native互動實踐

這樣的模式和系統提供的JavascriptInterface方式基本一致,但是我們可以做的事情比JavascriptInterface方式更多,而且整個系統解耦清晰,但是這個結構實際上還缺乏較多的東西,無法達到設計的目標,整個流程中缺乏擴充套件性,沒有攔截和二次處理機制。

可以在執行JsMethod之前新增一個攔截器,增強擴充套件性。

隨手記Android JS與Native互動實踐

安全性方面也可以通過新增攔截器的方式來實現,將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");
    }
}
複製程式碼

隨手記Android JS與Native互動實踐

以上就完成了一次JS和Native的通訊。整個通訊的細節不對外開放,使用者只用關注方法的開發,方法的資訊通過註解來承載,解析註解時可以通過編譯時生成程式碼來提高效率。白名單和資料加密直接通過攔截器來實現。整個系統完美的解決了之前專案中問題,而且也方便以後的業務發展。

總結

Hybrid App是以後的趨勢,JS和Native之間業務邏輯也會越來越重,所以專案中這塊的設計也非常重要,需要不斷的根據業務來調整,保證其穩定性的同時,又有很強的擴充套件能力。

參考連結

www.jianshu.com/p/93cea79a2…

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

相關文章