转载

JsBridge源码分析

参考:

Android 与 JS 之 JsBridge 使用与源码分析

今天分析的是大头鬼的 https://github.com/lzyzsd/JsBridge , 废话不多说, 先从一个例子开始.

用法简析

JsBridge源码分析 在h5页面中有一个”调用相机”的按钮, 点击按钮会传递用户的id给原生方法, 同时调起相机拍照.拍照完之后将图片上传到服务器, 这一步网路请求会有校验所以需要用户id这个参数.将图片上传完成之后原生再将服务器返回的图片地址传递给js, h5拿到图片地址之后将图片显示到页面上.当然为了简化, 我这里只是用了一些假的代码来模拟这些操作.java代码如下:

public class WebActivity extends Activity {
    private static final String TAG = "WebActivity";
    private BridgeWebView mBridgeWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.web_activity);
        mBridgeWebView = (BridgeWebView) findViewById(R.id.web_view);
        mBridgeWebView.loadUrl("file:///android_asset/web.html");
        mBridgeWebView.registerHandler("takePhoto", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "收到js的消息 = " + data);
                String avatarUrl = getAvatarByUserId(data);
                String msg = "用户的applyId是 = " + data;
                Toast.makeText(WebActivity.this, msg, Toast.LENGTH_SHORT).show();
                function.onCallBack(avatarUrl);
            }
        });


    }
    
    // 假装拍照获取照片, 然后将拍的照片上传到服务器
    // 上传到服务器需要用到用户的userId
    // 然后将服务器返回的用户的头像链接再返回给js, 设置到h5上
    private String getAvatarByUserId(String userId) {
        String url = null;
        if (TextUtils.equals("1234", userId)) {
            url = "http://ww3.sinaimg.cn/mw690/96a29af5jw8fdfu43tnvlj20ro0rotab.jpg";
        }
        return url;
    }
    

}

web.html的代码如下:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html;charset=gb2312">
    <script type="text/javascript">

        function jsCallJava() {
            window.WebViewJavascriptBridge.callHandler(
                'takePhoto'
                , 1234
                , function (img_url) {
                    document.getElementById("content").src = img_url
                }
            );
        }

    </script>
</head>
<body>
<div>
    <img id="content" width="100" height="100">
</div>
<br/>
<input type="button" value="调用相机" onclick="jsCallJava()"/>
<br/>
</body>
</html>

注意到html代码中callHandler方法的第一个参数’takePhoto’和java代码中registerHandler中的第一个参数”takePhoto”是一样的.另外, assets文件夹中还有一个”WebViewJavascriptBridge.js”的js文件也是不可或缺的.如果是实际使用这套方案, 应该将这个文件交给前端人员去调用.

JsBridge源码分析

源码分析

现在从源码, 一步步分析上面这个过程的代码调用流程.

首先从js代码开始, 点击”调用相机”按钮, 会调用jsCallJava()方法.在这个方法里, 调用了WebViewJavascriptBridge的callHandler方法

function jsCallJava() {
    window.WebViewJavascriptBridge.callHandler(
        'takePhoto'
        , 1234
        , function (img_url) {
            document.getElementById("content").src = img_url
        }
    );
}
function callHandler(handlerName, data, responseCallback) {
    _doSend({
        handlerName: handlerName,
        data: data
    }, responseCallback);
}

callHandler方法会调用_doSend方法

// sendMessage add message, 触发native处理 sendMessage
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        responseCallbacks[callbackId] = responseCallback;
        message.callbackId = callbackId;
    }

    sendMessageQueue.push(message);
    // yy://__QUEUE_MESSAGE__/
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

首先responseCallback是不为空的, 所以会走if里面的语句.首先会生成一个唯一的callbackId(避免重复), 类似 cb_1_1533191074448 这样的, 然后以callbackId为key, 回调函数为value, 放在responseCallbacks这个对象里, 同时在message这个对象里也存一份.然后再往sendMessageQueue这个数组里push一个message对象.接着会变更messagingIframe元素的的src属性.在这里, messagingIframe是一个iframe元素

function _createQueueReadyIframe(doc) {
    messagingIframe = doc.createElement('iframe');
    messagingIframe.style.display = 'none';
    doc.documentElement.appendChild(messagingIframe);
}

iframe元素的src属性变更后,java部分触发shouldOverrideUrlLoading 方法, 这部分代码在BridgeWebViewClient里面

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    try {
        url = URLDecoder.decode(url, "UTF-8");
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
        webView.handlerReturnData(url);
        return true;
    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
        webView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}

这里会走第二个if, 调用BridgeWebView的flushMessageQueue()方法

   /**
    * 刷新消息队列
    */
void flushMessageQueue() {
	if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
		loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

			@Override
			public void onCallBack(String data) {
			    ...
			}
		});
	}
}

在这个flushMessageQueue方法里, 如果当前是主线程就调用一个loadUrl方法

public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
    // jsUrl = "javascript:WebViewJavascriptBridge._fetchQueue();"
	this.loadUrl(jsUrl);
       // 添加至 Map<String, CallBackFunction>
	String functionName = BridgeUtil.parseFunctionName(jsUrl);
	// functionName = "_fetchQueue"
	responseCallbacks.put(functionName, returnCallback);
}

在这个方法里, 首先会调用WebViewJavascriptBridge的_fetchQueue()方法, 然后解析方法名字, 因为这里的方法名字是写死的, 其实就是 _fetchQueue , 请记住这个名字, 因为后面会用到 .然后将以这个_fetchQueue为key, 回调方法为value, 放到一个map里面.然后我们再去看js那端的方法.

// 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
function _fetchQueue() {
    // [{"handlerName":"takePhoto","data":1234,"callbackId":"cb_1_1532853343154"}]
    var messageQueueString = JSON.stringify(sendMessageQueue);
    console.log('messageQueueString = ' + messageQueueString);
    sendMessageQueue = [];
    // android can't read directly the return data, so we can reload iframe src to communicate with java
    var src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
    console.log('src = ' + src);
    // yy://return/_fetchQueue/%5B%7B%22handlerName%22%3A%22takePhoto%22%2C%22data%22%3A1234%2C%22callbackId%22%3A%22cb_1_1532853343154%22%7D%5D
    messagingIframe.src = src;
}

sendMessageQueue这个数组我们在_doSend()方法中用到过, 里面push了一个message对象, json格式化之后字符串就是 [{"handlerName":"takePhoto","data":1234,"callbackId":"cb_2_1532852750705"}] 这样的, 然后将sendMessageQueue这个数组置空, 接着再次变更iframe的src属性, 触发java的shouldOverrideUrlLoading方法, 传递的url是类似 yy://return/_fetchQueue/%5B%7B%22handlerName%22%3A%22takePhoto%22%2C%22data%22%3A1234%2C%22callbackId%22%3A%22cb_1_1532853343154%22%7D%5D 这样的.

现在又回到java端的方法了

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    Log.d(TAG, "url = " + url);
    try {
        url = URLDecoder.decode(url, "UTF-8");
        Log.d(TAG, "decodedUrl = " + url);
    } catch (UnsupportedEncodingException e) {
        e.printStackTrace();
    }

    if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
        webView.handlerReturnData(url);
        return true;
    } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
        webView.flushMessageQueue();
        return true;
    } else {
        return super.shouldOverrideUrlLoading(view, url);
    }
}

首先将url解码, 这里会走第一个if, 调用handlerReturnData(url)方法

   /**
    * 获取到CallBackFunction data执行调用并且从数据集移除
    * @param url
    */
void handlerReturnData(String url) {
    // _fetchQueue
	String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
	CallBackFunction f = responseCallbacks.get(functionName);
	String data = BridgeUtil.getDataFromReturnUrl(url);
	if (f != null) {
		f.onCallBack(data);
		responseCallbacks.remove(functionName);
		return;
	}
}

在这里会在url中解析出方法名和传递的数据, 方法名就是_fetchQueue .先根据方法名从Map中取出回调方法, 然后给回调方法传递数据, 然后调用回调方法, 完成之后Map中移除这个回调方法.

接着我们看这个回调方法中的逻辑, 也就是flushMessageQueue中的回调逻辑

void flushMessageQueue() {
	if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
		loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

			@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);
						}
					}
				}
			}
		});
	}
}

首先将数据解析成一个Message的list, 数据是类似 [{"handlerName":"takePhoto","data":1234,"callbackId":"cb_3_1532855579967"}] 这样的.

这个Message是自定义的类, 有callbackId, responseId, responseData, data, handlerName这几个成员变量.接着遍历这个list中的每一个Message, 因为没有responseId, 并且callbackId不为空, 所以会生成一个CallBackFunction, 这里面的逻辑我们暂时不看.接着会根据Message中的handlerName, 从messageHandlers中获取一个BridgeHandler对象, 这个对象是我们在WebActivity中注册放到messageHandlers这个map里的, 看一下代码

   mBridgeWebView.registerHandler("takePhoto", new BridgeHandler() {
       @Override
       public void handler(String data, CallBackFunction function) {
           Log.i(TAG, "收到js的消息 = " + data);
           String avatarUrl = getAvatarByUserId(data);
           String msg = "用户的applyId是 = " + data;
           Toast.makeText(WebActivity.this, msg, Toast.LENGTH_SHORT).show();
           function.onCallBack(avatarUrl);
       }
   });
   
