最近在研究Hybrid的相关东西,其中重要一项就是JS与Java的相互调用以及传递参数,传统的方法是通过Webview的addJavascriptInterface方法来注入一个JS全局对象,但这样容易产生很大的安全隐患。比较好的方法就是在加载网页的过程中动态注入一段JS脚本,JSBridge这个开源库就是利用此种方法,此库支持异步回调,方法参数支持js所有已知的类型,包括number、string、boolean、object、function;Java层方法可以返回void或能转为字符串的类型(如int、long、String、double、float等)或可序列化的自定义类型。下面将仔细分析它的工作原理。
脚本注入时机
在WebChromeClient的onProgressChanged方法中注入JS脚本。
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
| @Override public void onProgressChanged (WebView view, int newProgress) { if (newProgress <= 25) { mIsInjectedJS = false; } else if (!mIsInjectedJS) { view.loadUrl(mJsCallJava.getPreloadInterfaceJS()); mIsInjectedJS = true; Log.d(TAG, " inject js interface completely on progress " + newProgress);
} super.onProgressChanged(view, newProgress); } ````
# 如何生成脚本(重点)
```java
public JsCallJava (String injectedName, Class injectedCls) { try { if (TextUtils.isEmpty(injectedName)) { throw new Exception("injected name can not be null"); } mInjectedName = injectedName; mMethodsMap = new HashMap<String, Method>(); Method[] methods = injectedCls.getDeclaredMethods(); StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\""); sb.append(mInjectedName); sb.append(" 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]}}};"); for (Method method : methods) { String sign; if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) == null) { continue; } mMethodsMap.put(sign, method); sb.append(String.format("a.%s=", method.getName())); }
sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\""); sb.append(mInjectedName); sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h<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\""); sb.append(mInjectedName); sb.append(" 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\"&&d!==\"callback\"){a[d]=function(){return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0)))}}});b."); sb.append(mInjectedName); sb.append("=a;console.log(\""); sb.append(mInjectedName); sb.append(" initialization end\")})(window);"); mPreloadInterfaceJS = sb.toString(); } catch(Exception e){ Log.e(TAG, "init js error:" + e.getMessage()); } }
|
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
| private String genJavaMethodSign (Method method) { String sign = method.getName(); Class[] argsTypes = method.getParameterTypes(); int len = argsTypes.length; if (len < 1 || argsTypes[0] != WebView.class) { Log.w(TAG, "method(" + sign + ") must use webview to be first parameter, will be pass"); return null; } for (int k = 1; k < len; k++) { Class cls = argsTypes[k]; if (cls == String.class) { sign += "_S"; } else if (cls == int.class || cls == long.class || cls == float.class || cls == double.class) { sign += "_N"; } else if (cls == boolean.class) { sign += "_B"; } else if (cls == JSONObject.class) { sign += "_O"; } else if (cls == JsCallback.class) { sign += "_F"; } else { sign += "_P"; } } return sign; }
|
通过JsCallJava这个类的构造函数来实现,传入一个JS对象名以及一个JS要调用方法的类名,注意这个类的所有方法必须是public static。通过反射此注入类的所有静态方法,并把方法的名称和形参组合起来,一起作为key存到一个map中,map的value就是该静态方法。从代码也可以看出形参不区分整型、长整型、浮点类型等,因为在JS中统统都是number。
另外,JS是通过动态拼接起来,这样应该没人看得懂的,通过调试,把他还原如下:
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
| (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]}} }; a.aabc=a.alert=a.alert=a.alert=a.delayJsCallBack=a.getIMSI=a.getOsSdk= a.goBack=a.overloadMethod=a.overloadMethod=a.passJson2Java=a.passLongType= a.retBackPassJson=a.retJavaObject=a.testLossTime=a.toast=a.toast=function(){ var f=Array.prototype.slice.call(arguments,0); if(f.length<1){ throw"HostApp call error, message:miss method name" } var e=[]; for(var h=1;h<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"&&d!=="callback"){ a[d]=function(){ return c.apply(a,[d].concat(Array.prototype.slice.call(arguments,0))) } } });
b.HostApp=a; console.log("HostApp initialization end") }
)(window);
|
相关的地方本人都写了详细注释,应该没什么问题。JS调用Java是通过prompt方法来实现,参数为3个,第一个是要调用的Java注入类的方法名;第二个是传递的参数类型数组,第三个是要传递的参数数组。
ps:对于异步回调,做了特殊处理,其参数值是a对象中queue数组的索引,queue数组中存放的就是回调函数。
Webview处理prompt方法
在WebChromeClient的onJsPrompt方法处理prompt方法
1 2 3 4 5 6
| @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { result.confirm(mJsCallJava.call(view, message)); return true; }
|
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 76 77
| public String call(WebView webView, String jsonStr) { if (!TextUtils.isEmpty(jsonStr)) { try { JSONObject callJson = new JSONObject(jsonStr); String methodName = callJson.getString("method"); JSONArray argsTypes = callJson.getJSONArray("types"); JSONArray argsVals = callJson.getJSONArray("args"); String sign = methodName; int len = argsTypes.length(); Object[] values = new Object[len + 1]; int numIndex = 0; String currType;
values[0] = webView;
for (int k = 0; k < len; k++) { currType = argsTypes.optString(k); if ("string".equals(currType)) { sign += "_S"; values[k + 1] = argsVals.isNull(k) ? null : argsVals.getString(k); } else if ("number".equals(currType)) { sign += "_N"; numIndex = numIndex * 10 + k + 1; } else if ("boolean".equals(currType)) { sign += "_B"; values[k + 1] = argsVals.getBoolean(k); } else if ("object".equals(currType)) { sign += "_O"; values[k + 1] = argsVals.isNull(k) ? null : argsVals.getJSONObject(k); } else if ("function".equals(currType)) { sign += "_F"; values[k + 1] = new JsCallback(webView, mInjectedName, argsVals.getInt(k)); } else { sign += "_P"; } }
Method currMethod = mMethodsMap.get(sign);
if (currMethod == null) { return getReturn(jsonStr, 500, "not found method(" + sign + ") with valid parameters"); } if (numIndex > 0) { Class[] methodTypes = currMethod.getParameterTypes(); int currIndex; Class currCls; while (numIndex > 0) { currIndex = numIndex - numIndex / 10 * 10; currCls = methodTypes[currIndex]; if (currCls == int.class) { values[currIndex] = argsVals.getInt(currIndex - 1); } else if (currCls == long.class) { values[currIndex] = Long.parseLong(argsVals.getString(currIndex - 1)); } else { values[currIndex] = argsVals.getDouble(currIndex - 1); } numIndex /= 10; } }
return getReturn(jsonStr, 200, currMethod.invoke(null, values)); } catch (Exception e) { if (e.getCause() != null) { return getReturn(jsonStr, 500, "method execute error:" + e.getCause().getMessage()); } return getReturn(jsonStr, 500, "method execute error:" + e.getMessage()); } } else { return getReturn(jsonStr, 500, "call data empty"); } }
|
将prompt的json参数传递到了call方法中,然后就能得到相应的方法名,参数类型,参数值;通过方法名和参数类型就能从注入类找到相应方法,最后通过参数值构造实际参数,反射调用注入类的方法即可完成整个JS->Java的调用。
JS异步回调
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
| private static final String CALLBACK_JS_FORMAT = "javascript:%s.callback(%d, %d %s);"; public void apply (Object... args) throws JsCallbackException { if (mWebViewRef.get() == null) { throw new JsCallbackException("the WebView related to the JsCallback has been recycled"); } if (!mCouldGoOn) { throw new JsCallbackException("the JsCallback isn't permanent,cannot be called more than once"); } StringBuilder sb = new StringBuilder(); for (Object arg : args){ sb.append(","); boolean isStrArg = arg instanceof String; if (isStrArg) { sb.append("\""); } sb.append(String.valueOf(arg)); if (isStrArg) { sb.append("\""); } } String execJs = String.format(CALLBACK_JS_FORMAT, mInjectedName, mIndex, mIsPermanent, sb.toString()); Log.d("JsCallBack", execJs); mWebViewRef.get().loadUrl(execJs); mCouldGoOn = mIsPermanent > 0; }
|
异步回调是通过JsCallback的apply来进行的,具体实现就是上面分析的JS。
1 2 3 4 5
| 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]}}
|
ps:回调只能使用一次,用完就会被删除,如果要多次使用,可以使用JsCallback提供setPermanent(true)来实现。