Native 与 JS 的双向通信
本文会介绍 Native 应用中 Native 层与 JS 层是如何通信的,以及从通信原理中找到一些需要注意的地方。
前置知识:进程间通信
进程间通信(IPC,Inter-Process Communication)指的是两个不同的进程相互传递信息。在一个 Native 程序中,嵌入一个 Webview 控件以后,这个 Webview 控件相当于一个小型的浏览器,它会开启 UI 渲染线程、JS 虚拟机线程、网络线程等。所以 Native 与 JS 通信,其实是 Native 线程与 JS 虚拟机线程的通信。
不管是进程间通信还是线程间通信,理论上可复用的数据很高,如 Node.js 进程之间甚至可以共享一个 Server 或者 Socket。然而, JS 与 Native 的数据结构不同,所以 Native 的数据结构并不能复用。Native 与 JS 的通信会使用 HTML5 结构化克隆算法来序列化传递的数据,也就是说传递的数据最终会被转换成字符串,所以不能被 JSON.stringify 或其他序列化方法转换的的数据结构就会丢失。
Native 调用 JS
首先来说一说 Native 如何调用 JS。其实,所有的 Webview 控件都会自带一个方法用来执行 JS,只是它们的格式有所区别,主要有以下两种格式:
Native 调用 JS 是一件非常简单的事情,但是一般只有做自动化测试的时候才会这么做,因为 JS 能做的事情 Native 也能做,而且做得更好。
JS 调用 Native
JS 调用 Native 的方法在不同的平台都不一样,下面我们来分别讲解。
Internet Explorer
在 HTML 标准中,微软贡献了一个名为window.external
的全局变量。这个变量用来提供添加浏览器的搜索引擎、添加收藏夹、设置主页等外部功能,自然也可以作为 Native 与 JS 通信的桥梁。
在一个 IE 应用中,Webview 控件有一个ObjectForScripting
属性,这个属性可以被 JS 端的window.external
访问到。比如有如下 Native 代码:
ObjectForScripting
属性被指定成WebviewClass
这个类的一个实例,而ObjectForScripting
又等于window.external
,那么这个实例中的Test
函数就可以通过window.external.Test
访问到。
Microsoft Edge UWP
在 UWP 版本 Edge 浏览器中,微软依然是通过window.external
这个全局变量来访问 Native 代码,然而它和 IE 不同的是,它不是直接调用 Native 函数,而是通过window.external.notify
函数给 Native 层传递一串字符串,Native 层有一个叫ScriptNotify
的事件专门用来接收这个字符串。收到字符串以后,再从中提取一些特征信息(调用的函数名、参数等),并且执行响应的逻辑。
由于频繁手动调用 notify 麻烦且易错,所以一般会在 JS 层指定一个全局变量或全局函数来封装 Native 调用。一个典型的例子如下:
Native 代码:
HTML 代码:
Native 端先让 JS 层在 window 对象上挂载一个叫callNative
的全局变量,由于 Edge 调用 JS 是采用函数名和函数参数分开的写法,所以这里需要用eval
函数来执行 JS 代码。同时,Native 端也需要挂载ScriptNotify
事件,这里是直接调用Console.WriteLine
输出到控制台。最后,JS 端调用callNative.writeLine
函数,这个函数会调用window.external.notify
函数,将msg
传递给ScriptNotify
事件,进而触发Console.WriteLine
函数。
Microsoft Edge Webview2 (Chromium)
最近微软发布了 Webview2 控件,它是基于 Chromium 的浏览器。Webview2 和传统 Webview 在 Native 与 JS 双向通信上大同小异,主要区别是 Webview2 用window.chrome.webview.postMessage
替代了window.external.notify
,用WebMessageReceived
替代了ScriptNotify
,调用 JS 代码也可以直接执行 JS 而不需要用eval
函数包裹。
将上面的案例稍作修改就可以用于 Webview2:
Native 代码:
HTML 代码:
Android
Android 端的调用方式比较类似于 Internet Explorer,也是将 Native 的函数封装到一个对象里,然后将这个对象写入一个特殊的属性,作为 Native 与 JS 直接的桥梁。比 IE 灵活的一点是,Android 可以通过addJavascriptInterface
函数注入多个对象,而不是只能通过window.external
访问。
一个典型的例子:
Native 代码:
HTML 代码:
上面的例子中,我们先写了一个叫JSInterface
的类,里面有一些 Native 函数,然后在onCreate
生命周期中调用addJavascriptInterface
函数,第一个参数是需要传递给 JS 的对象,第二个参数是全局变量的名字。注入完毕后,就可以在 JS 端调用window.callNative1.Test
和window.callNative2.Test
函数了。
iOS
iOS 端采用了类似 Internet Explorer 的全局变量注入和类似 Webview2 的postMessage
通信注入两种结合的方式。
iOS 端需要调用AddScriptMessageHandler
函数来给 JS 端传递一个对象,第一个参数的要传递的对象,第二个参数是入口名称。和 IE 不同,iOS 端传入的对象中并不直接包含业务代码,而是一个消息接收对象,该对象必须包含一个叫DidReceiveScriptMessage
的方法用来接收 JS 传来的消息:
注入成功后,就可以在 JS 端通过window.webkit.messageHandlers[入口名称].postMessage
给 Native 发送消息了。
一个典型的例子:
Native 代码:
HTML 代码:
上面的例子中,Native 端先通过AddScriptMessageHandler
将类MessageHandler
的实例作为入口invokeAction
注入到 JS 端,然后 JS 端再调用window.webkit.messageHandlers.invokeAction.postMessage
与 Native 通信。
注意事项
数据丢失
通过进程间通信的原理和上面的例子,我们发现 Native 和 JS 通信时数据最终会变成字符串的格式。虽然可以通过 JSON5 来传递更多的信息,或者使用二进制流来传递文件,但是像函数、Date 等复杂对象依然不能被正确转换,因此不能传递复杂的数据。
通信开销
同上面的场景,通信前后需要对数据进行序列化。并且由于数据信息的缺失,拿到数据后我们可能还要对数据进行处理。如果频繁的进行跨端通信,会对性能产生很大的影响。
数据截断
跨端通信对于数据的大小是有限制的,在移动端尤为明显。如果将一个非常大的数据进行跨端传输,可能会造成内存占用大,导致被操作系统杀死。所以如果要传递大数据,可以借鉴 HTTP 通信中的报文机制,进行分段传输。
版权声明: 本文为 InfoQ 作者【Minar Kotonoha】的原创文章。
原文链接:【http://xie.infoq.cn/article/375bf1879fd8974e7305809d8】。文章转载请联系作者。
评论 (2 条评论)