Android混合开发的⼀些⼼得。
其实所谓这个混合开发,也就是hybird,就是⼀些简单的,html5和native 代码之间的交互。很多电商之类的app⾥⾯都有类似的功能,这种东西其实还是蛮重要的,主要就是你有什么功能都可以进⾏热部署,不需要再重新发版本。下⾯就简单介绍⼀下这种技术。
我们⾸先看下⾯⼀个场景,我们打开⽹易云⾳乐的app ⾥⾯的积分商城,(此时实际上是⼀个webview去加载了⼀个html界⾯。)
然后在显⽰出来的界⾯⾥⾯点击⼀下我的订单,因为我们没有登录过,所以此时⾃动给我弹出了native的登录界⾯。你看这就是⼀个典型的html和native 进⾏交互的⼀个场景。为了让⼤家感受的更深⼀些,可以看⼀下下⾯的gif 操作过程:
我们在chrome浏览器⾥直接打开这个链接然后也点击我的订单你会发现:
所以我么继续查看⽹页源代码,并且对js进⾏解压缩以后就会发现下⾯的代码了:
1 Js.fg = function(Jt) {
2var Jv = JC.cr(Jt, "d:action");
3switch (Jq.bv(Jv, "action")) {
4case "gopage":
5if (!this.fv.userId || this.fv.userId <= 0) {
6                location.href = "orpheus://welfare/login";
7return
8            } else {
9                location.href = Jq.bv(Jv, "destination")
10            }
11break
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
5public boolean shouldOverrideUrlLoading(WebView view, String url) {
6
7if (ains("orpheus://welfare/login")) {
8                    Intent intent=new Intent();
9                    intent.setClass(TestNetWebViewActivity.this,LoginActivity.class);
10                    startActivity(intent);
11return true;
12                }
13return 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
8function showToast(){
9var message = ElementById("message").value;
10var lengthLong = ElementById("length").checked;
11
12/*
13调⽤java⾥的makeToast⽅法,注意这⾥的app 就和addJavascriptInterface这个函数⾥的
14第⼆个参数值要保持⼀致,且⼤⼩写敏感
15*/
16            app.makeToast(message, lengthLong);
17return false;
18        }
19
20/*
21这个很好理解,就是当你这个html加载完成的时候把表单的submit提交定位到js的 showToast⽅法⾥⾯
22就理解成⽅法的重定向即可
23*/
24        load = function(){
25var form = ElementById("form");
26            submit = 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");
5class WebViewJavaScriptInterface {
6private Context context;
7
8public WebViewJavaScriptInterface(Context context) {
10        }
11
12        @JavascriptInterface
13public 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以下的系统来说,这个⽅法可以调⽤
任何你⼿机⾥的任何⽅法(当然是通过反射)。有兴趣的同学可以看⼀下这个链接:
所以除⾮你做的app 不⽀持4.2以下的系统,否则我们认为这个⽅案也是有缺陷的。
⽽且这个⽅法还有⼀个不⽅便的地⽅在于,你js是可以调⽤java了可以调⽤native代码了,
但是你js调⽤完java代码以后⽆法回调了。我如果想js调⽤完java代码以后马上进⾏回调js代码的操作
就⽆法做到了。有些⼈可能不明⽩回调js 代码⽆法起作⽤是什么意思,可以接着看下⾯的例⼦。
⾸先我定义⼀个按钮,这个按钮就⼲⼀件事就是通过java代码去调⽤js代码:
1  bt.setOnClickListener(new View.OnClickListener() {
2
3            @Override
4public void onClick(View v) {
5                wb.loadUrl("javascript:display_alert()");
6            }
7        });
然后在我们js调⽤java native函数⾥⾯也写⼀个这样类似的代码:
1  @JavascriptInterface
2public 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
html5标签区分大小写6        }
下⾯看下运⾏效果:
所以你看直接在按钮那边通过java来调⽤js是可以的,但是你要是通过js调⽤java 再在java的代码⾥回调js代码
那就完全⽆效了。
所以我们下⾯要解决的问题主要就是2块:
第⼀:让js能够安全的调⽤java代码,主要是对于4.2版本以下的⼿机来说
第⼆:让js调⽤java以后依旧可以回调js,这是对于所有⼿机来说的。
关于这种情况的解决⽅案,我也了很久,调研了很久。基本上都是通过
并且流程就是如下⼏步:
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代码!
但是,实际上这个开源库并不完美,有⼀点点⼩缺陷,⽽且⼀直没有得到很好的解决,(所以很多⼈转载⽂章或者写blog的时候很不负责任,第⼀个⼈怎么写他⾃⼰就怎么抄也不验证。)这其中就是因为有⼀段代码:
1public void onProgressChanged(WebView view, int newProgress) {
2//为什么要在这⾥注⼊JS
3//1 OnPageStarted中注⼊有可能全局注⼊不成功,导致页⾯脚本上所有接⼝任何时候都不可⽤
4//2 OnPageFinished中注⼊,虽然最后都会全局注⼊成功,但是完成时间有可能太晚,当页⾯在初始化调⽤接⼝函数时会等待时间过长
5//3 在进度变化时注⼊,刚好可以在上⾯两个问题中得到⼀个折中处理
6//为什么是进度⼤于25%才进⾏注⼊,因为从测试看来只有进度⼤于这个数字页⾯才真正得到框架刷新加载,保证100%注⼊成功
7if (newProgress <= 25) {
8            mIsInjectedJS = false;
9        } else if (!mIsInjectedJS) {
10            view.PreloadInterfaceJS());
11            mIsInjectedJS = true;
12            StopWatch.log(" inject js interface completely on progress " + newProgress);
13        }
15    }
你可以看⼀下这个注⼊的时机问题。第七⾏,这个地⽅是有问题的,因为⼤家都知道实际上你webview的性能⼀直以来都不是太好,还有很多机能很差或者rom 优化很差的 webview
根本就是⼀团坑,所以这个⾥⾯类似于硬编码的这个注⼊过程是不太完美的。在少部分机型以及少部分场景中,这⾥会⼀直注⼊失败的。导致整个框架都不可⽤。
所以有代码洁癖的同学要注意了,这个⽹上流传最⼴的开源⽅案⽬前是有缺陷的。要慎⽤~不过这种开源⽅案能cover住百分之95以上的⼿机我觉得也还⾏了。
所以⽬前来看,并没有⼀个特别有效⽽且安全完美的⽅案来规避这个问题。有⼈说hybrid 做的不错,实际上我看过他的js sdk。实际上啊,并不是⽤的我们所说的prompt⽅法
他还是和⽹易那个⼀样通过拦截url 分析url 来执⾏相应的操作的。native 回调js代码也是⾛的js⾥的_handleMessageFromWeixin 这份⽅法。有兴趣的同学可以去看下的做法。
但你其实想⼀想这个⽅法也是有缺陷的,因为url是可以伪造的,好在⾃⼰会在native代码⾥验证他的appid。所以⼀定程度上可以避免⼤部分的攻击。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。