天天看點

jsbridge實作及原理_Android JSBridge的原理與實作

原标題:Android JSBridge的原理與實作

JSBridge

在Android中,JSBridge已經不是什麼新鮮的事物了,各家的實作方式也略有差異。大多數人都知道WebView存在一個漏洞,見WebView中接口隐患與手機挂馬利用,雖然該漏洞已經在Android 4.2上修複了,即使用@JavaInterface代替addJavaInterface,但是由于相容性和安全性問題,基本上我們不會再利用Android系統為我們提供的addJavaInterface方法或者@JavaInterface注解來實作,是以我們隻能另辟蹊徑,去尋找既安全,又能實作相容Android各個版本的方案。

首先我們來了解一下為什麼要使用JSBridge,在開發中,為了追求開發的效率以及移植的便利性,一些展示性強的頁面我們會偏向于使用h5來完成,功能性強的頁面我們會偏向于使用native來完成,而一旦使用了h5,為了在h5中盡可能的得到native的體驗,我們native層需要暴露一些方法給js調用,比如,彈Toast提醒,彈Dialog,分享等等,有時候甚至把h5的網絡請求放着native去完成,而JSBridge做得好的一個典型就是微信,微信給開發者提供了JSSDK,該SDK中暴露了很多微信native層的方法,比如支付,定位等。

那麼,怎麼去實作一個相容Android各版本又具有一定安全性的JSBridge呢?我們知道,在WebView中,如果java要調用js的方法,是非常容易做到的,使用WebView.loadUrl(“java:function()”)即可,這樣,就做到了JSBridge的native層調用h5層的單向通信,但是h5層如何調native層呢,我們需要尋找這麼一個通道,仔細回憶一下,WebView有一個方法,叫setWebChromeClient,可以設定WebChromeClient對象,而這個對象中有三個方法,分别是onJsAlert,onJsConfirm,onJsPrompt,當js調用window對象的對應的方法,即window.alert,window.confirm,window.prompt,WebChromeClient對象中的三個方法對應的就會被觸發,我們是不是可以利用這個機制,自己做一些處理呢?答案是肯定的

至于js這三個方法的差別,可以詳見w3c Java 消息框 。一般來說,我們是不會使用onJsAlert的,為什麼呢?因為js中alert使用的頻率還是非常高的,一旦我們占用了這個通道,alert的正常使用就會受到影響,而confirm和prompt的使用頻率相對alert來說,則更低一點。那麼到底是選擇confirm還是prompt呢,其實confirm的使用頻率也是不低的,比如你點一個連結下載下傳一個檔案,這時候如果需要彈出一個提示進行确認,點選确認就會下載下傳,點取消便不會下載下傳,類似這種場景還是很多的,是以不能占用confirm。而prompt則不一樣,在Android中,幾乎不會使用到這個方法,就是用,也會進行自定義,是以我們完全可以使用這個方法。該方法就是彈出一個輸入框,然後讓你輸入,輸入完成後傳回輸入框中的内容。是以,占用prompt是再完美不過了。

到這一步,我們已經找到了JSBridge雙向通信的一個通道了,接下來就是如何實作的問題了。本文中實作的隻是一個簡單的demo,如果要在生産環境下使用,還需要自己做一層封裝。

要進行正常的通信,通信協定的制定是必不可少的。我們回想一下熟悉的http請求url的組成部分。形如http://host:port/path?param=value,我們參考http,制定JSBridge的組成部分,我們的JSBridge需要傳遞給native什麼資訊,native層才能完成對應的功能,然後将結果傳回呢?顯而易見我們native層要完成某個功能就需要調用某個類的某個方法,我們需要将這個類名和方法名傳遞過去,此外,還需要方法調用所需的參數,為了通信友善,native方法所需的參數我們規定為json對象,我們在js中傳遞這個json對象過去,native層拿到這個對象再進行解析即可。為了差別于http協定,我們的jsbridge使用jsbridge協定,為了簡單起見,問号後面不适用鍵值對,我們直接跟上我們的json字元串,于是就有了形如下面的這個uri。

jsbridge://className:port/methodName?jsonObj

