`
modabobo
  • 浏览: 506229 次
文章分类
社区版块
存档分类
最新评论

在WebView中如何让JS与Java安全地互相调用

 
阅读更多


在现在安卓应用原生开发中,为了追求开发的效率以及移植的便利性,使用WebView作为业务内容展示与交互的主要载体是个不错的折中方案。那么在这种Hybrid(混合式) App中,难免就会遇到页面JS需要与Java相互调用,调用Java方法去做那部分网页JS不能完成的功能。

网上的方法可以告诉我们这个时候我们可以使用addjavascriptInterface来注入原生接口到JS中,但是在安卓4.2以下的系统中,这种方案却我们的应用带来了很大的安全风险。攻击者如果在页面执行一些非法的JS(诱导用户打开一些钓鱼网站以进入风险页面),极有可能反弹拿到用户手机的shell权限。接下来攻击者就可以在后台默默安装木马,完全洞穿用户的手机。详细的攻击过程可以见乌云平台的这份报告:WebView中接口隐患与手机挂马利用

安卓4.2及以上版本(API >= 17),在注入类中为可调用的方法添加@JavascriptInterface注解,无注解的方法不能被调用,这种方式可以防范注入漏洞。那么有没一种安全的方式,可以完全兼顾安卓4.2以下版本呢?答案就是使用prompt,即WebChromeClient 输入框弹出模式。

我们参照Android WebView的Js对象注入漏洞解决方案这篇文章给出的解决方案, 但它JS下的方法有点笨拙, 动态生成JS文件过程也并没有清晰,且加载JS文件的时机也没有准确把握。那么如何改造才能便利地在JS代码中调用Java方法,并且安全可靠呢?

下面提到的源码及项目可以在这找到Safe Java-JS Bridge In Android WebView[Github]

一、动态地生成将注入的JS代码

JsCallJava在构造时,将要注入类的public且static方法拿出来,逐个生成方法的签名,依据方法签名先将方法缓存起来,同时结合方法名称与静态的HostApp-JS代码动态生成一段将要注入到webview中的字符串。

<figure class="highlight JAVA" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;"><br>JsCallJava.java</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">public</span> <span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">JsCallJava</span> (String injectedName, Class injectedCls) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">try</span> { mMethodsMap = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">new</span> HashMap&lt;String, Method&gt;(); <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//获取自身声明的所有方法(包括public private protected), getMethods会获得所有继承与非继承的方法</span> Method[] methods = injectedCls.getDeclaredMethods(); StringBuilder sb = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">new</span> StringBuilder(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"javascript:(function(b){console.log(\"HostApp initialization begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};"</span>); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">for</span> (Method method : methods) { String sign; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">null</span>) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">continue</span>; } mMethodsMap.put(sign, method); sb.append(String.format(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"a.%s="</span>, method.getName())); } sb.append(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"function(){var f=Array.prototype.slice.call(arguments,0);if(f.length&lt;1){throw\"HostApp call error, message:miss method name\"}var e=[];for(var h=1;h&lt;f.length;h++){var c=f[h];var j=typeof c;e[e.length]=j;if(j==\"function\"){var d=a.queue.length;a.queue[d]=c;f[h]=d}}var g=JSON.parse(prompt(JSON.stringify({method:f.shift(),types:e,args:f})));if(g.code!=200){throw\"HostApp call error, code:\"+g.code+\", message:\"+g.result}return g.result};Object.getOwnPropertyNames(a).forEach(function(d){var c=a[d];if(typeof c===\"function\"&amp;&amp;d!==\"callback\"){a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))}}});b."</span> + injectedName + <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"=a;console.log(\"HostApp initialization end\")})(window);"</span>); mPreloadInterfaceJS = sb.toString(); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">catch</span>(Exception e){ Log.e(TAG, <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"init js error:"</span> + e.getMessage()); } } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">private</span> String <span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">genJavaMethodSign</span> (Method method) { String sign = method.getName(); Class[] argsTypes = method.getParameterTypes(); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span> len = argsTypes.length; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (len &lt; <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span> || argsTypes[<span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>] != WebView.class) { Log.w(TAG, <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"method("</span> + sign + <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">") must use webview to be first parameter, will be pass"</span>); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">null</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">for</span> (<span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span> k = <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>; k &lt; len; k++) { Class cls = argsTypes[k]; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (cls == String.class) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_S"</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (cls == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span>.class || cls == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">long</span>.class || cls == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">float</span>.class || cls == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">double</span>.class) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_N"</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (cls == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">boolean</span>.class) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_B"</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (cls == JSONObject.class) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_O"</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (cls == JsCallback.class) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_F"</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_P"</span>; } } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> sign; } </pre></td> </tr></tbody></table></figure>

从上面可以看出,类的各个方法名称被拼接到前后两段静态压缩的JS代码当中,那么这样生成的完整清晰的HostApp-JS片段是怎样的呢? 我们假设HostJsScope类中目前只定义了toast、alert、getIMSI这三个公开静态方法,那么完整的片段就是下面这样:

<figure class="highlight JAVASCRIPT" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">HostApp JS片段</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">(<span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span><span class="params" style="margin: 0px; padding: 0px;">(global)</span>{</span> console.log(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp initialization begin"</span>); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> hostApp = { queue: [], callback: <span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span> <span class="params" style="margin: 0px; padding: 0px;">()</span> {</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> args = <span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">Array</span>.prototype.slice.call(<span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">arguments</span>, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> index = args.shift(); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> isPermanent = args.shift(); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">this</span>.queue[index].apply(<span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">this</span>, args); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (!isPermanent) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">delete</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">this</span>.queue[index]; } } }; hostApp.toast = hostApp.alert = hostApp.getIMSI = <span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span> <span class="params" style="margin: 0px; padding: 0px;">()</span> {</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> args = <span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">Array</span>.prototype.slice.call(<span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">arguments</span>, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (args.length &lt; <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">throw</span> <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp call error, message:miss method name"</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> aTypes = []; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">for</span> (<span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> i = <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>;i &lt; args.length;i++) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> arg = args[i]; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> type = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">typeof</span> arg; aTypes[aTypes.length] = type; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (type == <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"function"</span>) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> index = hostApp.queue.length; hostApp.queue[index] = arg; args[i] = index; } } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> res = <span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">JSON</span>.parse(prompt(<span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">JSON</span>.stringify({ method: args.shift(), types: aTypes, args: args }))); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (res.code != <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">200</span>) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">throw</span> <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp call error, code:"</span> + res.code + <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">", message:"</span> + res.result; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> res.result; }; <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//有时候,我们希望在该方法执行前插入一些其他的行为用来检查当前状态或是监测</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//代码行为,这就要用到拦截(Interception)或者叫注入(Injection)技术了</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">/** * Object.getOwnPropertyName 返回一个数组,内容是指定对象的所有属性 * * 其后遍历这个数组,分别做以下处理: * 1. 备份原始属性; * 2. 检查属性是否为 function(即方法); * 3. 若是重新定义该方法,做你需要做的事情,之后 apply 原来的方法体。 */</span> <span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">Object</span>.getOwnPropertyNames(hostApp).forEach(<span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span> <span class="params" style="margin: 0px; padding: 0px;">(property)</span> {</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> original = hostApp[property]; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (<span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">typeof</span> original === <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">'function'</span>&amp;&amp;property!==<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"callback"</span>) { hostApp[property] = <span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span> <span class="params" style="margin: 0px; padding: 0px;">()</span> {</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> original.apply(hostApp, [property].concat(<span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">Array</span>.prototype.slice.call(<span class="built_in" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">arguments</span>, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>))); }; } }); global.HostApp = hostApp; console.log(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp initialization end"</span>); })(window); </pre></td> </tr></tbody></table></figure>

其实在JsCallJava初始化时我们拼接的只是上面第15行hostApp.toast = hostApp.alert = hostApp.getIMSI = function ()这段。目的是将所有JS层调用函数嫁接到一个匿名函数1中,而后利用拦截技术,遍历hostApp下所有的函数,拿出对应的函数名,然后将hostApp下所有的函数调用嫁接到另一个匿名函数2,这样做的目的是hostApp下函数调用时首先执行匿名函数2,匿名函数2将对应的函数名作为第一个参数然后再调用匿名函数1,这样匿名函数1中就能区分执行时调用来源。实现了JS层调用入口统一,返回出口统一的结构体系。

二、HostApp JS片段注入时机

步骤一说明了HostApp-JS片段的拼接方法,同时JS片段拼接是在JsCallJava初始化完成的,而JsCallJava初始化是在实例化InjectedChromeClient对象时发起的。

<figure class="highlight JAVA" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">InjectedChromeClient.java</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">public</span> <span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">InjectedChromeClient</span> (String injectedName, Class injectedCls) { mJsCallJava = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">new</span> JsCallJava(injectedName, injectedCls); } </pre></td> </tr></tbody></table></figure>
从步骤一的代码,我们知道JsCallJava拼接出来的JS代码暂时被存到mPreloadInterfaceJS字段中。那么我们何时把这段代码串注入到Webview的页面空间内呢?答案是页面加载进度变化的过程中。
<figure class="highlight JAVA" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">InjectedChromeClient.java</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);"><span class="annotation" style="margin: 0px; padding: 0px;">@Override</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">public</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">void</span> <span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">onProgressChanged</span> (WebView view, <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span> newProgress) { <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//为什么要在这里注入JS</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//1 OnPageStarted中注入有可能全局注入不成功,导致页面脚本上所有接口任何时候都不可用</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//2 OnPageFinished中注入,虽然最后都会全局注入成功,但是完成时间有可能太晚,当页面在初始化调用接口函数时会等待时间过长</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//3 在进度变化时注入,刚好可以在上面两个问题中得到一个折中处理</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//为什么是进度大于25%才进行注入,因为从测试看来只有进度大于这个数字页面才真正得到框架刷新加载,保证100%注入成功</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (newProgress &lt;= <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">25</span>) { mIsInjectedJS = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">false</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (!mIsInjectedJS) { view.loadUrl(mJsCallJava.getPreloadInterfaceJS()); mIsInjectedJS = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">true</span>; Log.d(TAG, <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">" inject js interface completely on progress "</span> + newProgress); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">super</span>.onProgressChanged(view, newProgress); } </pre></td> </tr></tbody></table></figure>

从上面我们可以看出,注入的时机是准确把握在进度大于25%时。如果在OnPageFinished注入,页面document.ready的初始回调会等待时间过长,详细的原因我们会在后面讲到。

三、页面调用Java方法执行的过程

OK,上面两步解决了动态生成与成功注入的两大问题,接下来就要处理JS具体的调用过程。上面,我们知道页面调用Java方法时,匿名js函数在拼接好参数后prompt json数据。prompt消息被Java层的WebChromeClient.onJsPrompt拦截到。

<figure class="highlight JAVA" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">InjectedChromeClient.java</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);"><span class="annotation" style="margin: 0px; padding: 0px;">@Override</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">public</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">boolean</span> <span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">onJsPrompt</span>(WebView view, String url, String message, String defaultValue, JsPromptResult result) { result.confirm(mJsCallJava.call(view, message)); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">true</span>; } </pre></td> </tr></tbody></table></figure>

而JsCallJava.call的具体实现如下。

<figure class="highlight JAVA" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">JsCallJava.java</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">public</span> String <span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">call</span>(WebView webView, String jsonStr) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (!TextUtils.isEmpty(jsonStr)) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">try</span> { JSONObject callJson = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">new</span> JSONObject(jsonStr); String methodName = callJson.getString(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"method"</span>); JSONArray argsTypes = callJson.getJSONArray(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"types"</span>); JSONArray argsVals = callJson.getJSONArray(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"args"</span>); String sign = methodName; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span> len = argsTypes.length(); Object[] values = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">new</span> Object[len + <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>]; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span> numIndex = <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>; String currType; values[<span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>] = webView; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">for</span> (<span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span> k = <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>; k &lt; len; k++) { currType = argsTypes.optString(k); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"string"</span>.equals(currType)) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_S"</span>; values[k + <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>] = argsVals.isNull(k) ? <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">null</span> : argsVals.getString(k); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"number"</span>.equals(currType)) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_N"</span>; numIndex = numIndex * <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">10</span> + k + <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>; } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"boolean"</span>.equals(currType)) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_B"</span>; values[k + <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>] = argsVals.getBoolean(k); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"object"</span>.equals(currType)) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_O"</span>; values[k + <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>] = argsVals.isNull(k) ? <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">null</span> : argsVals.getJSONObject(k); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"function"</span>.equals(currType)) { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_F"</span>; values[k + <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>] = <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">new</span> JsCallback(webView, argsVals.getInt(k)); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> { sign += <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"_P"</span>; } } Method currMethod = mMethodsMap.get(sign); <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">// 方法匹配失败</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (currMethod == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">null</span>) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> getReturn(jsonStr, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">500</span>, <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"not found method("</span> + methodName + <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">") with valid parameters"</span>); } <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">// 数字类型细分匹配</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (numIndex &gt; <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>) { Class[] methodTypes = currMethod.getParameterTypes(); <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span> currIndex; Class currCls; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">while</span> (numIndex &gt; <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>) { currIndex = numIndex - numIndex / <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">10</span> * <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">10</span>; currCls = methodTypes[currIndex]; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (currCls == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">int</span>.class) { values[currIndex] = argsVals.getInt(currIndex - <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (currCls == <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">long</span>.class) { <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//WARN: argsJson.getLong(k + defValue) will return a bigger incorrect number</span> values[currIndex] = Long.parseLong(argsVals.getString(currIndex - <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>)); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> { values[currIndex] = argsVals.getDouble(currIndex - <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">1</span>); } numIndex /= <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">10</span>; } } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> getReturn(jsonStr, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">200</span>, currMethod.invoke(<span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">null</span>, values)); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">catch</span> (Exception e) { <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//优先返回详细的错误信息</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span> (e.getCause() != <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">null</span>) { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> getReturn(jsonStr, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">500</span>, <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"method execute error:"</span> + e.getCause().getMessage()); } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> getReturn(jsonStr, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">500</span>, <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"method execute error:"</span> + e.getMessage()); } } <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">else</span> { <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">return</span> getReturn(jsonStr, <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">500</span>, <span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"call data empty"</span>); } } </pre></td> </tr></tbody></table></figure>

这是一个完整的解析匹配过程,会依据js层传入的方法名、参数类型列表再次生成方法签名,与之前初始化构造好的缓存对象中的方法匹配。匹配成功后则判断js调用参数类型中是否有number类型,如果有依据Java层方法的定义决定是取int、long还是double类型的值。最后使用调用值列表和方法对象反射执行,返回函数执行的结果。这里有几点需要注意:

  • 方法反射执行时会将当前WebView的实例放到第一个参数,方便在HostJsScope静态方法依据Context拿到一些相关上下文信息;
  • 注入类(如HostJsScope)静态方法的参数定义可使用的类型有int/long/double、String、boolean、JSONObject、JsCallback,对应于js层传入的类型为number、string、boolean、object、function,注意number数字过大时(如时间戳),可能需要先转为string类型(Java方法中参数也须定义为String),避免精度丢失
  • Java方法的返回值可以是void 或 能转为字符串的类型(如int、long、String、double、float等)或可序列化的自定义类型
  • 如果执行失败或找不到调用方法时,Java层会将异常信息传递到JS层, JS匿名函数中会throw抛出错误;

四、HostApp在页面的使用

有了上面的准备工作,现在我们在页面中就可以很方便地使用HostApp了,而不需要加载任何依赖文件。如li标签的点击:

<figure class="highlight HTML" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">test.html</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 6 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);"><span class="tag" style="margin: 0px; padding: 0px;">&lt;<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">ul</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">class</span>=<span class="value" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"entry"</span>&gt;</span> <span class="tag" style="margin: 0px; padding: 0px;">&lt;<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">onclick</span>=<span class="value" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp.alert('HostApp.alert');"</span>&gt;</span>HostApp.alert<span class="tag" style="margin: 0px; padding: 0px;">&lt;/<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span>&gt;</span> <span class="tag" style="margin: 0px; padding: 0px;">&lt;<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">onclick</span>=<span class="value" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp.toast('HostApp.toast');"</span>&gt;</span>HostApp.toast<span class="tag" style="margin: 0px; padding: 0px;">&lt;/<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span>&gt;</span> <span class="tag" style="margin: 0px; padding: 0px;">&lt;<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">onclick</span>=<span class="value" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp.testLossTime(new Date().getTime() + '');"</span>&gt;</span>HostApp.testLossTime<span class="tag" style="margin: 0px; padding: 0px;">&lt;/<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span>&gt;</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">&lt;!-- 时间戳长整型调用前先转换为string --&gt;</span> <span class="tag" style="margin: 0px; padding: 0px;">&lt;<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">onclick</span>=<span class="value" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp.toast(HostApp.getIMSI());"</span>&gt;</span>HostApp.getIMSI<span class="tag" style="margin: 0px; padding: 0px;">&lt;/<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">li</span>&gt;</span> <span class="tag" style="margin: 0px; padding: 0px;">&lt;/<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">ul</span>&gt;</span> </pre></td> </tr></tbody></table></figure>

但同时有一种业务情景时,页面初始加载完备时就应立即触发的调用,如果我们这样写:

<figure class="highlight JAVASCRIPT" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">test.html</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">document.addEventListener(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">'DOMContentLoaded'</span>, <span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span><span class="params" style="margin: 0px; padding: 0px;">()</span> {</span> HostApp.toast(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">'document ready now'</span>);; }, <span class="literal" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">false</span>); </pre></td> </tr></tbody></table></figure>

那么HostApp的调用极有可能不成功,因为端注入HostApp-JS片段的时机可能在document.ready前也可能在其后。那么如何解决这个矛盾的问题呢?

如果document.ready的时候HostApp JS已经注入成功,这种情况OK没有问题。当document.ready的时候HostApp JS还未开始注入,这种情景下我们的js脚本层就需要做出变动,即轮询状态,直到端注入成功或者超时(1.5s),再发生回调。具体实现如下(下面的是以zepto.js的$.ready()函数改造为例)。

<figure class="highlight JAVASCRIPT" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">zepto.js</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);"><span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//针对DOM的一些操作</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">// Define methods that will be available on all</span> <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">// Zepto collections</span> $.fn = { <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//DOM Ready</span> ready: <span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span><span class="params" style="margin: 0px; padding: 0px;">(callback, jumpHostAppInject)</span> {</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> originCb = callback; <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">var</span> mcounter = <span class="number" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">0</span>; <span class="comment" style="margin: 0px; padding: 0px; color: rgb(147, 161, 161); font-style: italic;">//尝试等待(1500ms超时)让端注入HostApp Js</span> callback = <span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span> <span class="params" style="margin: 0px; padding: 0px;">()</span> {</span> <span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">if</span>(!window.HostApp &amp;&amp; mcounter++ <span class="xml" style="margin: 0px; padding: 0px;">&lt; <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">150</span>)<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">setTimeout</span>(<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">callback</span>, <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">10</span>);<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">else</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">originCb</span>($); }; //是否跳过等待<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">HostApp</span>的注入 <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">if</span> (<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">jumpHostAppInject</span>) { <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">callback</span> = <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">originCb</span>; } <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">if</span> (<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">readyRE.test</span>(<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">document.readyState</span>)) <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">callback</span>($); <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">else</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">document.addEventListener</span>('<span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">DOMContentLoaded</span>', <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">function</span>() { <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">callback</span>($) }, <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">false</span>); <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">return</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">this</span> }, <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">...</span> <span class="attribute" style="margin: 0px; padding: 0px; color: rgb(181, 137, 0);">...</span> };</span> </pre></td> </tr></tbody></table></figure>

这样的机制也就解释了为什么不把Java层的JS注入放在OnPageFinish了,如果那样页面轮询的次数就会上升,等待的时间就会变长,而且有可能会超时。好了,有了上面的改动,页面初始加载完备时需要立即触发HostApp的调用,如下:

<figure class="highlight JAVASCRIPT" style="margin: 15px 0px 0px; padding: 7px 15px; border: 1px solid rgb(153, 153, 153); line-height: 1.6; overflow: auto; position: relative; font-size: 0.9em; color: rgb(68, 68, 68); font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: justify; background: rgb(238, 238, 238);"><figcaption style="margin: 0px 0px 5px; padding: 0px; color: rgb(153, 153, 153); text-shadow: rgb(255, 255, 255) 0px 0px 1px;"><span style="margin: 0px; padding: 0px;">test.html</span></figcaption><table style="margin: 0px; padding: 0px; border-spacing: 0px;"><tbody style="margin: 0px; padding: 0px;"><tr style="margin: 0px; padding: 0px;"> <td class="gutter" style="margin: 0px; padding: 0px 15px 0px 0px; color: rgb(153, 153, 153); border-right-width: 1px; border-right-style: solid; border-right-color: rgb(153, 153, 153); text-align: right;"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">1 2 3 4 5 </pre></td> <td class="code" style="margin: 0px; padding: 0px 0px 0px 15px; color: rgb(102, 102, 102);"><pre style="margin-top: 0px; margin-bottom: 0px; padding: 0px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace; border: none; background: rgb(238, 238, 238);">&lt;script type=<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"text/javascript"</span>&gt; $(<span class="function" style="margin: 0px; padding: 0px;"><span class="keyword" style="margin: 0px; padding: 0px; color: rgb(133, 153, 0);">function</span> <span class="params" style="margin: 0px; padding: 0px;">()</span> {</span> HostApp.alert(<span class="string" style="margin: 0px; padding: 0px; color: rgb(42, 161, 152);">"HostApp ready now"</span>); }); <span class="xml" style="margin: 0px; padding: 0px;">&lt;/<span class="title" style="margin: 0px; padding: 0px; color: rgb(38, 139, 210);">script</span>&gt;</span> </pre></td> </tr></tbody></table></figure>

更多使用说明及完整源代码见:Safe Java-JS Bridge In Android WebView[Github]




原文:http://www.pedant.cn/2014/07/04/webview-js-java-interface-research/

感谢分享。




分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics