Android 混合开发 的一些心得。

其实所谓这个混合开发,也就是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。所以一定程度上可以避免大部分的攻击。

 

posted @ 2015-12-17 11:08  希尔瓦娜斯女神  阅读(16679)  评论(4编辑  收藏  举报