Android 混合開發 的一些心得。

其實所謂這個混合開發,也就是hybird,就是一些簡單的,html5和native 代碼之間的交互。不少電商之類的app裏面都有相似的功能,javascript

這種東西其實仍是蠻重要的,主要就是你有什麼功能均可以進行熱部署,不須要再從新發版本。下面就簡單介紹一下這種技術。html

 

咱們首先看下面一個場景,咱們打開網易雲音樂的app 裏面的積分商城,(此時其實是一個webview去加載了一個html界面。)html5

而後在顯示出來的界面裏面點擊一下個人訂單,由於咱們沒有登陸過,因此此時自動給我彈出了native的登陸界面。你看這就是一個java

典型的html和native 進行交互的一個場景。爲了讓你們感覺的更深一些,能夠看一下下面的gif 操做過程:android

 

 

通過簡單的抓包,咱們能夠知道 這個webview訪問的地址是:http://music.163.com/store/m/product/indexgit

咱們在chrome瀏覽器裏 直接打開這個連接 而後也點擊個人訂單 你會發現:github

因此我麼繼續查看網頁源代碼,而且對js進行解壓縮之後就會發現下面的代碼了:web

 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了。chrome

因此咱們能夠繼續纔想到,網易雲音樂的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。因此必定程度上能夠避免大部分的攻擊。

相關文章
相關標籤/搜索