乐趣区

iOS- JSBridge的原理

作者:心叶时间:2019-03-25 10:18
原理概述
简介
JSBridge 是 Native 代码与 JS 代码的通信桥梁。目前的一种统一方案是:H5 触发 url scheme->Native 捕获 url scheme-> 原生分析, 执行 -> 原生调用 h5。如下图:

url scheme 介绍
上图中有提到 url scheme 这个概念, 那这到底是什么呢?

url scheme 是一种类似于 url 的链接, 是为了方便 app 直接互相调用设计的

具体为, 可以用系统的 OpenURI 打开一个类似于 url 的链接 (可拼入参数), 然后系统会进行判断, 如果是系统的 url scheme, 则打开系统应用, 否则找看是否有 app 注册这种 scheme, 打开对应 app
需要注意的是, 这种 scheme 必须原生 app 注册后才会生效, 如微信的 scheme 为 (weixin://)

而本文 JSBridge 中的 url scheme 则是仿照上述的形式的一种方式
具体为,app 不会注册对应的 scheme, 而是由前端页面通过某种方式触发 scheme(如用 iframe.src), 然后 Native 用某种方法捕获对应的 url 触发事件, 然后拿到当前的触发 url, 根据定义好的协议, 分析当前触发了那种方法, 然后根据定义来执行等

注意,iOS10 以后,urlscheme 必须符合 url 规范,否则会报错

实现流程
基于上述的基本原理, 现在开始设计一种 JSBridge 的实现
实现思路
要实现 JSBridge, 我们可以进行关键步骤分析

第一步: 设计出一个 Native 与 JS 交互的全局桥对象
第二步:JS 如何调用 Native
第三步:Native 如何得知 api 被调用
第四步: 分析 url- 参数和回调的格式
第五步:Native 如何调用 JS
第六步:H5 中 api 方法的注册以及格式

如下图:
第一步: 设计出一个 Native 与 JS 交互的全局桥对象
我们规定,JS 和 Native 之间的通信必须通过一个 H5 全局对象 JSbridge 来实现, 该对象有如下特点
该对象名为 ”JSBridge”, 是 H5 页面中全局对象 window 的一个属性
var JSBridge = window.JSBridge || (window.JSBridge = {});

该对象有如下方法
– registerHandler(String,Function) H5 调用, 注册本地 JS 方法, 注册后 Native 可通过 JSBridge 调用。调用后会将方法注册到本地变量 messageHandlers 中

– callHandler(String,JSON,Function) H5 调用, 调用原生开放的 api, 调用后实际上还是本地通过 url scheme 触发。调用时会将回调 id 存放到本地变量 responseCallbacks 中

– _handleMessageFromNative(JSON) Native 调用, 原生调用 H5 页面注册的方法, 或者通知 H5 页面执行回调方法

如图

第二步:JS 如何调用 Native
在第一步中, 我们定义好了全局桥对象, 可以我们是通过它的 callHandler 方法来调用原生的, 那么它内部经历了一个怎么样的过程呢?如下:
callHandler 函数内部实现过程
在执行 callHandler 时, 内部经历了以下步骤:

(1) 判断是否有回调函数, 如果有, 生成一个回调函数 id, 并将 id 和对应回调添加进入回调函数集合 responseCallbacks 中
(2) 通过特定的参数转换方法, 将传入的数据, 方法名一起, 拼接成一个 url scheme

//url scheme 的格式如
// 基本有用信息就是后面的 callbackId,handlerName 与 data
// 原生捕获到这个 scheme 后会进行分析
var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data
(3) 使用内部早就创建好的一个隐藏 iframe 来触发 scheme
// 创建隐藏 iframe 过程
var messagingIframe = document.createElement(‘iframe’);
messagingIframe.style.display = ‘none’;
document.documentElement.appendChild(messagingIframe);

// 触发 scheme
messagingIframe.src = uri;
注意点:
注意, 正常来说是可以通过 window.location.href 达到发起网络请求的效果的,但是有一个很严重的问题,就是如果我们连续多次修改 window.location.href 的值,在 Native 层只能接收到最后一次请求,前面的请求都会被忽略掉。所以 JS 端发起网络请求的时候,需要使用 iframe,这样就可以避免这个问题。
第三步:Native 如何得知 api 被调用
在上一步中, 我们已经成功在 H5 页面中触发 scheme, 那么 Native 如何捕获 scheme 被触发呢?
根据系统不同,Android 和 iOS 分别有自己的处理方式
Android 捕获 url scheme
在 Android 中 (WebViewClient 里), 通过 shouldoverrideurlloading 可以捕获到 url scheme 的触发
public boolean shouldOverrideUrlLoading(WebView view, String url){
// 读取到 url 后自行进行分析处理

// 如果返回 false,则 WebView 处理链接 url,如果返回 true,代表 WebView 根据程序来执行 url
return true;
}
另外,Android 中也可以不通过 iframe.src 来触发 scheme,android 中可以通过 window.prompt(uri, “”); 来触发 scheme, 然后 Native 中通过重写 WebViewClient 的 onJsPrompt 来获取 uri
iOS 捕获 url scheme
iOS 中,UIWebView 有个特性:在 UIWebView 内发起的所有网络请求,都可以通过 delegate 函数在 Native 层得到通知。这样, 我们可以在 webview 中捕获 url scheme 的触发 (原理是利用 shouldStartLoadWithRequest)
– (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];

NSString *requestString = [[request URL] absoluteString];
// 获取利润 url scheme 后自行进行处理
return YES;
}
之后 Native 捕获到了 JS 调用的 url scheme, 接下来就该到下一步分析 url 了
第四步: 分析 url- 参数和回调的格式
在前面的步骤中,Native 已经接收到了 JS 调用的方法, 那么接下来, 原生就应该按照定义好的数据格式来解析数据了
url scheme 的格式, 前面已经提到。Native 接收到 Url 后, 可以按照这种格式将回调参数 id、api 名、参数提取出来, 然后按如下步骤进行

