Android 混合開發 的一些心得。

希爾瓦娜斯女神發表於2015-12-17

其實所謂這個混合開發,也就是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。所以一定程度上可以避免大部分的攻擊。

 

相關文章