Android JS與Native交互實踐

前言

在移動開發(fā)中访雪,開發(fā)的需求和節(jié)奏都越來越快隘庄,而Native App在這種節(jié)奏中略顯笨拙,開發(fā)周期長绞佩、用戶升級慢寺鸥、應(yīng)用市場審核時間長都深受開發(fā)者弊病。而這時候很多開發(fā)者都提出了Hybrid App的概念品山,這種開發(fā)模式有著迭代靈活胆建、多端統(tǒng)一、開發(fā)周期短肘交、快速上線等優(yōu)勢笆载。但是Hybrid App也有其不足的地方,在性能很難到達Native App的水平涯呻,在訪問設(shè)備上的硬件時也不是那么得心應(yīng)手凉驻。對于這些問題,現(xiàn)在已經(jīng)有較多的解決方案复罐,比較重的框架有Facebook的React Native涝登,輕量級別也有ionic。如果是已經(jīng)成熟的產(chǎn)品效诅,Web頁面較多遷移比較困難胀滚,也可以使用VasSonic來提升WebView體驗趟济,然后通過JS調(diào)用Native。目前公司項目中由于歷史原因采用后者的方式來實現(xiàn)咽笼,但是在使用過程中由于沒有統(tǒng)一的管理顷编,存在了通訊方式多樣、調(diào)用混亂和安全性差等幾個問題剑刑。下文主要講述如何通過重新設(shè)計JS調(diào)用框架來解決以上問題媳纬。

Android WebView JS交互

首先介紹一下WebView中JS和Native相互調(diào)用的方式、相互之間的差異施掏。

Android調(diào)用JS

WebView調(diào)用JS有以下兩種方式:

  • 通過WebView.loadUrl()

  • 通過WebView.evaluateJavascript()

在API 19之前是只能通過WebView.loadUrl()進行調(diào)用JavaScript钮惠。在API 19的時候新提供了WebView.evaluateJavascript(),它的運行效率會比loadUrl()高其监,還可以傳入一個回調(diào)對象萌腿,方便獲取Web端的回傳信息。


webView.evaluateJavascript("fromAndroid()", new ValueCallback<String>() {

@Override

    public void onReceiveValue(String value) {

    //do something

    }

});

JS調(diào)用Android代碼

JS調(diào)用Native代碼有以下三種方式:

  • 通過WebView.addJavascriptInterface()

  • 通過WebViewClient.shouldOverrideUrlLoading()

  • 通過WebChromeClient.onJsAlert()抖苦、onJsConfirm()毁菱、onJsPrompt()

WebView.addJavascriptInterface()是官方推薦的做法,在默認情況下WebView是關(guān)閉了JavaScript的調(diào)用锌历,需要調(diào)用WebSetting.setJavaScriptEnabled(true)來進行開啟贮庞。這個方法需要一個Object類型的JavaScript Interface,然后通過@JavascriptInterface來標注提供的JS調(diào)用的方法究西,下面是一個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代碼調(diào)用

if(typeof androidAppProxy !== "undefined"){

    androidAppProxy.showMessage("Message from JavaScript");

} else {

    alert("Running outside Android app");

}

這樣就可以實現(xiàn)JS調(diào)用Android代碼,使用者只需要關(guān)注被JS調(diào)用方法的實現(xiàn)卤材,對調(diào)用的過程是不可知的遮斥。使用的時候有幾個要注意的地方:

  1. 提供用于JS調(diào)用的方法必須為public類型

  2. 在API 17及以上,提供用于JS調(diào)用的方法必須要添加注解@JavascriptInterface

  3. 這個方法不是在主線程中被調(diào)用的

WebViewClient.shouldOverrideUrlLoading()是通過攔截Url的方式來實現(xiàn)與JS的交互扇丛。shouldOverrideUrlLoading()返回true時术吗,代表攔截這次請求,讓我們自己處理帆精。shouldOverrideUrlLoading()返回false時较屿,代表不攔截這次請求,讓WebView去處理這次請求卓练。

WebChromeClient.onJsAlert()隘蝎、onJsConfirm()、onJsPrompt()三種方式和WebViewClient.shouldOverrideUrlLoading()類似襟企,都是通過攔截請求的方式達到交互功能嘱么。

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

  • JavascriptInterface是系統(tǒng)提供的方式拱撵,在效率和可靠性上肯定是優(yōu)于后者的辉川,而且以后會一直持續(xù)維護和優(yōu)化。缺點在于擴展性和管理方面不太強拴测,在Android 4.2以下存在漏洞,需要移除掉系統(tǒng)提供的一些接口府蛇,并小心處理提供的接口集索。

  • 攔截請求的方式優(yōu)點是便于管理和擴展,可以按照自身的業(yè)務(wù)進行設(shè)計汇跨,方便應(yīng)對復(fù)雜的邏輯务荆,而且可以在安全性上有所保證。缺點主要是官方對這種方式不提供支持穷遂,以后高版本有需要遷移整個邏輯的可能性函匕。還有就是效率不高,H5快速調(diào)用多個請求時會有丟失的可能蚪黑。

