在Android中,JSBridge已经不是什么新鲜的事物了,各家的实现方式也略有差异。大多数人都知道WebView存在一个漏洞,见WebView中接口隐患与手机挂马利用,虽然该漏洞已经在Android 4.2上修复了,即使用@JavascriptInterface代替addJavascriptInterface,但是由于兼容性和安全性问题,基本上我们不会再利用Android系统为我们提供的addJavascriptInterface方法或者@JavascriptInterface注解来实现,所以我们只能另辟蹊径,去寻找既安全,又能实现兼容Android各个版本的方案。
首先我们来了解一下为什么要使用JSBridge,在开发中,为了追求开发的效率以及移植的便利性,一些展示性强的页面我们会偏向于使用h5来完成,功能性强的页面我们会偏向于使用native来完成,而一旦使用了h5,为了在h5中尽可能的得到native的体验,我们native层需要暴露一些方法给js调用,比如,弹Toast提醒,弹Dialog,分享等等,有时候甚至把h5的网络请求放着native去完成,而JSBridge做得好的一个典型就是微信,微信给开发者提供了JSSDK,该SDK中暴露了很多微信native层的方法,比如支付,定位等。
那么,怎么去实现一个兼容Android各版本又具有一定安全性的JSBridge呢?我们知道,在WebView中,如果java要调用js的方法,是非常容易做到的,使用WebView.loadUrl(“javascript:function()”)即可,这样,就做到了JSBridge的native层调用h5层的单向通信,但是h5层如何调native层呢,我们需要寻找这么一个通道,仔细回忆一下,WebView有一个方法,叫setWebChromeClient,可以设置WebChromeClient对象,而这个对象中有三个方法,分别是onJsAlert,onJsConfirm,onJsPrompt,当js调用window对象的对应的方法,即window.alert,window.confirm,window.prompt,WebChromeClient对象中的三个方法对应的就会被触发,我们是不是可以利用这个机制,自己做一些处理呢?答案是肯定的。
至于js这三个方法的区别,可以详见w3c JavaScript 消息框 。一般来说,我们是不会使用onJsAlert的,为什么呢?因为js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而confirm和prompt的使用频率相对alert来说,则更低一点。那么到底是选择confirm还是prompt呢,其实confirm的使用频率也是不低的,比如你点一个链接下载一个文件,这时候如果需要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,类似这种场景还是很多的,因此不能占用confirm。而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,所以我们完全可以使用这个方法。该方法就是弹出一个输入框,然后让你输入,输入完成后返回输入框中的内容。因此,占用prompt是再完美不过了。
到这一步,我们已经找到了JSBridge双向通信的一个通道了,接下来就是如何实现的问题了。本文中实现的只是一个简单的demo,如果要在生产环境下使用,还需要自己做一层封装。
要进行正常的通信,通信协议的制定是必不可少的。我们回想一下熟悉的http请求url的组成部分。形如http://host:port/path?param=value,我们参考http,制定JSBridge的组成部分,我们的JSBridge需要传递给native什么信息,native层才能完成对应的功能,然后将结果返回呢?显而易见我们native层要完成某个功能就需要调用某个类的某个方法,我们需要将这个类名和方法名传递过去,此外,还需要方法调用所需的参数,为了通信方便,native方法所需的参数我们规定为json对象,我们在js中传递这个json对象过去,native层拿到这个对象再进行解析即可。为了区别于http协议,我们的jsbridge使用jsbridge协议,为了简单起见,问号后面不适用键值对,我们直接跟上我们的json字符串,于是就有了形如下面的这个uri
1
|
jsbridge
:
//className:port/methodName?jsonObj
|
有人会问,这个port用来干嘛,其实js层调用native层方法后,native需要将执行结果返回给js层,不过你会觉得通过WebChromeClient对象的onJsPrompt方法将返回值返回给js不就好了吗,其实不然,如果这么做,那么这个过程就是同步的,如果native执行异步操作的话,返回值怎么返回呢?这时候port就发挥了它应有的作用,我们在js中调用native方法的时候,在js中注册一个callback,然后将该callback在指定的位置上缓存起来,然后native层执行完毕对应方法后通过WebView.loadUrl调用js中的方法,回调对应的callback。那么js怎么知道调用哪个callback呢?于是我们需要将callback的一个存储位置传递过去,那么就需要native层调用js中的方法的时候将存储位置回传给js,js再调用对应存储位置上的callback,进行回调。于是,完整的协议定义如下:
1
|
jsbridge
:
//className:callbackAddress/methodName?jsonObj
|
假设我们需要调用native层的Logger类的log方法,当然这个类以及方法肯定是遵循某种规范的,不是所有的java类都可以调用,不然就跟文章开头的WebView漏洞一样了,参数是msg,执行完成后js层要有一个回调,那么地址就如下
1
|
jsbridge
:
//Logger:callbackAddress/log?{"msg":"native log"}
|
至于这个callback对象的地址,可以存储到js中的window对象中去。至于怎么存储,后文会慢慢倒来。
上面是js向native的通信协议,那么另一方面,native向js的通信协议也需要制定,一个必不可少的元素就是返回值,这个返回值和js的参数做法一样,通过json对象进行传递,该json对象中有状态码code,提示信息msg,以及返回结果result,如果code为非0,则执行过程中发生了错误,错误信息在msg中,返回结果result为null,如果执行成功,返回的json对象在result中。下面是两个例子,一个成功调用,一个调用失败。
1
2
3
4
5
|
{
"code"
:
500
,
"msg"
:
"method is not exist"
,
"result"
:
null
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"code"
:
0
,
"msg"
:
"ok"
,
"result"
:
{
"key1"
:
"returnValue1"
,
"key2"
:
"returnValue2"
,
"key3"
:
{
"nestedKey"
:
"nestedValue"
"nestedArray"
:
[
"value1"
,
"value2"
]
}
}
}
|
那么这个结果如何返回呢,native调用js暴露的方法即可,然后将js层传给native层的port一并带上,进行调用即可,调用的方式就是通过WebView.loadUrl方式来完成,如下。
1
|
mWebView
.
loadUrl
(
"javascript:JSBridge.onFinish(port,jsonObj);"
)
;
|
关于JsBridge.onFinish方法的实现,后面再叙述。前面我们提到了native层的方法必须遵循某种规范,不然就非常不安全了。在native中,我们需要一个JSBridge统一管理这些暴露给js的类和方法,并且能实时添加,这时候就需要这么一个方法
1
|
JSBridge
.
register
(
"jsName"
,
javaClass
.
class
)
|
这个javaClass就是满足某种规范的类,该类中有满足规范的方法,我们规定这个类需要实现一个空接口,为什么呢?主要作用就混淆的时候不会发生错误,还有一个作用就是约束JSBridge.register方法第二个参数必须是该接口的实现类。那么我们定义这个接口
1
2
|
public
interface
IBridge
{
}
|
类规定好了,类中的方法我们还需要规定,为了调用方便,我们规定类中的方法必须是static的,这样直接根据类而不必新建对象进行调用了(还要是public的),然后该方法不具有返回值,因为返回值我们在回调中返回,既然有回调,参数列表就肯定有一个callback,除了callback,当然还有前文提到的js传来的方法调用所需的参数,是一个json对象,在java层中我们定义成JSONObject对象;方法的执行结果需要通过callback传递回去,而java执行js方法需要一个WebView对象,于是,满足某种规范的方法原型就出来了。
1
2
3
|
public
static
void
methodName
(
WebView
web
view
,
JSONObject
jsonObj
,
Callback
callback
)
{
}
|
js层除了上文说到的JSBridge.onFinish(port,jsonObj);方法用于回调,应该还有一个方法提供调用native方法的功能,该函数的原型如下
1
|
JSBridge
.
call
(
className
,
methodName
,
params
,
callback
)
|
在call方法中再将参数组合成形如下面这个格式的uri
1
|
jsbridge
:
//className:callbackAddress/methodName?jsonObj
|
然后调用window.prompt方法将uri传递过去,这时候java层就会收到这个uri,再进一步解析即可。
万事具备了,只欠如何编码了,别急,下面我们一步一步的来实现,先完成js的两个方法。新建一个文件,命名为JSBridge.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
|
(
function
(
win
)
{
var
hasOwnProperty
=
Object
.
prototype
.
hasOwnProperty
;
var
JSBridge
=
win
.
JSBridge
||
(
win
.
JSBridge
=
{
}
)
;
var
JSBRIDGE_PROTOCOL
=
'JSBridge'
;
var
Inner
=
{
callbacks
:
{
}
,
call
:
function
(
obj
,
method
,
params
,
callback
)
{
console
.
log
(
obj
+
" "
+
method
+
" "
+
params
+
" "
+
callback
)
;
var
port
=
Util
.
getPort
(
)
;
console
.
log
(
port
)
;
this
.
callbacks
[
port
]
=
callback
;
var
uri
=
Util
.
getUri
(
obj
,
method
,
params
,
port
)
;
console
.
log
(
uri
)
;
window
.
prompt
(
uri
,
""
)
;
}
,
onFinish
:
function
(
port
,
jsonObj
)
{
var
callback
=
this
.
callbacks
[
port
]
;
callback
&
callback
(
jsonObj
)
;
delete
this
.
callbacks
[
port
]
;
}
,
}
;
var
Util
=
{
getPort
:
function
(
)
{
return
Math
.
floor
(
Math
.
random
(
)
*
(
1
30
)
)
;
}
,
getUri
:
function
(
obj
,
method
,
params
,
port
)
{
params
=
this
.
getParam
(
params
)
;
var
uri
=
JSBRIDGE_PROTOCOL
+
'://'
+
obj
+
':'
+
port
+
'/'
+
method
+
'?'
+
params
;
return
uri
;
}
,
getParam
:
function
(
obj
)
{
if
(
obj
&
typeof
obj
===
'object'
)
{
return
JSON
.
stringify
(
obj
)
;
}
else
{
return
obj
||
''
;
}
}
}
;
for
(
var
key
in
Inner
)
{
if
(
!
hasOwnProperty
.
call
(
JSBridge
,
key
)
)
{
JSBridge
[
key
]
=
Inner
[
key
]
;
}
}
}
)
(
window
)
;
|
可以看到,我们里面有一个Util类,里面有三个方法,getPort()用于随机生成port,getParam()用于生成json字符串,getUri()用于生成native需要的协议uri,里面主要做字符串拼接的工作,然后有一个Inner类,里面有我们的call和onFinish方法,在call方法中,我们调用Util.getPort()获得了port值,然后将callback对象存储在了callbacks中的port位置,接着调用Util.getUri()将参数传递过去,将返回结果赋值给uri,调用window.prompt(uri, “”)将uri传递到native层。而onFinish()方法接受native回传的port值和执行结果,根据port值从callbacks中得到原始的callback函数,执行callback函数,之后从callbacks中删除。最后将Inner类中的函数暴露给外部的JSBrige对象,通过一个for循环一一赋值即可。
当然这个实现是最最简单的实现了,实际情况要考虑的因素太多,由于本人不是很精通js,所以只能以java的思想去写js,没有考虑到的因素姑且忽略吧,比如内存的回收等等机制。
这样,js层的编码就完成了,接下来实现java层的编码。
上文说到java层有一个空接口来进行约束暴露给js的类和方法,同时也便于混淆
1
2
|
public
interface
IBridge
{
}
|
首先我们要将js传来的uri获取到,编写一个WebChromeClient子类。
1
2
3
4
5
6
7
|
public
class
JSBridgeWebChromeClient
extends
WebChromeClient
{
@Override
public
boolean
onJsPrompt
(
WebView
view
,
String
url
,
String
message
,
String
defaultValue
,
JsPromptResult
result
)
{
result
.
confirm
(
JSBridge
.
callJava
(
view
,
message
)
)
;
return
true
;
}
}
|
之后不要忘记了将该对象设置给WebView
1
2
3
4
5
|
WebView
mWebView
=
(
WebView
)
findViewById
(
R
.
id
.
webview
)
;
WebSettings
settings
=
mWebView
.
getSettings
(
)
;
settings
.
setJavaScriptEnabled
(
true
)
;
mWebView
.
setWebChromeClient
(
new
JSBridgeWebChromeClient
(
)
)
;
mWebView
.
loadUrl
(
"file:///android_asset/index.html"
)
;
|
核心的内容来了,就是JSBridgeWebChromeClient中调用的JSBridge类的实现。前文提到该类中有这么一个方法提供注册暴露给js的类和方法
1
|
JSBridge
.
register
(
"jsName"
,
javaClass
.
class
)
|
该方法的实现其实很简单,从一个Map中查找key是不是存在,不存在则反射拿到对应的Class中的所有方法,将方法是public static void 类型的,并且参数是三个参数,分别是Webview,JSONObject,Callback类型的,如果满足条件,则将所有满足条件的方法put进去,整个实现如下
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
|
public
class
JSBridge
{
private
static
Map
>
exposedMethods
=
new
HashMap
(
)
;
public
static
void
register
(
String
exposedName
,
Class
extends
IBridge
>
clazz
)
{
if
(
!
exposedMethods
.
containsKey
(
exposedName
)
)
{
try
{
exposedMethods
.
put
(
exposedName
,
getAllMethod
(
clazz
)
)
;
}
catch
(
Exception
e
)
{
e
.
printStackTrace
(
)
;
}
}
}
private
static
HashMapgetAllMethod
(
Class
injectedCls
)
throws
Exception
{
HashMap
mMethodsMap
=
new
HashMap
(
)
;
Method
[
]
methods
=
injectedCls
.
getDeclaredMethods
(
)
;
for
(
Method
method
:
methods
)
{
String
name
;
if
(
method
.
getModifiers
(
)
!=
(
Modifier
.
PUBLIC
|
Modifier
.
STATIC
)
||
(
name
=
method
.
getName
(
)
)
==
null
)
{
continue
;
}
Class
[
]
parameters
=
method
.
getParameterTypes
(
)
;
if
(
null
!=
parameters
&
parameters
.
length
==
3
)
{
if
(
parameters
[
0
]
==
WebView
.
class
&&
parameters
[
1
]
==
JSONObject
.
class
&&
parameters
[
2
]
==
JSCallback
.
class
)
{
mMethodsMap
.
put
(
name
,
method
)
;
}
}
}
return
mMethodsMap
;
}
}
|
而至于JSBridge类中的callJava方法,就是将js传来的uri进行解析,然后根据调用的类名别名从刚刚的map中查找是不是存在,存在的话拿到该类所有方法的methodMap,然后根据方法名从methodMap拿到方法,反射调用,并将参数传进去,参数就是前文说的满足条件的三个参数,即WebView,JSONObject,Callback。
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
|
public
static
String
callJava
(
WebView
webView
,
String
uriString
)
{
String
methodName
=
""
;
String
className
=
""
;
String
param
=
"{}"
;
String
port
=
""
;
if
(
!
TextUtils
.
isEmpty
(
uriString
)
&
uriString
.
startsWith
(
"JSBridge"
)
)
{
Uri
uri
=
Uri
.
parse
(
uriString
)
;
className
=
uri
.
getHost
(
)
;
param
=
uri
.
getQuery
(
)
;
port
=
uri
.
getPort
(
)
+
""
;
String
path
=
uri
.
getPath
(
)
;
if
(
!
TextUtils
.
isEmpty
(
path
)
)
{
methodName
=
path
.
replace
(
"/"
,
""
)
;
}
}
if
(
exposedMethods
.
containsKey
(
className
)
)
{
HashMapString
,
Method
>
methodHashMap
=
exposedMethods
.
get
(
className
)
;
if
(
methodHashMap
!=
null
&
methodHashMap
.
size
(
)
!=
0
&&
methodHashMap
.
containsKey
(
methodName
)
)
{
Method
method
=
methodHashMap
.
get
(
methodName
)
;
if
(
method
!=
null
)
{
try
{
method
.
invoke
(
null
,
webView
,
new
JSONObject
(
param
)
,
new
Callback
(
webView
,
port
)
)
;
}
catch
(
Exception
e
)
{
e
.
printStackTrace
(
)
;
}
}
}
}
return
null
;
}
|
看到该方法中使用了 new Callback(webView, port)进行新建对象,该对象就是用来回调js中回调方法的java对应的类。这个类你需要将js传来的port传进来之外,还需要将WebView的引用传进来,因为要使用到WebView的loadUrl方法,为了防止内存泄露,这里使用弱引用。如果你需要回调js的callback,在对应的方法里调用一下callback.apply()方法将返回数据传入即可,
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
|
public
class
Callback
{
private
static
Handler
mHandler
=
new
Handler
(
Looper
.
getMainLooper
(
)
)
;
private
static
final
String
CALLBACK_JS_FORMAT
=
"javascript:JSBridge.onFinish('%s', %s);"
;
private
String
mPort
;
private
WeakReference
mWebViewRef
;
public
Callback
(
WebView
view
,
String
port
)
{
mWebViewRef
=
new
WeakReference
(
view
)
;
mPort
=
port
;
}
public
void
apply
(
JSONObject
jsonObject
)
{
final
String
execJs
=
String
.
format
(
CALLBACK_JS_FORMAT
,
mPort
,
String
.
valueOf
(
jsonObject
)
)
;
if
(
mWebViewRef
!=
null
&
mWebViewRef
.
get
(
)
!=
null
)
{
mHandler
.
post
(
new
Runnable
(
)
{
@Override
public
void
run
(
)
{
mWebViewRef
.
get
(
)
.
loadUrl
(
execJs
)
;
}
}
)
;
}
}
}
|
唯一需要注意的是apply方法我把它扔在主线程执行了,为什么呢,因为暴露给js的方法可能会在子线程中调用这个callback,这样的话就会报错,所以我在方法内部将其切回主线程。
编码完成的差不多了,那么就剩实现IBridge即可了,我们来个简单的,就来显示Toast为例好了,显示完给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
|
public
class
BridgeImpl
implements
IBridge
{
public
static
void
showToast
(
WebView
webView
,
JSONObject
param
,
final
Callback
callback
)
{
String
message
=
param
.
optString
(
"msg"
)
;
Toast
.
makeText
(
webView
.
getContext
(
)
,
message
,
Toast
.
LENGTH_SHORT
)
.
show
(
)
;
if
(
null
!=
callback
)
{
try
{
JSONObject
object
=
new
JSONObject
(
)
;
object
.
put
(
"key"
,
"value"
)
;
object
.
put
(
"key1"
,
"value1"
)
;
callback
.
apply
(
getJSONObject
(
0
,
"ok"
,
object
)
)
;
}
catch
(
Exception
e
)
{
e
.
printStackTrace
(
)
;
}
}
}
private
static
JSONObject
getJSONObject
(
int
code
,
String
msg
,
JSONObject
result
)
{
JSONObject
object
=
new
JSONObject
(
)
;
try
{
object
.
put
(
"code"
,
code
)
;
object
.
put
(
"msg"
,
msg
)
;
object
.
putOpt
(
"result"
,
result
)
;
return
object
;
}
catch
(
JSONException
e
)
{
e
.
printStackTrace
(
)
;
}
return
null
;
}
}
|
你可以往该类中扔你需要的方法,但是必须是public static void且参数列表满足条件,这样才能找到该方法。
不要忘记将该类注册进去
1
|
JSBridge
.
register
(
"bridge"
,
BridgeImpl
.
class
)
;
|
进行一下简单的测试,将之前实现好的JSBridge.js文件扔到assets目录下,然后新建index.html,输入
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
|
html
>
head
>
meta
charset
=
"utf-8"
>
title
>
JSBridgetitle
>
meta
name
=
"viewport"
content
=
"width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1, user-scalable=no"
/
>
script
src
=
"file:///android_asset/JSBridge.js"
type
=
"text/javascript"
>
script
>
script
type
=
"text/javascript"
>
script
>
style
>
style
>
head
>
body
>
div
>
h3
>
JSBridge
测试
h3
>
div
>
ul
class
=
"list"
>
li
>
div
>
button
onclick
=
"JSBridge.call('bridge','showToast',{'msg':'Hello JSBridge'},function(res){alert(JSON.stringify(res))})"
>
测试
showToast
button
>
div
>
li
>
br
/
>
ul
>
body
>
html
>
|
很简单,就是按钮点击时调用JSBridge.call()方法,回调函数是alert出返回的结果。
接着就是使用WebView将该index.html文件load进来测试了
1
|
mWebView
.
loadUrl
(
"file:///android_asset/index.html"
)
;
|
效果如下图所示
可以看到整个过程都走通了,然后我们测试下子线程回调,在BridgeImpl中加入测试方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
static
void
testThread
(
WebView
webView
,
JSONObject
param
,
final
Callback
callback
)
{
new
Thread
(
new
Runnable
(
)
{
@Override
public
void
run
(
)
{
try
{
Thread
.
sleep
(
3000
)
;
JSONObject
object
=
new
JSONObject
(
)
;
object
.
put
(
"key"
,
"value"
)
;
callback
.
apply
(
getJSONObject
(
0
,
"ok"
,
object
)
)
;
}
catch
(
InterruptedException
e
)
{
e
.
printStackTrace
(
)
;
}
catch
(
JSONException
e
)
{
e
.
printStackTrace
(
)
;
}
}
}
)
.
start
(
)
;
}
|
在index.html中加入
1
2
3
4
5
6
7
8
9
10
|
ul
class
=
"list"
>
li
>
div
>
button
onclick
=
"JSBridge.call('bridge','testThread',{},function(res){alert(JSON.stringify(res))})"
>
测试子线程回调
button
>
div
>
li
>
br
/
>
ul
>
|
理想的效果应该是3秒钟之后回调弹出alert显示
很完美,代码也不多,就实现了功能。如果你需要使用到生成环境中去,上面的代码你一定要再自己封装一下,因为我只是简单的实现了功能,其他因素并没有考虑太多。
当然你也可以参考一个开源的实现
Safe Java-JS WebView Bridge
最后还是惯例,贴上代码
http://download.csdn.net/detail/sbsujjbcy/9446915
问啊-定制化IT教育平台,牛人一对一服务,有问必答,开发编程社交头条 官方网站:www.wenaaa.com
QQ群290551701 聚集很多互联网精英,技术总监,架构师,项目经理!开源技术研究,欢迎业内人士,大牛及新手有志于从事IT行业人员进入!