[Android WebView] 初步探索微雲自己的JSBridge
來自專欄不想做產品的程序員不是好創業者31 人贊了文章
導語 最近在開發的需求中有很多需要與web端進行交互,web需要調用本地的相關代碼、修改本地的ui樣式等等。趁此機會學習了一個js與web的相關知識,針對原有的方案進行小小的優化,初步實現了微雲的JSBridge,還有很多很多地方需要繼續改進。
PS: 尾部有騰訊內部推薦信息,騰訊微雲團隊求賢若渴,歡迎各位大神投簡歷
一、需求背景
在Android客戶端的開發中,我們傾向於將展示頁面用h5開替代,而功能性強的頁面我們會偏 向於使用native來完成,很多時候就需要我們來實現js與java層之間的相互調用。比如在最近做的微雲項目中,有一些需求比如「邀請好友使用微雲」等等,就需要調用到native的一些分享邏輯,實現h5需要的分享功能,還有h5頁面展示時可能需要控制activity的titlebar,控制activity的展示樣式等等,還有很多與web通信後的跳轉,這都需要我們來開發自己的JSBridge來實現需要的功能。
在Android中,JSBridge已經不是什麼新鮮的事物了,它其實是一個很簡單的東西,更多的是一種形式、一種思想,各家的實現方式也略有差異。JSBridge做得好的一個典型就是微信和qq,因為qq和微信中有大量的web頁接入的場景,它們給開發者提供了JSSDK,該SDK中暴露了很多微信native層的方法,比如支付,定位等,以便開發者可以接入自己的功能。微雲雖然不像qq微信一樣有大量開發者接入的場景,但為了以後開發方便還是初步實現的自己的JSBridge。本文將對js和android端通信的原理和方法進行探討,並且Android說明Android端jssdk和JSBridge的初步實現。
二、Android與js的通信原理與方式
Android方式網上很多文章已經說的很詳細了,主要方式都是相同的只不過實現上可能有小的不同,這裡就對通信方式簡單介紹下。
Android中的JSBridge是H5與Native通信的橋樑,其作用是實現H5與Native間的雙向通信。要實現H5與Native的雙向通信,解決如下四個問題即可:
- Java如何調用JavaScript
- JavaScript如何調用Java
- 方法參數以及回調如何處理
- 通信協議的制定
下面從以上問題依次開始討論
1、java調用js
1. 通過webview的loadurl()
WebView.loadUrl("javascript:function()");
function為開始webview載入的html中定義好的函數。
2. 通過webview的evaluateJavascript()方式
// 只需要將第一種方法的loadUrl()換成下面該方法即可{...... mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() { @Override public void onReceiveValue(String value) { //此處為 js 返回的結果 } });}
如果不需要返回值可以使用第一種方式,第一種方式使用起來比較簡潔,也常用做js調用java後的回調。相比第一種方式,第二種方式執行效率更高,同時也可以直接獲取返回值,但是第二種方式的兼容性比較差,只有Android4.4以上的版本才可以使用。在使用時可以將兩種方式混合使用,調用js時進行方法的判斷,如果4.4以下使用loadurl,4.4以上就使用evaluateJavascript的方式。
2、js調用Java
Android webview web頁面js調用native層代碼目前有四種方式:
- WebViewClient.shouldOverrideUrlLoading()
- prompt
- console.log
- addJavascriptInterface
前三種都是通過事先與通過事先與前端構造好的偽協議,並且攔截已經定義好的scheme進行相應的處理目前qq和微信也是使用攔截schema的形式來進行通信的。第四種方式是由webview本身提供的能力,是Android官方提供的js和Native通信方案,其實現如下:
addJavascriptInterface
1.實現一個java類,供js調用
public class MyJavascriptInterface { @JavascriptInterface public void showToast(String toast) { Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show(); }}
2.在webView中註冊這個類:
webView.addJavascriptInterface(new MyJavascriptInterface(), "javascriptInterface");
3.在js中直接調用這個介面:
function showToast(text){ window.javascriptInterface.showToast(text);}
大多數人都知道WebView存在一個漏洞,見WebView中介面隱患與手機掛馬利用,雖然該漏洞已經在Android 4.2上修復了(即使用@JavascriptInterface代替addJavascriptInterface),但是由於兼容性和安全性問題,基本上我們不會再利用Android系統為我們提供的addJavascriptInterface方法或者@JavascriptInterface註解來實現,所以我們只能另闢蹊徑,去尋找既安全,又能實現兼容Android各個版本的方案。
WebViewClient.shouldOverrideUrlLoading()
當網頁中有超鏈接跳轉請求時,將會調用WebViewClient的shouldOverrideUrlLoading方法,可以通過該方法回調的參數獲取當前請求的URL。前端頁面與終端可以規定請求的偽協議,並在此方法中對URL進行攔截,若當前請求協議與約定協議成功匹配,說明由應用的代碼處理該URL,webview本身不處理該請求,達到攔截跳轉的效果。
在使用jsBridge方式實際開發中,一般在頁面通過創建iframe發起偽協議請求,native層攔截該請求並進行處理,一段時間後將iframe銷毀。
js端的處理:
function openURL (url) { 。。。。。。 var iframe = document.createElement(iframe); iframe.style.cssText = display:none;width:0px;height:0px;; var container = document.body || document.documentElement; container.appendChild(iframe); iframe.onload = fail; iframe.src = url; setTimeout(function() { iframe.parentNode.removeChild(iframe); }, 0); 。。。。。。 }
java端的處理:
public class CustomWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { ...... //截取url並操作 return true; } return super.shouldOverrideUrlLoading(view, url); }}
使用prompt
當js調用window對象的對應的方法,即window.alert,window.confirm,window.prompt,WebChromeClient對象中的三個方法對應的就會被觸發但是一般來說,是不會使用onJsAlert的,為什麼呢?因為js中alert使用的頻率還是非常高的,一旦我們佔用了這個通道,alert的正常使用就會受到影響,而confirm和prompt的使用頻率相對alert來說,則更低一點。那麼到底是選擇confirm還是prompt呢,其實confirm的使用頻率也是不低的,比如你點一個鏈接下載一個文件,這時候如果需要彈出一個提示進行確認,點擊確認就會下載,點取消便不會下載,類似這種場景還是很多的,因此不能佔用confirm。而prompt則不一樣,在Android中,幾乎不會使用到這個方法,就是用,也會進行自定義,所以我們完全可以使用這個方法。該方法就是彈出一個輸入框,然後讓你輸入,輸入完成後返回輸入框中的內容。因此,佔用prompt是再完美不過了。
js端的處理:
function invoke(obj, method, params, callback) { ...... console.log(uri); window.prompt(uri, ""); },
java端的處理:
public class JSBridgeWebChromeClient extends WebChromeClient { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { //對message進行處理 return true; }}
console.log
後三種通過攔截url的處理方法其實大同小異,console.log通過設置webview的WebChromeClient重寫onConsoleMessage方法可以攔截console對象的log方法,onConsoleMessage通過參數可以獲取到message參數,該參數可以設為與native層調用的偽協議,從而進行攔截處理。
js端的處理其實就是通過 console.log(uri)進行log輸出,然後在java端進行log的攔截。
java端的處理:
class MyWebChromeClient extends WebChromeClient{ //通過Console方式交互需要重寫onConsoleMessage @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { String message = consoleMessage.message(); //對message進行處理 return super.onConsoleMessage(consoleMessage); }}
3、Java調用js的四種方式的對比
表一、js調用Java的四種方式對比
上述已經說到,addJavascriptInterface方式存在兼容性和安全性的問題,一般情況下是不會進行使用的,剩下三種截取scheme的方法中,對比處理速度console.log方式是最快的,網路上最常用的iframe方式其實是最慢的,眾所周知android碎片化嚴重程度(各種機型&系統定製),難免有些產商不按常理出牌,在有些機型上會出現H5調了console.log或者prompt但終端卻毫不知情的情況,所以這樣的機型可能在某些機型上並不適用(可能這部分機型佔比很小很小),所以幾乎市面上的JSBridge的方式都是通過iframe的形式,也可以兼容ios和android兩端,微雲這裡先使用iframe的形式下發url,以保證功能的實現和穩定性,後續優化速度時可以兼用iframe和console.log,在使用時通過回調的形式判斷console.log是否可以響應,無法響應就用iframe的形式。
三、在微雲中的初步實現
1.通信協議的制定
上述已經找到了JSBridge雙向通信的一個通道了,接下來就是如何實現的問題了。要進行正常的通信,通信協議的制定是必不可少的。我們回想一下熟悉的http請求url的組成部分。形如http://host:port/path?param=value,我們參考http,制定JSBridge的組成部分,我們的JSBridge需要傳遞給native什麼信息,native層才能完成對應的功能,然後將結果返回呢?顯而易見我們native層要完成某個功能就需要調用某個類的某個方法,我們需要將這個類名和方法名傳遞過去,此外,還需要方法調用所需的參數,為了通信方便,native方法所需的參數我們規定為json對象,我們在js中傳遞這個json對象過去,native層拿到這個對象再進行解析即可。如下就是通信的格式:
weiyun://className/methodName?jsonObj#port
比如微雲中要實現設置客戶端title的字體和透明度等屬性:
weiyun://set/transparentTitleBar?bgclr=bgColor&txtclr=titleclr&titleColor&alpha=alpha#sn=1020
set與要調用的類綁定,transparentTitleBar是該類中的方法,bgclr=bgColor&txtclr=titleclr&titleColor&alpha=alpha為相應的參數,接收到後Java層將其解析成j son格式,sn=1020是回調的編號用於回調js方法。
其實WebChromeClient對象的onJsPrompt方法是可以將返回值返回給js的,但是如果這麼做,那麼這個過程就是同步的,如果native執行非同步操作的話,返回值怎麼返回呢?這時候port就發揮了它應有的作用,我們在js中調用native方法的時候,在js中註冊一個callback,然後將該callback在指定的位置上緩存起來,然後native層執行完畢對應方法後通過WebView.loadUrl調用js中的方法,回調對應的callback。那麼js怎麼知道調用哪個callback呢?我們需要將callback的一個存儲位置傳遞過去,就是上面例子的sn=1020,在js中註冊callback時,就需要通過map將port與callback對應存儲,js再調用對應存儲位置上的callback,進行回調。
2.原來實現方案中存在的問題
正常情況下,每個web與native的交互都需要一個不同的url,每次截取url時都需要逐個進行判斷,例如web端可能只有一個功能與Android本地交互,需要一個url,需要在
mWebView.setWebViewClient(new WebViewClient() {@Override public boolean shouldOverrideUrlLoading(WebView view, String url) { //判斷url是否是我們需要截取的if (。。。) {return true; }return false; } ...}
截取時進行判斷是否是我們想要的url,但是當需要越來越多,url越來越多時,需要做的判斷就更多代碼也就非常冗餘,並且也無法保證安全性。
3.問題的優化以及JSBridge的實現
3.1.android端實現思路
前面提到了native層的方法必須遵循某種規範,不然就非常不安全了。在native中,我們需要一個JSBridge類統一管理這些暴露給js的類和方法,並且能實時添加,這時候就需要這麼一個方法
JSBridge.register("ClassName",javaClass.class)
來將上述url中的className與對應的實現類註冊起來,url中的MethodName就是對應類中的方法名,JSBridge類中還需要實現url的各個解析方法,以及調用Java的方法和回調js的方法,當截取並解析出className、MethodName等信息後,在已經註冊的白名單中查找是否有該className對應的實現類已經註冊了,如果有就通過反射的方式調用相應方法,為了調用方便,我們規定類中的方法必須是static的,因為這裡是通過反射調用方法的,這樣就可以直接根據類而不必新建對象進行調用了(還要是public的),然後該方法不具有處理後的返回值,因為返回值我們在回調中返回。
public static boolean callJavaMethod(String className, String methodName, Object... params) { ...... Method method = classNameLists.get(methodName); ...... try { method.invoke(null, params); return true; } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } return false; }
這樣通過反射的形式去調用解析出來的方法,就不需要對每一個url都進行判斷,然後調用滿足條件的url了,如果需要調用相應的方法,只需要事先進行註冊,然後根據url中定義好的類名與方法名進行調用即可。所有實現的類在JSBridge類中統一的註冊,然後由JSBridge類中的callJavaMethod統一找到調用的方法,減少WebViewActivity中不必要的處理,減少Activity與處理過程的耦合,相當於WebViewActivity的一個P層吧。
上述註冊時的這個javaClass就是滿足某種規範的類,該類中有滿足規範的方法,我們規定這個類需要實現一個空介面,主要作用就混淆的時候不會發生錯誤,因為我們是通過解析出來的MothodName去反射調用Java方法的混淆後可能導致找不到相應的類名;還有一個作用就是約束JSBridge.register方法第二個參數必須是該介面的實現類,保證註冊時的安全性。那麼定義這個介面
public interface IBridge{}
並且在混淆文件中加入
-keep public class * implements com.qq.qcloud.share.jsbridge.IBridge{*;}-keepclasseswithmembernames public class * implements com.qq.qcloud.share.jsbridge.IBridge{*;}
當調用完方法完成相應的處理之後,就需要將執行的結果返回給js,這個返回值和js的參數做法一樣,通過json對象進行傳遞,該json對象中有狀態碼code,提示信息msg,以及返回結果result,如果code為非0,則執行過程中發生了錯誤,錯誤信息在msg中,返回結果result為null,如果執行成功,返回的json對象在result中。下面是兩個例子,一個成功調用,一個調用失敗。
{ "code":0, "msg":"success", "result":{ //成功的結果 }}{ "code":1, "msg":"fail", "result":{ //失敗的結果 }}
將結果返回給js層,native調用js暴露的方法即可,然後將js層傳給native層的port一併帶上,進行調用即可,調用的方式就是通過WebView.loadUrl方式來完成,如下。
JSBridge.callJSFunc(webView,"wy.onClientResponse",callbackCode,json.toString());public static void callJSFunc(WebView webView, String func, String... params) { ...... StringBuilder sb = new StringBuilder("javascript:" + func + "("); if (params.length > 0) { for (String param : params) { sb.append(""); sb.append(param); sb.append(""); sb.append(","); } sb.replace(sb.length() - 1, sb.length(), ""); } sb.append(")"); webView.loadUrl(sb.toString()); } ......
3.2 js端的實現思路
我看了下QQ的jssdk的代碼,主要的工作就是針對不同的業務場景,來給客戶端傳遞不同的url,是不過qq的業務相當龐大,有很多開發者需要接入qq來調用qq客戶端的方法,QQ的jssdk將各個調用介面暴露出來提供給其他js文件,js的開發者並不需要知道schema和相應的url是如何定義的,比如調用mqq.ui.setLoading(params),就是來設置ui上的loading態的樣式。
所以為了完成這個迭代的需求,這裡也初步完成了一個jssdk的demo,因為只有當前迭代的需求所以沒有考慮到的因素還有很多,後續會和web以及ios的同學一起來完善,核心的方法就是通過使用 iframe 發起偽協議請求給客戶端:
openURL:function(url){ ...... var iframe = document.createElement(iframe); iframe.style.cssText = display:none;width:0px;height:0px;; var container = document.body || document.documentElement; container.appendChild(iframe); // android 必須先append 然後賦值 iframe.onload = fail; iframe.src = url; setTimeout(function() { iframe.parentNode.removeChild(iframe); }, 0); ...... }, ......
執行之前需要將參數拼接成url:
var Util = { ...... getPort: function () { return Math.floor(Math.random() * (1 << 30)); }, getUri:function(obj, method, params, callback){ var port = Util.getPort(); var callbackPort = ; if(callback) { console.log(port); Inner.callbacks[port] = callback; callbackPort = #sn=+(port); } params = this.param(params); var uri = JSBRIDGE_PROTOCOL + :// + obj + / + method + ? + params + callbackPort; return uri; }, ...... getParam:function(obj, separator = &, encode = false) { if(obj == null) return ; var params = []; for(let o in obj) { params.push(o + = + (encode ? encodeURIComponent(obj[o]) : obj[o])); } return params.join(separator); }, ...... };
在生成port時,將callback與對應的port存入Inner.callbacks中,所以當js收到客戶端傳來的回調參數時,通過port從緩存中取出並執行callback方法。
只需要通過exports將需要調用的介面暴露給開發者,當調用一個jssdk提供的方法時,會將參數封裝成相應的url協議傳遞到客戶端。舉個例子,我們自己在Android Studio的assert文件夾中新建一個test.html文件,在點擊按鈕時執行arouseShareSheet方法
/** * 呼起分享鏈接的dialog,參數為空 * params * { * } */ arouseShareSheet : function(callback){ Inner.invoke(action,arouseShareSheet,{},callback) },
invoke方法,將輸入的參數拼接成該功能要下發的協議,並且執行了openUrl,通過iframe將url發送給客戶端,客戶端解析相應的url,在已經註冊的名單中找到與「action」對應的實現類,實現類需要統一繼承IBridge介面,該類通過反射執行類中的arouseShareSheet方法,該方法的作用就是呼氣Android端的一個分享文件的dialog,分享走的就是客戶端的邏輯了,執行完後loadurl的方法將port和參數回調給js端,從而執行arouseShareSheet()方法傳入的callback。
public class ShareFragmentForWebView extends BaseDialogFragment implements IBridge { ...... public static ShareFragmentForWebView arouseShareSheet (JSONObject jsonObject, String callbackCode, WebView webView,BaseWebViewActivity activity) { ...... } ......}
下圖呼起分享dialog
四、後續還可以優化的地方
1.執行速度上的優化
上述js調用java時也已經說明了,現在jssdk中是通過iframe的形式來傳遞url,但是這種方法相比console.log的方式速度上是比較慢的,因為創建iframe的請求,需要一定的耗時,而console.log默認行為是向web控制台輸出一條消息,相當於Android端的log,但是console.log有可能在某些邊緣機型上不適用,所以後續的優化可以將兩種方式結合使用。
後續可以在載入頁面時進行默認進行console.log的調用已經定義好的url,客戶端接收到特定的url後進 行相應的回調,可以聯通就用console.log的方式,就相當於在頁面載入之後進行一次「ping」操作。
2.jssdk功能上還需完善
本需求中只完成了調用本地的dialog,改變titlebar的樣式,設置分享的信息等等,相當於針對Android端方式的調用,後續還要和web端的同學一起完善,需要兼容ios的調用和相關的配置、針對Android和ios不同版本的不同支持、更多調用客戶端功能的封裝、以及安全性考慮等等因素。
3.需要完善定製WebViewActivity樣式的功能
當調用WebView.loadurl()載入頁面時,載入頁面的url上可以帶有相應的參數來設置WebViewActivity的樣式,比如QQ中載入一個url:http://YOUR_HOST/PATH?...&_wv=N...
_wv=n就是用來設置ui樣式的參數。當然也可以通過上述的JSBridge的形式截取scheme來更改樣式,但是一般來說,都是在頁面載入完成後,才能執行iframe或者console.log,客戶端收到請求後再來改變樣式,這就會導致頁面載入完成後,再改變了樣式,這樣視覺的體驗比較差。所以如果再傳入url的時候,解析url的參數來改變樣式,同時非同步載入頁面,這樣體驗就會好很多。
PPS: 騰訊微雲團隊求賢若渴
PPS: 騰訊微雲團隊求賢若渴
PPS: 騰訊微雲團隊求賢若渴
因業務的不斷發展,騰訊微雲團隊招聘終端開發工程師,與我們一起服務最廣大的用戶,探索最前沿的技術,完成最有挑戰的工作,招聘需求如下:
騰訊微雲Android開發工程師(深圳)
工作職責:
負責騰訊微雲Android終端應用的架構設計;
負責騰訊微雲Android版本現有功能的維護及新功能模塊的開發;
負責定位並解決現有模塊存在的問題;
負責承擔版本的系統設計、性能及內存調優工作
崗位要求:
本科以上學歷,計算機或相關專業;
2年以上手機客戶端開發經驗;
熟悉Android客戶端應用開發技術,有成熟產品者優先;
具備良好的分析解決問題能力,能獨立承擔任務和有系統進度把控能力;
具備良好的責任心與工作激情,團隊合作能力強。
感興趣的朋友可將簡歷投遞至:tmacchen@tencent.com
工作地點:
深圳
推薦閱讀:
※Android編譯打包燒錄
※Android使用ViewPager實現左右循環滑動及輪播效果
※讓你相見恨晚的安卓軟體們?