JS調(diào)用框架設(shè)計

為了解決前言中提到的通訊方式多樣盅惜、調(diào)用混亂和安全性差等幾個問題,需要重新設(shè)計JS調(diào)用框架忌穿,將整個流程從WebView中剝離出來抒寂,達到低耦合的目的。綜合考慮后掠剑,決定沿用項目中之前的解決方式屈芜,通過攔截WebView請求的方式來實現(xiàn)。攔截性的方式在設(shè)計框架之前還需要考慮到通訊協(xié)議的問題朴译。

協(xié)議設(shè)計

png1.png

如上圖所示井佑,通過設(shè)計通訊協(xié)議達到多端統(tǒng)一通訊。協(xié)議上面可以參考現(xiàn)有的通訊協(xié)議眠寿,或者根據(jù)項目需求和前端設(shè)計一套通用協(xié)議躬翁。這里推薦一種簡單的現(xiàn)有的協(xié)議:統(tǒng)一資源標志符。

png2.png

jsbridge://method1?a=123&b=345&jsCall=jsMethod1"

該種標識允許用戶對網(wǎng)絡(luò)中(一般指萬維網(wǎng))的資源通過特定的協(xié)議進行交互操作澜公,在這里不用完全使用姆另,只使用了其中的三個字段。scheme定義為jsbridge坟乾,用于區(qū)分別的網(wǎng)絡(luò)請求迹辐。authority用來定義JS需要訪問的方法。后面的query用來傳參數(shù)甚侣,如果需要客戶端回調(diào)信息給前端明吩,就可以加個參數(shù)jsCall=jsMethod1,然后客戶端處理完后就可以通過WebView進行回調(diào)殷费。


WebView.loadUrl("javascript:jsMethod1(result=1)")

這樣就定義了一種簡單的交互方式印荔,能讓JS和Native擁有基礎(chǔ)的交互能力低葫。如果需要傳文件,可以通過將文件流轉(zhuǎn)成Base64然后在通訊仍律,當然如果文件太大嘿悬,這種方式會有內(nèi)存方面的風險。這里還有另外一種方式水泉,攔截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");

    }

}

框架設(shè)計

在設(shè)計中,主要考慮了以下幾點:

  1. 安全性: 防止第三方網(wǎng)站獲取用戶私密信息和通訊被第三方截取信息草则。(域名白名單钢拧、數(shù)據(jù)加密)

  2. 易用性: 設(shè)計框架都需要考慮易用性,方便實用炕横。如Android的JavascriptInterface方式源内,使用者只用關(guān)注被調(diào)用方法的實現(xiàn)。(參考Android的JavascriptInterface方式)

  3. 可移植性: 現(xiàn)在Android系統(tǒng)日新月異份殿,每個版本都有較大改動和優(yōu)化膜钓,如果出現(xiàn)更好的方案或者特性時,要方便遷移整個JsBridge方案伯铣。(設(shè)計中要職責分明)

  4. 擴展性: 方便業(yè)務(wù)邏輯擴展呻此。(添加中間件)

通過分析整個通訊的流程,結(jié)合項目中的需要腔寡,大體抽象出通訊流程中的五個角色:

  1. JsBridge: 整個Js框架的管理焚鲜,提供對外的接口,連接Processor和JsProvider放前。

  2. JsCall: 抽象一次請求忿磅,包含一次請求的內(nèi)容和環(huán)境,擁有回調(diào)信息給前端的接口凭语。

  3. IProcessor: 協(xié)議的抽象體葱她。由于項目原因需要多套協(xié)議兼容,所以抽象出協(xié)議似扔,負責請求的分類和解析吨些。

  4. JsProvider: Js方法的提供者,本身是Object類型炒辉,方便現(xiàn)有代碼遷移豪墅,而且和JavascriptInterface方式一致,也方便以后遷移黔寇。

  5. @JsMethod: Js方法的一個注解偶器,類似@JavascriptInterface。

png3.png

這樣的模式和系統(tǒng)提供的JavascriptInterface方式基本一致,但是我們可以做的事情比JavascriptInterface方式更多屏轰,而且整個系統(tǒng)解耦清晰颊郎,但是這個結(jié)構(gòu)實際上還缺乏較多的東西,無法達到設(shè)計的目標霎苗,整個流程中缺乏擴展性姆吭,沒有攔截和二次處理機制。

可以在執(zhí)行JsMethod之前添加一個攔截器叨粘,增強擴展性猾编。

png4.png

安全性方面也可以通過添加攔截器的方式來實現(xiàn),將JS請求攔截在執(zhí)行JsMethod之前升敲,而每個JsMethod的安全級別可以通過擴展注解參數(shù)來標注。例如下面代碼轰传,添加permission字段來標示方法的安全級別驴党。


@JsMethod(permission = "high")

