其實所謂這個混合開發,也就是hybird,就是一些簡單的,html5和native 代碼之間的交互。很多電商之類的app里面都有類似的功能,
這種東西其實還是蠻重要的,主要就是你有什么功能都可以進行熱部署,不需要再重新發版本。下面就簡單介紹一下這種技術。
我們首先看下面一個場景,我們打開網易雲音樂的app 里面的積分商城,(此時實際上是一個webview去加載了一個html界面。)
然后在顯示出來的界面里面點擊一下我的訂單,因為我們沒有登錄過,所以此時自動給我彈出了native的登錄界面。你看這就是一個
典型的html和native 進行交互的一個場景。為了讓大家感受的更深一些,可以看一下下面的gif 操作過程:
經過簡單的抓包,我們可以知道 這個webview訪問的地址是:http://music.163.com/store/m/product/index
我們在chrome瀏覽器里 直接打開這個鏈接 然后也點擊我的訂單 你會發現:
所以我么繼續查看網頁源代碼,並且對js進行解壓縮以后就會發現下面的代碼了:
1 Js.fg = function(Jt) { 2 var Jv = JC.cr(Jt, "d:action"); 3 switch (Jq.bv(Jv, "action")) { 4 case "gopage": 5 if (!this.fv.userId || this.fv.userId <= 0) { 6 location.href = "orpheus://welfare/login"; 7 return 8 } else { 9 location.href = Jq.bv(Jv, "destination") 10 } 11 break 12 } 13 };
到這應該可以理解了,就是點擊了我的訂單以后 js的功能把超鏈接定位成orpheus://welfare/login了。
所以我們可以繼續才想到,網易雲音樂的app 就是在這個webview里面 捕捉到了這個超鏈接的信息以后 然后跳轉到
自己定義的activity!這就是這個功能的實現原理。
那么我們就依葫蘆畫瓢來試着仿照一下 能否實現這個功能。我們主要是在webview 上寫一些代碼:
1 wb=(WebView)findViewById(R.id.wb); 2 wb.getSettings().setJavaScriptEnabled(true); 3 wb.setWebViewClient(new WebViewClient() { 4 @Override 5 public boolean shouldOverrideUrlLoading(WebView view, String url) { 6 7 if (url.contains("orpheus://welfare/login")) { 8 Intent intent=new Intent(); 9 intent.setClass(TestNetWebViewActivity.this,LoginActivity.class); 10 startActivity(intent); 11 return true; 12 } 13 return super.shouldOverrideUrlLoading(view, url); 14 } 15 }); 16 wb.loadUrl(URL);
然后看一下 是否能像網易雲音樂那樣實現我們想要的功能:
看下實際運行的gif:
這個方案可以看到是完全可行的。但是這個方案 依舊是有缺陷的,你只能適用於這種簡單的情況,
而且他的原理實際上就是利用webview 重新訪問一個新url的時候 對新的url 進行分析 然后
決定自己下一步該做什么,也就是說這個js---java代碼的調用過程完全依托於對url的字符串的分析。
所謂再復雜一些的場景這個方案就hold不住了!所以我們需要一個新的方案。能讓js 方便愉快的
傳值到我們的java代碼里面!
我們首先在assets這個android路徑下面 放一個我們自己寫的html代碼:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title>JavaScript View</title> 5 6 <script type="text/javascript"> 7 8 function showToast(){ 9 var message = document.getElementById("message").value; 10 var lengthLong = document.getElementById("length").checked; 11 12 /* 13 調用java里的makeToast方法,注意這里的app 就和addJavascriptInterface這個函數里的 14 第二個參數值要保持一致,且大小寫敏感 15 */ 16 app.makeToast(message, lengthLong); 17 return false; 18 } 19 20 /* 21 這個很好理解,就是當你這個html加載完成的時候 把表單的submit提交定位到js的 showToast方法里面 22 就理解成方法的重定向即可 23 */ 24 window.onload = function(){ 25 var form = document.getElementById("form"); 26 form.onsubmit = showToast; 27 } 28 </script> 29 </head> 30 31 <body> 32 33 <form id="form"> 34 Message: <input id="message" name="message" type="text"/><br /> 35 Long: <input id="length" name="length" type="checkbox" /><br /> 36 37 <input type="submit" value="Make Toast" /> 38 </form> 39 40 </body> 41 </html>
然后把我們的java 代碼稍作修改:
1 wb = (WebView) findViewById(R.id.wb); 2 wb.getSettings().setJavaScriptEnabled(true); 3 wb.addJavascriptInterface(new WebViewJavaScriptInterface(this), "app"); 4 wb.loadUrl("file:///android_asset/web.html"); 5 class WebViewJavaScriptInterface { 6 private Context context; 7 8 public WebViewJavaScriptInterface(Context context) { 9 this.context = context; 10 } 11 12 @JavascriptInterface 13 public void makeToast(String message, boolean lengthLong) { 14 Toast.makeText(context, message, (lengthLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT)).show(); 15 } 16 17 }
然后看一下跑起來的效果:
可以看出來我們從js這邊完美調用java代碼的 方案就成功了。
但是實際上呢,這個addJavascriptInterface 方法在4.2 以下呢,是有一個很嚴重的安全漏洞的,
我們上面的代碼 你看到了 我是有一個注解在哪里的,但是如果你的手機是4.2以下的系統,這種系統
是不會檢測你那個方法是否有注解的,所以原則上來說 對於4.2以下的系統來說,這個方法可以調用
任何你手機里的任何方法(當然是通過反射)。有興趣的同學可以看一下這個鏈接:
http://jaq.alibaba.com/blog.htm?id=48
所以除非你做的app 不支持4.2以下的系統,否則我們認為 這個方案也是有缺陷的。
而且這個方法 還有一個不方便的地方在於,你js是可以調用java了可以調用native代碼了,
但是你js調用完java代碼以后 無法回調了。我如果想js調用完java代碼以后馬上進行回調js代碼的操作
就無法做到了。有些人可能不明白 回調js 代碼無法起作用是什么意思,可以接着看下面的例子。
首先我定義一個按鈕,這個按鈕就干一件事 就是通過java代碼去調用js代碼:
1 bt.setOnClickListener(new View.OnClickListener() { 2 3 @Override 4 public void onClick(View v) { 5 wb.loadUrl("javascript:display_alert()"); 6 } 7 });
然后在我們js調用java native函數里面 也寫一個這樣類似的代碼:
1 @JavascriptInterface 2 public void makeToast(String message, boolean lengthLong) { 3 Toast.makeText(context, message, (lengthLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT)).show(); 4 wb.loadUrl("javascript:display_alert()"); 5 6 }
下面看下運行效果:
所以你看 直接在按鈕那邊通過java來調用js是可以的,但是你要是通過js調用java 再在java的代碼里回調js代碼
那就完全無效了。
所以我們下面要解決的問題 主要就是2塊:
第一:讓js能夠安全的調用java代碼,主要是對於4.2版本以下的手機來說
第二:讓js調用java以后 依舊可以回調js,這是對於所有手機來說的。
關於這種情況的解決方案,我也找了很久,調研了很久。基本上都是通過
WebChromeClient.onJsPrompt 來完成對應的功能。
並且流程就是如下幾步:
1.我們假設你js要調用的java native代碼 是a 這個類的 a1 a2 a3 3個方法。
2.利用反射機制 把a1 a2 a3 這3個方法 給保存成字符串,存在一個str里面
3.找機會把這個還有對象方法信息的str 轉成我們需要的js代碼 然后將這個js 代碼注入到webview 要加載的html源碼里面!
4.這樣js就只能執行 注入后的修改過的html代碼里的 ”js代碼了“ 也就是說 你無法利用js 調用任何方法,只能通過前面3步 注入的js代碼 來調用對應的native方法
原理上隔絕了 前面說的4.2以下的 漏洞。
5.js代碼成功注入以后 ,就會通過onpromt方法 來完成jscalljava的這個過程。包括要執行的方法名字,參數類型啥之類的都會檢查一遍。再次杜絕了4.2以下的那個漏洞,
並且從原理上 可以在java中任意時間 場景回調我們的js代碼!
那目前來看 基本上所有的hybrid開發 都是上面這個流程,而且要兼容4.2以下的sdk的時候 基本上我反編譯了很多app 都是利用的http://www.pedant.cn/2014/07/04/webview-js-java-interface-research/
這篇文章提到的https://github.com/pedant/safe-java-js-webview-bridge 這個開源庫。
但是,實際上這個開源庫 並不完美,有一點點小缺陷,而且一直沒有得到很好的解決,(所以很多人轉載文章或者寫blog的時候很不負責任,第一個人怎么寫他自己就怎么抄 也不驗證。)這其中就是因為有一段代碼:
1 public void onProgressChanged(WebView view, int newProgress) { 2 //為什么要在這里注入JS 3 //1 OnPageStarted中注入有可能全局注入不成功,導致頁面腳本上所有接口任何時候都不可用 4 //2 OnPageFinished中注入,雖然最后都會全局注入成功,但是完成時間有可能太晚,當頁面在初始化調用接口函數時會等待時間過長 5 //3 在進度變化時注入,剛好可以在上面兩個問題中得到一個折中處理 6 //為什么是進度大於25%才進行注入,因為從測試看來只有進度大於這個數字頁面才真正得到框架刷新加載,保證100%注入成功 7 if (newProgress <= 25) { 8 mIsInjectedJS = false; 9 } else if (!mIsInjectedJS) { 10 view.loadUrl(mJsCallJava.getPreloadInterfaceJS()); 11 mIsInjectedJS = true; 12 StopWatch.log(" inject js interface completely on progress " + newProgress); 13 } 14 super.onProgressChanged(view, newProgress); 15 }
你可以看一下 這個注入的時機問題。第七行,這個地方是有問題的,因為大家都知道實際上你webview的性能一直以來都不是太好,還有很多機能很差 或者rom 優化很差的 webview
根本就是一團坑,所以這個里面 類似於 硬編碼的 這個注入過程 是不太完美的。在少部分機型 以及少部分場景中,這里會一直注入失敗的。導致整個框架都不可用。
所以有代碼潔癖的同學要注意了,這個網上流傳最廣的開源方案 目前是有缺陷的。要慎用~不過這種開源方案 能cover住百分之95以上的手機 我覺得也還行了。
所以目前來看,並沒有一個特別有效而且安全完美的方案來規避這個問題。有人說微信hybrid 做的不錯,實際上微信我看過他的js sdk。實際上啊,微信並不是用的我們所說的prompt方法
他還是和網易那個一樣 通過攔截url 分析url 來執行相應的操作的。native 回調js代碼也是走的js里的_handleMessageFromWeixin 這份方法。有興趣的同學可以去看下微信的做法。
但你其實想一想 微信這個方法也是有缺陷的,因為url是可以偽造的,好在微信自己會在native代碼里 驗證他的appid。所以一定程度上可以避免大部分的攻擊。