有人會問,這個port用來幹嘛,其實js層調用native層方法後,native需要将執行結果傳回給js層,不過你會覺得通過WebChromeClient對象的onJsPrompt方法将傳回值傳回給js不就好了嗎,其實不然,如果這麼做,那麼這個過程就是同步的,如果native執行異步操作的話,傳回值怎麼傳回呢?這時候port就發揮了它應有的作用,我們在js中調用native方法的時候,在js中注冊一個callback,然後将該callback在指定的位置上緩存起來,然後native層執行完畢對應方法後通過WebView.loadUrl調用js中的方法,回調對應的callback。那麼js怎麼知道調用哪個callback呢?于是我們需要将callback的一個存儲位置傳遞過去,那麼就需要native層調用js中的方法的時候将存儲位置回傳給js,js再調用對應存儲位置上的callback,進行回調。于是,完整的協定定義如下:

jsbridge://className:callbackAddress/methodName?jsonObj

假設我們需要調用native層的Logger類的log方法,當然這個類以及方法肯定是遵循某種規範的,不是所有的java類都可以調用,不然就跟文章開頭的WebView漏洞一樣了,參數是msg,執行完成後js層要有一個回調,那麼位址就如下

jsbridge://Logger:callbackAddress/log?{"msg":"native log"}

至于這個callback對象的位址,可以存儲到js中的window對象中去。至于怎麼存儲,後文會慢慢倒來。

上面是js向native的通信協定,那麼另一方面,native向js的通信協定也需要制定,一個必不可少的元素就是傳回值,這個傳回值和js的參數做法一樣,通過json對象進行傳遞,該json對象中有狀态碼code,提示資訊msg,以及傳回結果result,如果code為非0,則執行過程中發生了錯誤,錯誤資訊在msg中,傳回結果result為null,如果執行成功,傳回的json對象在result中。下面是兩個例子,一個成功調用,一個調用失敗。

{

"code":500,

"msg":"method is not exist",

"result":null

}

{

"code":0,

"msg":"ok",

"result":{

"key1":"returnValue1",

"key2":"returnValue2",

"key3":{

"nestedKey":"nestedValue"

"nestedArray":["value1","value2"]

}

}

}

那麼這個結果如何傳回呢,native調用js暴露的方法即可,然後将js層傳給native層的port一并帶上,進行調用即可,調用的方式就是通過WebView.loadUrl方式來完成,如下。

mWebView.loadUrl("java:JSBridge.onFinish(port,jsonObj);");

關于JsBridge.onFinish方法的實作,後面再叙述。前面我們提到了native層的方法必須遵循某種規範,不然就非常不安全了。在native中,我們需要一個JSBridge統一管理這些暴露給js的類和方法,并且能實時添加,這時候就需要這麼一個方法

JSBridge.register("jsName",javaClass.class)

這個javaClass就是滿足某種規範的類,該類中有滿足規範的方法,我們規定這個類需要實作一個空接口,為什麼呢?主要作用就混淆的時候不會發生錯誤,還有一個作用就是限制JSBridge.register方法第二個參數必須是該接口的實作類。那麼我們定義這個接口

publicinterfaceIBridge{

}

類規定好了,類中的方法我們還需要規定,為了調用友善,我們規定類中的方法必須是static的,這樣直接根據類而不必建立對象進行調用了(還要是public的),然後該方法不具有傳回值,因為傳回值我們在回調中傳回,既然有回調,參數清單就肯定有一個callback,除了callback,當然還有前文提到的js傳來的方法調用所需的參數,是一個json對象,在java層中我們定義成JSONObject對象;方法的執行結果需要通過callback傳遞回去,而java執行js方法需要一個WebView對象,于是,滿足某種規範的方法原型就出來了。

publicstaticvoidmethodName(WebViewwebview,JSONObjectjsonObj,Callbackcallback){

}

js層除了上文說到的JSBridge.onFinish(port,jsonObj);方法用于回調,應該還有一個方法提供調用native方法的功能,該函數的原型如下

JSBridge.call(className,methodName,params,callback)

在call方法中再将參數組合成形如下面這個格式的uri

jsbridge://className:callbackAddress/methodName?jsonObj

然後調用window.prompt方法将uri傳遞過去,這時候java層就會收到這個uri,再進一步解析即可。

首先我們要将js傳來的uri擷取到,編寫一個WebChromeClient子類。

