public static void webViewLoadLocalJs(WebView view, String path) {
String jsContent = assetFile2Str(view.getContext(), path);
view.loadUrl("javascript:" + jsContent);
负责通讯的js片段以文件形式放在asserts目录下,assetFile2Str将文件以流的方式读取出来转换成字符串格式,然后通过loadUrl就加载了通讯的js片段,js的具体内容下面再一一分析。
至此,通讯的准备工作就完成了,下面来看看具体的通讯实现。
js调用native有个前提条件,就是上面讲到的js片段必须注入完毕,所以这里有部分逻辑是对该部分准备工作的监听,负责通讯的js通过发送事件的方式告诉调用方js:
var readyEvent = doc.createEvent('Events');
readyEvent.initEvent('WebViewJavascriptBridgeReady');
readyEvent.bridge = WebViewJavascriptBridge;
doc.dispatchEvent(readyEvent);
调用方js可以通过如下方式来监听该事件
function connectWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) {
// 准备完毕,可以通讯了
} else {
document.addEventListener(
'WebViewJavascriptBridgeReady'
, function() {
// 准备完毕,可以通讯了
false
监听到桥js加载完毕(步骤1),接下来就可以通讯了,调用方js通过调用桥js的callHandler函数来发起通讯(步骤2),来看一个获取H5所运行的设备的信息例子:
function getDeviceInfo(){
window.WebViewJavascriptBridge.callHandler(
'NativeHandler'
, {'funcName' : 'getDeviceInfo'}
, function(responseData) {
document.getElementById("show").innerHTML = "getDeviceInfo involved,responseData from native is " + responseData
各个参数的作用后面会讲到,先来看一下callHandler的实现
function callHandler(handlerName, data, responseCallback) {
_doSend({
handlerName: handlerName,
data: data
}, responseCallback);
callHandler调用了内部doSend方法
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message.callbackId = callbackId;
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
dosend方法先是判断是否需要有结果回调,如果有则生成唯一的回调id并将该id与回调方法一起保存进map以便调用结束后通知回调方法。回调id的生成用一个自增长的整数和当前时间戳组成。唯一的id会被放进传递给native的message对象中,使得native执行完毕之后可以告诉js应该将结果返回给哪个id对应的回调方法。
需要注意的是,dosend的方法并没有直接将调用者发送来的数据告诉native,而是将message对象保存进待发送消息队列sendMessageQueue,然后改变iframe标签的src来通知native有消息要取,这是怎么做到的呢?
首先这里要简单介绍下iframe:
iframe 标签规定一个内联框架
一个内联框架被用来在当前 HTML 文档中嵌入另一个文档
iframe能在当前页面加载其他的页面,也就是说制定了其src之后会触发WebViewClient的shouldOverrideUrlLoading方法。
看一下iframe的初始化
var doc = document;
_createQueueReadyIframe(doc);
function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement('iframe');
messagingIframe.style.display = 'none';
doc.documentElement.appendChild(messagingIframe);
在documentElement上增加了一个不可见的iframe标签,使用该标签来触发通讯。那么究竟是如何通讯的呢?
看一下native的WebViewClient的shouldOverrideUrlLoading方法(步骤3)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith(JsBridgeUtil.YY_RETURN_DATA)) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
LogUtil.e("decode url fail:" + url, e);
mWebView.handlerReturnData(url);
return true;
} else if (url.startsWith(JsBridgeUtil.YY_OVERRIDE_SCHEMA)) {
try {
url = URLDecoder.decode(url, "UTF-8");
} catch (UnsupportedEncodingException e) {
LogUtil.e("decode url fail:" + url, e);
mWebView.flushMessageQueue();
return true;
} else {
return super.shouldOverrideUrlLoading(view, url);
其中JsBridgeUtil.YY_RETURN_DATA 为 ”yy://return/“这部分用来处理从js端获取到的返回数据。
JsBridgeUtil.YY_OVERRIDE_SCHEMA为”yy://“ 刚刚桥js的doSend方法改变的iframe的src为CUSTOM_PROTOCOL_SCHEME + ‘: //’ + QUEUE_HAS_MESSAGE为”yy://fetchQuere“,会匹配仅第二个条件,并执行flushMessageQueue方法(步骤4)
public void flushMessageQueue() {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
loadUrl(JsBridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override
public void onCallBack(String data) {
// callback detail ignored
flushMessageQueue主要做了两件事,一是判断当前线程是否是主线程,只有主线程才会继续执行后面的逻辑。二是调用loadUrl方法,loadUrl方法需要两个参数,第一个是要加载url地址,第二个是一个回调,回调的内容暂且不关注,后面再说。继续看loadUrl方法
public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
this.loadUrl(jsUrl);
responseCallbacks.put(JsBridgeUtil.parseFunctionName(jsUrl), returnCallback);
public static String parseFunctionName(String jsUrl) {
return jsUrl.replace("javascript:WebViewJavascriptBridge.", "").replaceAll("\\(.*\\);", "");
上面部分逻辑一方面执行js的调用,另一方面将回调与该js的调用关联起来。调用loadUrl的时候传入的JsBridgeUtil.JS_FETCH_QUEUE_FROM_JAVA实际为"javascript:WebViewJavascriptBridge._fetchQueue();",由此可以知道现在调用的是桥js的_fetchQueue()方法,那我们接着去分析_fetchQueue(步骤5)
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
//android can't read directly the return data, so we can reload iframe src to communicate with java
bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
_fetchQueue逻辑很简单,只是将前面放到messageQueue里面的消息转变成json格式并将queue置空,然后再次按照定义好的规则拼接url放置到iframe上去,这次拼接出来的内容大致是 ”yy://return/_fetchQueue/{jsonData}“,一定看上去很眼熟吧,对这次会进入到shouldOverrideUrlLoading的第一个条件并执行webview的handlerReturnData方法。(步骤6)
public void handlerReturnData(String url) {
String functionName = JsBridgeUtil.getFunctionFromReturnUrl(url);
CallBackFunction f = responseCallbacks.get(functionName);
String data = JsBridgeUtil.getDataFromReturnUrl(url);
if (f != null) {
f.onCallBack(data);
responseCallbacks.remove(functionName);
public static String getFunctionFromReturnUrl(String url) {
String temp = url.replace(YY_RETURN_DATA, EMPTY_STR); //YY_RETURN_DATA="yy://return/"
String[] functionAndData = temp.split(SPLIT_MARK); //SPLIT_MARK="/"
if (functionAndData.length >= 1) {
return functionAndData[0];
return null;
handlerReturnData先是通过操作字符串获取方法名,在这里获取到的正是_fetchQueue ,可想而知从回调map中取出key为_fetchQueue的回调正式刚才调用fetchQueue方法时我们忽略具体实现的那个回调,现在让我们来看下这个回调到底干了什么:(步骤7)
@Override
public void onCallBack(String data) {
// deserializeMessage 反序列化消息
List<Message> list = null;
try {
list = Message.toArrayList(data);
} catch (Exception e) {
e.printStackTrace();
return;
if (list == null || list.size() == 0) {
return;
for (int i = 0; i < list.size(); i++) {
Message m = list.get(i);
String responseId = m.getResponseId();
// 是否是response CallBackFunction
if (!TextUtils.isEmpty(responseId)) {
CallBackFunction function = responseCallbacks.get(responseId);
String responseData = m.getResponseData();
function.onCallBack(responseData);
responseCallbacks.remove(responseId);
} else {
CallBackFunction responseFunction = null;
// if had callbackId 如果有回调Id
final String callbackId = m.getCallbackId();
if (!TextUtils.isEmpty(callbackId)) {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
Message responseMsg = new Message();
responseMsg.setResponseId(callbackId);
responseMsg.setResponseData(data);
queueMessage(responseMsg);
} else {
responseFunction = new CallBackFunction() {
@Override
public void onCallBack(String data) {
// do nothing
// BridgeHandler执行
BridgeHandler handler;
if (!TextUtils.isEmpty(m.getHandlerName())) {
handler = messageHandlers.get(m.getHandlerName());
} else {
handler = defaultHandler;
if (handler != null) {
handler.handler(m.getData(), responseFunction);
首先将js传过来的json格式的字符串转换为message列表,如果列表不为空则挨个处理,处理过程大概如下:如果有repsponseId则说明是native调用js的返回结果,由于我们分析的是js调用native所以这条分路暂时不管,看下另外一条,如果callBackId不为空生成一个新的callBackFunction,并将callBackId在返回数据中一并返回,目的是为了方便js找到该callBackId对应的调用方js的callBackFunction,如果callBackId为空则说明这次请求不需要结果回调,则生成一个空实现的回调。这里callBackId是否为空是由最初调用方js决定的,可以参考 步骤2 处获取设备信息的例子。
接着会根据js设置的 BridgeHandler的名字去匹配对应的BridgeHandler,要调用的BridgeHandler的名字也是在最初调用方js那里确定好的,在 步骤2 处例子是 ”NativeHandler“。
那么这些BridgeHandler是在什么时候又是怎样注册进来的呢?这个问题比较简单,注册时机一般在使用webview的activity或者fragment对webview进行初始化的时候,注册方法更简单这里不浪费口舌。
假如根据handlerName能匹配到对应的BridgeHandler(一般都能匹配到,因为BridgeHandler的name都是两端提前商量好的),则执行BridgeHandler的handler方法,然后handler方法会根据事先定义好的规则匹配要执行的方法和传入的参数(如果有的话),执行完之后回调刚才根据callBackId是否为空生成的CallBackFunction (步骤8),例如:
@Override
public void handler(String data, CallBackFunction function) {
//获取js端要调用的方法名
String funcName = getFunctionName(data);
switch (funcName) {
case "getDeviceInfo":
function.onCallBack(new Gson().toJson(new DeviceInfo()));
break;
case "xxx":
default:
function.onCallBack("no such function named: " + funcName);
break;
BridgeHandler将处理结果通过执行CallBackFunction的onCallBack方法回传给js,通过步骤7中onCallBack的具体实现我们知道具体传递工作交给了queueMessage方法,queueMessage又调用了dispatchMessage方法,源码如下
private void queueMessage(Message m) {
if (startupMessage != null) {
startupMessage.add(m);
} else {
dispatchMessage(m);
void dispatchMessage(Message m) {
String messageJson = m.toJson();
// 为json字符串转义特殊字符
messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
String javascriptCommand = String.format(MyBridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
// 必须要找主线程才会将数据传递出去
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
this.loadUrl(javascriptCommand);
可见执行结果通过一些格式处理字符转义之后,以”javascript:WebViewJavascriptBridge._handleMessageFromNative(’{resultData}’);“的形式调用桥接js的_handleMessageFromNative方法(步骤9),
function _handleMessageFromNative(messageJSON) {
console.log(messageJSON);
if (receiveMessageQueue) {
receiveMessageQueue.push(messageJSON);
_dispatchMessageFromNative(messageJSON);
_handleMessageFromNative在将处理结果直接放进messageQueue之后执行了_dispatchMessageFromNative
function _dispatchMessageFromNative(messageJSON) {
setTimeout(function() {
var message = JSON.parse(messageJSON);
var responseCallback;
//java call finished, now need to call js callback function
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) {
return;
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
} else {
//直接发送
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function(responseData) {
_doSend({
responseId: callbackResponseId,
responseData: responseData
var handler = WebViewJavascriptBridge._messageHandler;
if (message.handlerName) {
handler = messageHandlers[message.handlerName];
//查找指定handler
try {
handler(message.data, responseCallback);
} catch (exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
这段会看着非常眼熟,因为他的逻辑跟 步骤7 中CallBackFunction的实现如出一辙,这也可以理解,因为通讯是双向的,自然两边的实现逻辑也就一致,只是实现的语言不一样。
有了上面 步骤7 的分析,这段就好理解多了,因为 步骤7 中内部生成的CallBackFunction为Message指定了responseId 为js传过去的callBackId,那么我们也就能根据这个callBackId对应到 步骤2 中存入responseCallbacks中的responseCallback,也就是调用方js定义的callBack,在获取设备信息的例子汇总该callBack就是
function(responseData) {
document.getElementById("show").innerHTML = "getDeviceInfo involved,responseData from native is " + responseData
至此,js调用native的过程我们就分析完了。调用过程在js和native之间来回转换,会有些绕,用图表的方式总结一下可能会更清晰一些:
通讯前准备通讯的实现需要注入一段js代码,js代码的注入在页面加载完毕也就是WebViewClent的onPageFinished方法中@Override public void onPageFinished(WebView view, String url) { super.onPageFinished(mWebView, url); //加载本地通讯...
JSBrige系列直通车,由浅入深理解JS-Native的通信过程:
JSbridge系列解析(一):JS-Native调用方法
JSbridge系列解析(二):lzyzsd/JsBridge使用方法
JSbridge系列解析(三):lzyzsd/JsBridge源码解析
JSbridge系列解析(四):Web端发消息给Native代码流程具体分析...
#import "ExampleUIWebViewController.h"
#import "WebViewJavascriptBridge.h"@interface ExampleUIWebViewController ()
@property WebViewJavascriptBridge* bridge;
@end@implementation ExampleUIWebViewControl
h5和原生结合开发app越来越流行。其实就是webview 的js调用native的方法。也就是需要搭建一个桥。这样的桥早就有人搭建好了,那就是jsbridge。
git地址:
https://github.com/lzyzsd/JsBridge.git
其实很简单,那些添加依赖我就不说了。
两种情况:
1.js调用本地的方法:
webView.loa...
该插件公开以下方法:
cordova . plugins . jsCallNative . run ( success , error ) ;
you should define a function called "sayHi()" in the "index.html"
when you call cordova.plugins.jsCallNative.run, it will call sayHi() function, also, we can do something in the native when you call the plugin.
// when you do something, like demo button click event
在混合开发中,很多情况下我们需要和native进行交互。那么我们怎么和native进行交互呢。。我自己也是研究了很久,最终搞好了一套稳定的,记下当笔记,遇到相同问题的筒子门可以参考一下
1.url传参,在一些简单的页面开发中,可以通过url?key1=value1&key2=value2 进行一些简单的数据交互,但是这种交互是单项的。
而且native端筒子很烦躁处理每一条跳转。
2.hy...
# Build, watch for changes and run the application
tns run
# Build, watch for changes and debug the application
tns debug < platform>
# Build for production
tns build < platform> --env.production
```javascript
import React, { useState } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Image, ScrollView } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import Constants from 'expo-constants';
const ImagePickerComponent = () => {
const [selectedImages, setSelectedImages] = useState([]);
const pickImage = async () => {
let result = await ImagePicker.launchCameraAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
if (!result.cancelled) {
setSelectedImages([...selectedImages, { uri: result.uri }]);
return (
<View style={styles.container}>
<TouchableOpacity style={styles.button} onPress={pickImage}>
<Text style={styles.buttonText}>Take a photo</Text>
</TouchableOpacity>
<ScrollView horizontal={true}>
{selectedImages.map((image, index) => (
<View key={index}>
<Image source={{ uri: image.uri }} style={styles.image} />
</View>
</ScrollView>
</View>
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
button: {
alignItems: 'center',
backgroundColor: '#DDDDDD',
padding: 10,
marginBottom: 10,
buttonText: {
fontSize: 20,
image: {
width: 100,
height: 100,
margin: 5,
export default ImagePickerComponent;
这个组件使用了 `expo-image-picker` 库来打开相机拍照,并将拍摄的图片加入到一个数组中。这个数组中的每一个元素都是一个对象,包含了图片的 URI。然后,使用 `ScrollView` 组件将所有的图片展示出来。