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 条评论)