publicclassJSBridgeWebChromeClientextendsWebChromeClient{

@Override

publicbooleanonJsPrompt(WebViewview,Stringurl,Stringmessage,StringdefaultValue,JsPromptResultresult){

result.confirm(JSBridge.callJava(view,message));

returntrue;

}

}

之後不要忘記了将該對象設定給WebView

WebViewmWebView=(WebView)findViewById(R.id.webview);

WebSettingssettings=mWebView.getSettings();

settings.setJavaEnabled(true);

mWebView.setWebChromeClient(newJSBridgeWebChromeClient());

mWebView.loadUrl("file:///android_asset/index.html");

核心的内容來了,就是JSBridgeWebChromeClient中調用的JSBridge類的實作。前文提到該類中有這麼一個方法提供注冊暴露給js的類和方法

JSBridge.register("jsName",javaClass.class)

該方法的實作其實很簡單,從一個Map中查找key是不是存在,不存在則反射拿到對應的Class中的所有方法,将方法是public static void 類型的,并且參數是三個參數,分别是Webview,JSONObject,Callback類型的,如果滿足條件,則将所有滿足條件的方法put進去,整個實作如下

jsbridge實作及原理_Android JSBridge的原理與實作

而至于JSBridge類中的callJava方法,就是将js傳來的uri進行解析,然後根據調用的類名别名從剛剛的map中查找是不是存在,存在的話拿到該類所有方法的methodMap,然後根據方法名從methodMap拿到方法,反射調用,并将參數傳進去,參數就是前文說的滿足條件的三個參數,即WebView,JSONObject,Callback。

jsbridge實作及原理_Android JSBridge的原理與實作

看到該方法中使用了 new Callback(webView, port)進行建立對象,該對象就是用來回調js中回調方法的java對應的類。這個類你需要将js傳來的port傳進來之外,還需要将WebView的引用傳進來,因為要使用到WebView的loadUrl方法,為了防止記憶體洩露,這裡使用弱引用。如果你需要回調js的callback,在對應的方法裡調用一下callback.apply()方法将傳回資料傳入即可,

jsbridge實作及原理_Android JSBridge的原理與實作

唯一需要注意的是apply方法我把它扔在主線程執行了,為什麼呢,因為暴露給js的方法可能會在子線程中調用這個callback,這樣的話就會報錯,是以我在方法内部将其切回主線程。

編碼完成的差不多了,那麼就剩實作IBridge即可了,我們來個簡單的,就來顯示Toast為例好了,顯示完給js回調,雖然這個回調沒有什麼意義。

jsbridge實作及原理_Android JSBridge的原理與實作

你可以往該類中扔你需要的方法,但是必須是public static void且參數清單滿足條件,這樣才能找到該方法。

不要忘記将該類注冊進去

JSBridge.register("bridge",BridgeImpl.class);

進行一下簡單的測試,将之前實作好的JSBridge.js檔案扔到assets目錄下,然後建立index.html,輸入

jsbridge實作及原理_Android JSBridge的原理與實作

很簡單,就是按鈕點選時調用JSBridge.call()方法,回調函數是alert出傳回的結果。

接着就是使用WebView将該index.html檔案load進來測試了

mWebView.loadUrl("file:///android_asset/index.html");

效果如下圖所示

jsbridge實作及原理_Android JSBridge的原理與實作

可以看到整個過程都走通了,然後我們測試下子線程回調,在BridgeImpl中加入測試方法

jsbridge實作及原理_Android JSBridge的原理與實作

在index.html中加入

jsbridge實作及原理_Android JSBridge的原理與實作

理想的效果應該是3秒鐘之後回調彈出alert顯示

很完美,代碼也不多,就實作了功能。如果你需要使用到生成環境中去,上面的代碼你一定要再自己封裝一下,因為我隻是簡單的實作了功能,其他因素并沒有考慮太多。

APP架構師

APP架構師是一個數十萬開發者探讨APP開發架構的公衆号,分享最有價值的幹貨文章,我們探讨Android性能優化,Android記憶體洩露,動态化、插件化等最新的Android開發技術,還有IOS開發架構,進階開發知識,我們的願景是服務每個APP開發者,做一個有逼格的APP架構師!傳回搜狐,檢視更多

責任編輯: