Open CodingMeUp opened 4 years ago
Hybrid 方案是基于 WebView 的,JavaScript 执行在 WebView 的 Webkit 引擎中。因此,Hybrid 方案中 JSBridge 的通信原理会具有一些 Web 特性。
JavaScript 调用 Native 的方式,主要有两种:注入 API 和 拦截 URL SCHEME
// 对于 iOS 的 UIWebView,实例如下:
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
context[@"postBridgeMessage"] = ^(NSArray<NSArray > calls) {
// Native 逻辑
};
// iOS 的 WKWebView
@interface WKWebVIewVC ()
(void)viewDidLoad { [super viewDidLoad];
WKWebViewConfiguration configuration = [[WKWebViewConfiguration alloc] init]; configuration.userContentController = [[WKUserContentController alloc] init]; WKUserContentController userCC = configuration.userContentController; // 注入对象,前端调用其方法时,Native 可以捕获到 [userCC addScriptMessageHandler:self name:@"nativeBridge"];
WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
// TODO 显示 WebView }
(void)userContentController:(WKUserContentController )userContentController didReceiveScriptMessage:(WKScriptMessage )message { if ([message.name isEqualToString:@"nativeBridge"]) { NSLog(@"前端传递的数据 %@: ",message.body); // Native 逻辑 } } // 安卓 public class JavaScriptInterfaceDemoActivity extends Activity { private WebView Wv;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
Wv = (WebView)findViewById(R.id.webView);
final JavaScriptInterface myJavaScriptInterface = new JavaScriptInterface(this);
Wv.getSettings().setJavaScriptEnabled(true);
Wv.addJavascriptInterface(myJavaScriptInterface, "nativeBridge");
// TODO 显示 WebView
}
public class JavaScriptInterface { Context mContext;
JavaScriptInterface(Context c) {
mContext = c;
}
public void postMessage(String webMessage){
// Native 逻辑
}
} }
// 前端调用
// uiwebview:
window.postBridgeMessage(message);
// wkwebview:
window.webkit.messageHandlers.nativeBridge.postMessage(message);
// 安卓webview:
window.nativeBridge.postMessage(message);
- 拦截 URL SCHEME
先解释一下 URL SCHEME:URL SCHEME是一种类似于url的链接,是为了方便app直接互相调用设计的,形式和普通的 url 近似,主要区别是 protocol 和 host 一般是自定义的,例如: qunarhy://hy/url?url=http://ymfe.tech,protocol 是 qunarhy,host 则是 hy。
拦截 URL SCHEME 的主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。
在时间过程中,这种方式有一定的**缺陷**:
1. 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。
2. 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。
#### Native 调用 JavaScript
相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,毕竟不管是 iOS 的 UIWebView 还是 WKWebView,还是 Android 的 WebView 组件,都以子组件的形式存在于 View/Activity 中,直接调用相应的 API 即可。
Native 调用 JavaScript,其实就是执行拼接 JavaScript 字符串,从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。(闭包里的方法,JavaScript 自己都调用不了,更不用想让 Native 去调用了)
```js
// iOS 的 UIWebView
result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];
// iOS 的 WKWebView
[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler];
// Android,在 Kitkat(4.4)之前并没有提供 iOS 类似的调用方式,只能用 loadUrl 一段 JavaScript 代码, 使用 loadUrl 的方式,并不能获取 JavaScript 执行后的结果
webView.loadUrl("javascript:" + javaScriptString);
// 而 Kitkat 之后的版本,也可以用 evaluateJavascript 方法实现
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) { }
});
通信原理是 JSBridge 实现的核心,实现方式可以各种各样,但是万变不离其宗。推荐的实现方式如下:
对于其他方式,诸如 React Native、微信小程序 的通信方式都与上描述的近似,并根据实际情况进行优化。
以 React Native 的 iOS 端举例:JavaScript 运行在 JSCore 中,实际上可以与上面的方式一样,利用注入 API 来实现 JavaScript 调用 Native 功能。不过 React Native 并没有设计成 JavaScript 直接调用 Object-C,而是为了与 Native 开发里事件响应机制一致,设计成 需要在 Object-C 去调 JavaScript 时才通过返回值触发调用。原理基本一样,只是实现方式不同。
JSBridge 的接口主要功能有两个:
因此,JSBridge 可以设计如下:
window.JSBridge = {
// 调用 Native
invoke: function(msg) {
// 判断环境,获取不同的 nativeBridge
nativeBridge.postMessage(msg);
},
receiveMessage: function(msg) {
// 处理 msg
}
};
RPC 中有一个非常重要的环节是 句柄解析调用 ,这点在 JSBridge 中体现为 句柄与功能对应关系。同时,我们将句柄抽象为 桥名(BridgeName),最终演化为一个 BridgeName 对应一个 Native 功能或者一类 Native 消息 基于此点,JSBridge 的实现可以优化为如下
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, data) {
// 判断环境,获取不同的 nativeBridge
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {}
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {};
// 具体逻辑
}
};
JSBridge 大概的雏形出现了。消息都是单向的,那么对于调用 Native 功能时 JSBridge 的Callback 实现 ,其实就是 RPC 框架的回调机制。当然也可以用更简单的 JSONP 机制解释:
当发送 JSONP 请求时,url 参数里会有 callback 参数,其值是 当前页面唯一 的,而同时以此参数值为 key 将回调函数存到 window 上,随后,服务器返回 script 中,也会以此参数值作为句柄,调用相应的回调函数
由此可见,callback 参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge:用一个自增的唯一 id,来标识并存储回调函数,并把此 id 以参数形式传递给 Native,而 Native 也以此 id 作为回溯的标识。这样,即可实现 Callback 回调逻辑。
(function () {
var id = 0,
callbacks = {},
registerFuncs = {};
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 传到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
responstId = msg.responstId;
// 具体逻辑
// bridgeName 和 callbackId 不会同时存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相应句柄
callbacks[callbackId](msg.data); // 执行调用
}
} else if (bridgeName) {
if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
var ret = {},
flag = false;
registerFuncs[bridgeName].forEach(function(callback) => {
callback(data, function(r) {
flag = true;
ret = Object.assign(ret, r);
});
});
if (flag) {
nativeBridge.postMessage({ // 回调 Native
responstId: responstId,
ret: ret
});
}
}
}
},
register: function(bridgeName, callback) {
if (!registerFuncs[bridgeName]) {
registerFuncs[bridgeName] = [];
}
registerFuncs[bridgeName].push(callback); // 存储回调
}
};
})();
对于 Native 端涉及的并不多。在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑也很简单,主要的代码逻辑是:
与由 Native 端注入正好相反,
RNBridge桥接是什么?
RN可以分成JS 和 Native两部分,这两部分都有自己的线程
线程通信是通过Bridge通信,传输的是JSON信息,其中包含了module id,method id和一些需要的数据。这两边并不能直接地相互感知,也不能共享相同的内存。
很多人认为队列是一个很好的解决方案。你发送JSON/XML队列消息,这个消息遵从相应的协议,并且每个服务都知道怎么去读取和解析成对应的数据和行为。这个队列就很像RN里面的Bridge。
js代码和objective-c之间的通信。RN在ios上使用的是内置的JSCore,在Android需要格外编译一个Js引擎 JS引擎知道如何去更加高效地把JS转换成机器语言。
这也是为什么Android App需要更多的时间去加载,因为需要加载JSCore到项目里。不过在0.60.2之后,RN可以使用Hermes,这对于RN来说是更理想的JS引擎。
RN 桥接bridge怎么工作的
关键概念名词解释
打开App时,每一步发生了什么:
用户点击App的图标
UIManager线程:加载所有的Native库和Native组件比如 Text、Button、Image等
告诉Js线程,Native部分准备好了,Js侧开始加载main.bundle.js,这里面包含了所有的js和react逻辑以及组件。
Js侧通过Bridge发送一条JSON消息到Native侧,告诉Native怎么创建UI。值得一提的是:所有经过Bridge的通信都是异步的,并且是打包发送的。这是为了避免阻塞UI,举个栗子:![avatar](https://user-gold-cdn.xitu.io/2019/12/3/16eca09d44ceb497?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)
Shadow线程最先拿到消息,然后创建UI树
它使用Yoga布局引擎去获取所有基于flex样式的布局,并且转化成Native的布局,宽、高、间距等。
UIManager执行一些操作并且像这样在屏幕上展示UI
RN目前架构的优缺点
RN基于这个架构有以下优点:
但是,有优点肯定也会有缺点。下面我们介绍一下能够解决现有缺点的新架构:Fabric 。 当前架构的缺点是:
Fabric http://facebook.github.io/react-native/blog/2018/06/14/state-of-react-native-2018 Fabric新的概念:JSI,Fabric,Turbo Modules,和 CodeGen。
使用新架构,App启动的流程是怎么样的。
为了完成整个流程,我们几乎做了同样的事情,但是没有了Bridge,现在我们可以有更好的性能,我们可以用同步的方式进行操作,甚至可以对UI上的同步操作进行优先级排序。启动时间也将更快,App也将更小。