(1) 根据 api 名, 在本地找寻对应的 api 方法, 并且记录该方法执行完后的回调函数 id

(2) 根据提取出来的参数, 根据定义好的参数进行转化
如果是 JSON 格式需要手动转换, 如果是 String 格式直接可以使用

(3) 原生本地执行对应的 api 功能方法

(4) 功能执行完毕后, 找到这次 api 调用对应的回调函数 id, 然后连同需要传递的参数信息, 组装成一个 JSON 格式的参数

回调的 JSON 格式为:{responseId: 回调 id,responseData: 回调数据}
responseId String 型 H5 页面中对应需要执行的回调函数的 id, 在 H5 中生成 url scheme 时就已经产生
responseData JSON 型 Native 需要传递给 H5 的回调数据, 是一个 JSON 格式: {code:( 整型, 调用是否成功,1 成功,0 失败),result: 具体需要传递的结果信息, 可以为任意类型,msg: 一些其它信息, 如调用错误时的错误信息 }

(5) 通过 JSBridge 通知 H5 页面回调
参考 第五步 Native 如何调用 JS

第五步:Native 如何调用 JS
到了这一步, 就该 Native 通过 JSBridge 调用 H5 的 JS 方法或者通知 H5 进行回调了, 具体如下
// 将回调信息传给 H5
JSBridge._handleMessageFromNative(messageJSON);
如上, 实际上是通过 JSBridge 的_handleMessageFromNative 传递数据给 H5, 其中的 messageJSON 数据格式根据两种不同的类型, 有所区别, 如下
Native 通知 H5 页面进行回调
数据格式为: 上文中的回调的 JSON 格式
Native 主动调用 H5 方法
Native 主动调用 H5 方法时, 数据格式是:{handlerName:api 名,data: 数据,callbackId: 回调 id}

handlerName String 型 需要调用的,h5 中开放的 api 的名称
data JSON 型 需要传递的数据, 固定为 JSON 格式 (因为我们固定 H5 中注册的方法接收的第一个参数必须是 JSON, 第二个是回调函数)

注意, 这一步中, 如果 Native 调用的 api 是 h5 没有注册的,h5 页面上会有对应的错误提示。
另外,H5 调用 Native 时,Native 处理完毕后一定要及时通知 H5 进行回调, 要不然这个回调函数不会自动销毁, 多了后会引发内存泄漏。
第六步:H5 中 api 方法的注册以及格式
前面有提到 Native 主动调用 H5 中注册的 api 方法, 那么 h5 中怎么注册供原生调用的 api 方法呢?格式又是什么呢? 如下
H5 中注册供原生调用的 API
// 注册一个测试函数
JSBridge.registerHandler(‘testH5Func’,function(data,callback){
alert(‘ 测试函数接收到数据:’+JSON.stringify(data));
callback&&callback(‘ 测试回传数据 …’);
});
如上述代码为注册一个供原生调用的 api
H5 中注册的 API 格式注意
如上代码, 注册的 api 参数是 (data,callback)
其中第一个 data 即原生传过来的数据, 第二个 callback 是内部封装过一次的, 执行 callback 后会触发 url scheme, 通知原生获取回调信息
思路
大致思路就是

h5 调用 Native 的关键步骤进行拆分, 由以前的直接传递 url scheme 变为传递一个统一的 url scheme, 然后 Native 主动获取传递的参数

完善以前: H5 调用 Native-> 将所有参数组装成为 url scheme-> 原生捕获 scheme, 进行分析
完善以后: H5 调用 Native-> 将所有参数存入本地数组 -> 触发一个固定的 url scheme-> 原生捕获 scheme-> 原生通过 JSBridge 主动获取参数 -> 进行分析

实现
这种完善后的流程和以前有所区别, 如下
JSBridge 对象图解

JSBridge 实现完整流程

注意由于这次完善的核心是:Native 主动调用 JS 函数, 并获取返回值。而在 Android4.4 以前,Android 是没有这个功能的, 所以并不完全适用于 Android
所以一般会进行一个兼容处理,Android 中采用以前的 scheme 传法,iOS 使用完善后的方案 (也便于 4.4 普及后后续的完善)
完整的 JSBridge
上述分析了 JSBridge 的实现流程, 那么实际项目中, 我们就应该结合上述两种, 针对 Android 和 iOS 的不同情况, 统一出一种完整的方案, 如下
完整调用流程图

例子
基于上面的思想,个人在 github 上维护了一个用于学习的项目(非装载内容):https://github.com/yelloxing/…

不采用 url scheme 方式
前面提到的 JSBridge 都是基于 url scheme 的, 但其实如果不考虑 Android4.2 以下,iOS7 以下, 其实也可以用另一套方案的, 如下:

Native 调用 JS 的方法不变
JS 调用 Native 是不再通过触发 url scheme, 而是采用自带的交互, 比如
Android 中, 原生通过 addJavascriptInterface 开放一个统一的 api 给 JS 调用, 然后将触发 url scheme 步骤变为调用这个 api, 其余步骤不变 (相当于以前是 url 接收参数, 现在变为 api 函数接收参数)
iOS 中, 原生通过 JavaScriptCore 里面的方法来注册一个统一 api, 其余和 Android 中一样 (这里就不需要主动获取参数了, 因为参数可以直接由这个函数统一接收)

退出移动版