public void registerHandler(String handlerName, BridgeHandler handler) {
	if (handler != null) {
           // 添加至 Map<String, BridgeHandler>
		messageHandlers.put(handlerName, handler);
	}
}

在这里我们就能明白, 为什么java代码中的方法名字和js代码中的方法名字都要取做takePhoto

再回过来, 然后我们就调用这个BridgeHandler的handler方法, 传递的参数一个是Message中的data, 另一个是上面的CallBackFunction.

接着在handler方法中, 我们先做了一些自定义的操作, 然后调用了CallBackFunction的onCallBack方法, 参数是一张图片的url.

看一下这个onCallBack方法中的逻辑

responseFunction = new CallBackFunction() {
	@Override
	public void onCallBack(String data) {
		Message responseMsg = new Message();
		responseMsg.setResponseId(callbackId);
		responseMsg.setResponseData(data);
		queueMessage(responseMsg);
	}
};

创建了一个responseMsg, 然后调用了queueMessage()方法

private void queueMessage(Message m) {
	if (startupMessage != null) {
		startupMessage.add(m);
	} else {
		dispatchMessage(m);
	}
}

这里会走else, 接着看dispatchMessage()方法

   /**
    * 分发message 必须在主线程才分发成功
    * @param m Message
    */
void dispatchMessage(Message m) {
       String messageJson = m.toJson();
       //escape special characters for json string  为json字符串转义特殊字符
       messageJson = messageJson.replaceAll("(////)([^utrn])", "////////$1$2");
       messageJson = messageJson.replaceAll("(?<=[^////])(/")", "/////"");
	messageJson = messageJson.replaceAll("(?<=[^////])(/')", "/////'");
	// javascript:WebViewJavascriptBridge._handleMessageFromNative('{/"responseData/":/"http:////////ww3.sinaimg.cn////mw690////96a29af5jw8fdfu43tnvlj20ro0rotab.jpg/",/"responseId/":/"cb_4_1532856634427/"}');
       String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
       // 必须要找主线程才会将数据传递出去 --- 划重点
       if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
           this.loadUrl(javascriptCommand);
       }
   }

首先将这个Message转化成json格式的字符串, 去掉一些特殊字符, 然后再主线程调用js方法, 方法是WebViewJavascriptBridge._handleMessageFromNative方法, 然后我们去看一下这个代码

// 提供给native调用,receiveMessageQueue 在会在页面加载完后赋值为null,所以
function _handleMessageFromNative(messageJSON) {
    console.log(messageJSON);
    if (receiveMessageQueue && receiveMessageQueue.length > 0) {
        receiveMessageQueue.push(messageJSON);
    } else {
        _dispatchMessageFromNative(messageJSON);
    }
}

这里receiveMessageQueue是空的, 所以会走else

// 提供给native使用,
function _dispatchMessageFromNative(messageJSON) {
    setTimeout(function() {
        // {"responseData":"http:////ww3.sinaimg.cn//mw690//96a29af5jw8fdfu43tnvlj20ro0rotab.jpg","responseId":"cb_1_1532864396479"}
        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);
                }
            }
        }
    });
}

首先这里的messageJSON是类似 {"responseData":"http:////ww3.sinaimg.cn//mw690//96a29af5jw8fdfu43tnvlj20ro0rotab.jpg","responseId":"cb_1_1532864396479"} 这样的, 所以responseId是有的, 接着根据responseId从responseCallbacks中取出responseCallback, 这个responseCallback是在_doSend()方法中存进去的, 也就是一开始js代码中callHandler方法中传进去的一个方法.将message.responseData传递给这个方法, 执行完之后从responseCallbacks这个对象里删除responseCallback方法.至于responseCallback里的方法具体是什么逻辑就是将图片地址设置到页面上了

function jsCallJava() {
    window.WebViewJavascriptBridge.callHandler(
        'takePhoto'
        , 1234
        , function (img_url) {
            document.getElementById("content").src = img_url
        }
    );
}

到这里, js调用java代码的一次完整流程就分析完了, 还是有点绕的.

最后盗一张图总结一下

JsBridge源码分析

一次流程估计不容易记住, 多看几遍就顺了.

结语

最后是我对于这个jsbridge的看法, 首先优点是封装的比较方便, java端和js端的代码写起来都更加方便了.可以改进的地方就是java调用js可以分版本使用loadUrl()或者evaluateJavascript()方法, 然后js调用java我更喜欢在onJsPrompt()中回调.最后一点是js调用java没有办法全局调用, 每个webview都需要注册一个handler, 然后单独在里面写逻辑.不过这一点其实不算什么缺点, 毕竟每个项目的要求都不一样, 不能强求.

原文  https://mundane799699.github.io/2018/08/02/jsbridge-analysis/
正文到此结束
Loading...