public void requestInfo(IJsCall jsCall) {

// do something

}

框架骨架搭建好了后,還需要一些優(yōu)化性的設(shè)計:

  1. 日志系統(tǒng):添加日志開關(guān)获茬,打印關(guān)鍵性的日志港庄。

  2. 線程轉(zhuǎn)換:由于WebViewClient.shouldOverrideUrlLoading是在主線程里面執(zhí)行,可以參考Android做法恕曲,將JS方法都放到其它線程去做鹏氧,不影響頁面流暢度。

  3. 異常機制:將框架中發(fā)生的異常統(tǒng)一管理后佩谣,拋出給框架調(diào)用者把还。

  4. ... (結(jié)合業(yè)務(wù)設(shè)計)

實現(xiàn)效果

最后,框架大體設(shè)計完畢茸俭,實現(xiàn)都是比較簡單的〉趼模現(xiàn)在來看看使用的時候懒鉴,首先是JS發(fā)起一個請求:


  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;

}

創(chuàng)建一個解析當前協(xié)議的對象,這個是以后都可以復(fù)用的:


public class JsProcessor implements IProcessor {

    public static final int TYPE_PROCESSOR = 1;

    /**

    * 協(xié)議編號

    * @return

    */

    @Override

    public int getType() {

        return TYPE_PROCESSOR;

    }

    /**

    * 判斷請求是不是屬于這個協(xié)議

    * @param url

    * @return

    */

    @Override

    public boolean isProtocol(String url) {

        return !TextUtils.isEmpty(url) && url.startsWith("jsbridge");

    }

    /**

    * 解析協(xié)議

    * @param context

    * @param webView

    * @param url

    * @param webViewContext WebView的環(huán)境

    * @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;

            }

        };

    }

}

創(chuàng)建一個提供JS方法的對象漫萄,在對外提供的方法上加入注解@JsMethod腾窝,并標注調(diào)用該方法的協(xié)議編號缀踪、方法名稱和權(quán)限級別,方法中所需要的信息都通過IJsCall獲取虹脯,處理完成后驴娃,通過IJsCall回調(diào)信息給JS。


public class JsProvider {



    @JsMethod(processorType = JsProcessor.TYPE_PROCESSOR, name = "method1", permission = "high")

    public void method1(IJsCall jsCall) {

        // do anything

        // ...

        // ...

        // ...

        jsCall.callback("xxxx");

    }

}

png5.png

以上就完成了一次JS和Native的通訊归形。整個通訊的細節(jié)不對外開放托慨,使用者只用關(guān)注方法的開發(fā),方法的信息通過注解來承載暇榴,解析注解時可以通過編譯時生成代碼來提高效率厚棵。白名單和數(shù)據(jù)加密直接通過攔截器來實現(xiàn)蕉世。整個系統(tǒng)完美的解決了之前項目中問題,而且也方便以后的業(yè)務(wù)發(fā)展婆硬。

總結(jié)

Hybrid App是以后的趨勢狠轻,JS和Native之間業(yè)務(wù)邏輯也會越來越重,所以項目中這塊的設(shè)計也非常重要彬犯,需要不斷的根據(jù)業(yè)務(wù)來調(diào)整向楼,保證其穩(wěn)定性的同時,又有很強的擴展能力谐区。

參考鏈接

http://www.reibang.com/p/93cea79a2443

https://zh.wikipedia.org/zh-hans/%E7%BB%9F%E4%B8%80%E8%B5%84%E6%BA%90%E6%A0%87%E5%BF%97%E7%AC%A6

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末湖蜕,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宋列,更是在濱河造成了極大的恐慌昭抒,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件炼杖,死亡現(xiàn)場離奇詭異灭返,居然都是意外死亡,警方通過查閱死者的電腦和手機坤邪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門熙含,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人艇纺,你說我怎么就攤上這事怎静。” “怎么了喂饥?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵消约,是天一觀的道長。 經(jīng)常有香客問我员帮,道長或粮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任捞高,我火速辦了婚禮氯材,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘硝岗。我一直安慰自己氢哮,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布型檀。 她就那樣靜靜地躺著冗尤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裂七,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天皆看,我揣著相機與錄音,去河邊找鬼背零。 笑死腰吟,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的徙瓶。 我是一名探鬼主播毛雇,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼侦镇!你這毒婦竟也來了灵疮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤壳繁,失蹤者是張志新(化名)和其女友劉穎始藕,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體氮趋,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年江耀,在試婚紗的時候發(fā)現(xiàn)自己被綠了剩胁。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡祥国,死狀恐怖昵观,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情舌稀,我是刑警寧澤啊犬,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站壁查,受9級特大地震影響觉至,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜睡腿,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一语御、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧席怪,春花似錦应闯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春骨田,著一層夾襖步出監(jiān)牢的瞬間耿导,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工盛撑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碎节,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓抵卫,卻偏偏與公主長得像狮荔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子介粘,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容