移动端跨平台开发实现原理浅析

685 阅读10分钟

近几年移动互联网发展迅猛,App的需求量更是持续飙升,得益于H5研发周期短,快平台等优势,做APP或多或少的都离不开前端开发。有的公司以h5开发为辅,主业务模块用native开发,有的以h5代码为主,native主要提供一些原生能力给前端调用,市面上也有很多这种框架。 我们做前端开发,不应该紧局限于前端代码本身,了解前后关系可以很好的提高我们自身专业水平,毕竟前端的框架原理只能说给前端的同学听, 而边界知识可以帮我们跨界交(zhuang)流(B)。 比如说在需求评审上可以给产品或者相关的开发一些问题或者建议,又或者是某种方案的可行性,这应该就是专业的表现吧!

在开始之前先抛出三个问题,我们可以带着这三个问题一起完成接下来的学习:

  1. 浏览器内核、V8 引擎经常出现在我们的视线中,那么浏览器内核有哪些模块组成?

  2. 微信小程序内为什么不能访问 windowdocument 对象?

  3. Hybird 应用、小程序、RN 等跨端方案优缺点以及实现方式有什么区别?

纵观所有的跨平台技术,大多数都是基于 JS 语言来实现的, 而 JS 的执行需要依赖于 JS 引擎。所以,了解跨平台开发的第一站应该是 JS Engine

浏览器内核

浏览器内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是 JS引擎。下面表格中展示了不通浏览器之间渲染内核JS引擎的差异:

浏览器内核JS 引擎
IE/EdgeTrident/EdgeHTMLJscript(< IE9 ) Chakra
SafariWebkit/Webkit2JavaScriptCore
ChromeChromium(Webkit)/BlinkV8
FireFoxGeckoSpiderMonkey,TraceMonkey,JaegerMonkey
  • IE/Edge:微软的 IE 浏览器浏览器更新至 IE10 后,伴随着 WIN10 系统的上市,迁移到了全新的浏览器 Edge。除了 JS 引擎沿用之前 IE9 就开始使用的查克拉(Chakra),渲染引擎使用了新的内核 EdgeHTML(本质上不是对 Trident 的完全推翻重建,而是在 Trident 基础上删除了过时的旧技术支持的代码,扩展和优化了对新的技术的支持,所以被看做是全新的内核)

  • Safari:Safari 自 2003 年面世,就一直是苹果公司的产品自带的浏览器,它使用的是苹果研发和开源的 Webkit 引擎。Webkit 引擎包含 WebCore 排版引擎及 JavaScriptCore 解析引擎,均是从 KDE 的 KHTML 及 KJS 引擎衍生而来。Webkit2 发布于 2010 年,它实现了元件的抽象画,提高了元件的重复利用效率,提供了更加干净的网页渲染和更高效的渲染效率。另外,Webkit 也是苹果 Mac OS X 系统引擎框架版本的名称,主要用于 Safari、Dashboard、Mail。

  • Chrome:提到 Chrome 浏览器,一般人会认为使用的 Webkit 内核,这种说法不完全准确。Chrome 发布于 2008 年,使用的渲染内核是 Chromium,它是 fork 自 Webkit,但把 Webkit 梳理得更有条理可读性更高,效率提升明显。2013 年,由于 Webkit2 和 Chromium 在沙箱设计上的冲突,谷歌联手 Opera 自研和发布了 Blink 引擎,逐步脱离了 Webkit 的影响。所以,可以这么认为:Chromium 扩展自 Webkit 止于 Webkit2,其后 Chrome 切换到了 Blink 引擎。另外,Chrome 的 JS 引擎使用的 V8 引擎,应该算是最著名和优秀的开源 JS 引擎,大名鼎鼎的 Node.js 就是选用 V8 作为底层架构。

  • Firefox:火狐的内核 Gecko 也是开源引擎,任何程序员都能为其提供扩展和建议。火狐的 JS 引擎历经 SpiderMonkeyTraceMonkey 到现在的 JaegerMonkey。其中 JaegerMonkey 部分技术借鉴了 V8JSCoreWebkit,算是集思广益。

WebKit

我们以 WebKit 为例来看看浏览器内核的内部架构:

WebKit

简单点讲,WebKit 就是一个页面渲染以及逻辑处理引擎,前端代码 HTMLCSSJavaScript 作为输入,经过 WebKit 的处理,就输出成了我们能看到以及操作的 Web 页面。从上图我们可以看出来,WebKit 由图中框住的四个部分组成。而其中最主要的就是 WebCoreJSCore(或者是其它 JS 引擎),WebKit Embedding API 是负责浏览器 UI 与 WebKit 进行交互的部分,而 WebKit Ports 则是让 Webkit 更加方便的移植到各个操作系统、平台上,提供的一些调用 Native Library 的接口,比如在渲染层面,在 iOS 系统中,Safari 是交给 CoreGraphics 处理,而在 Android 系统中,Webkit 则是交给 Skia

WebCore

时至今日,WebKit 已经有很多的分支以及各大厂家也进行了很多优化改造,唯独 WebCore 这个部分是所有 WebKit 共享的。WebCore 是 WebKit 中代码最多的部分,也是整个 WebKit 中最核心的渲染引擎。

img

首先浏览器通过 URL 定位到了一堆由 HTML、CSS、JS 组成的资源文件,通过加载器(这个加载器的实现也很复杂,在此不多赘述)把资源文件给 WebCore。之后 HTML Parser 会把 HTML 解析成 DOM 树,CSS Parser 会把 CSS 解析成 CSSOM 树。最后把这两棵树合并,生成最终需要的渲染树,再经过布局,与具体 WebKit Ports 的渲染接口,把渲染树渲染输出到屏幕上,成为了最终呈现在用户面前的 Web 页面。

JSCore

JS 引擎的使命都相同,那就是解释执行 JS 脚本。而从上面的渲染流程图我们可以看到,JS 和 DOM 树之间存在着互相关联,这是因为浏览器中的 JS 脚本最主要的功能就是操作 DOM 树,并与之交互。同样的,我们也通过一张图看下它的工作流程:

img

它主要分为以下三个部分:词法分析(Lexer)、语法分析(parser)以及解释执行(LLInt)

词法分析很好理解,就是把一段我们写的源代码分解成 Token 序列的过程,这一过程也叫分词。在 JSCore,词法分析是由 Lexer 来完成(有的编译器或者解释器把分词叫做 Scanner)。

tokens

Parser 会把 Lexer 分析之后生成的 token 序列进行语法分析,并生成对应的一棵抽象语法树(AST)。这个树长什么样呢?在这里推荐一个网站:esprima Parser,输入 JS 语句可以立马生成我们所需的 AST。

parser

解释执行(LLInt) JS 源代码经过了词法分析和语法分析这两个步骤,转成了字节码,其实就是经过任何一门程序语言必经的步骤–编译。

跨平台发展史

  • 2009 年,PhoneGap0.6 发布,是 PhoneGap 历史上第一个稳定版本
  • 2010 年,Cordova 被公布,Hybird开发如火如荼
  • 2013 年,iOS 7 发布后,JSCore 作为一个系统级 Framework 被苹果提供给开发者,于是产生了更多的可能性
  • 2015 年,react native 诞生
  • 2016 年,Weex 诞生

常见的移动端开发模式

混合开发 Hybrid

要解决跨端问题的第一步就是解决两种语言之间的通信,比如在 iOS 端就是 OC/Swift 和 JS 的通信,而 Android 端则是 Java 和 JS 的通信。我们以 iOS 端为例来看通信是怎么完成的

iOS 中的 JSCore

parser

  1. JSVirtualMachine——JavaScript 的虚拟机

    JavaScriptCore 中提供了一个名为 JSVirtualMachine 的类,顾名思义,这个类可以理解为一个 JS 虚拟机。在 Native 中,只要你愿意,你可以创建任意多个 JSVirtualMachine 对象,各个 JSViretualMachine 对象间是相互独立的,他们之间不能共享数据也不能传递数据,如果你把他们放在不同的 Native 线程,他们就可以并行的执行无关的 JS 任务。

  2. JSContext——JavaScript 运行环境

    JSContext 上下文对象可以理解为是 JS 的运行环境,同一个 JSVirtualMachine 对象可以关联多个 JSContext 对象,并且在 WebView 中每次刷新操作后,此 WebView 的 JS 运行环境都是不同的 JSContext 对象。其作用就是用来执行 JS 代码,在 Native 和 JS 间进行数据的传递。

  3. JSValue——JavaScript 值对象

    JavaScript 和 Objective-C 虽然都是面向对象语言,但其实现机制完全不同,OC 是基于类的,JS 是基于原型的,并且他们的数据类型间也存在很大的差异。因此若要在 Native 和 JS 间无障碍的进行数据的传递,就需要一个中间对象做桥接,这个对象就是 JSValue。

  4. JSExport

    JSExport 是一个协议,Native 中遵守此解析的类可以将方法和属性转换为 JS 的接口供 JS 调用。

几种交互方式

  1. 拦截 url(适用于 UIWebView 和 WKWebView)

  2. JavaScriptCore(只适用于 UIWebView,iOS7+)

  3. WKScriptMessageHandler(只适用于 WKWebView,iOS8+)

  4. WebViewJavascriptBridge(适用于 UIWebView 和 WKWebView,属于第三方框架)

拦截 url

[1].和 native 开发人员约定好协议,如如 zmlean://scan 表示启动二维码扫描,zmlean://location 表示获取定位。

[2].实现 UIWebView 代理的 shouldStartLoadWithRequest:navigationType: 方法,在方法中对 url 进行拦截,如果是步骤 <1> 中定义好的协议则执行对应原生代码,返回 false,否则返回 true 继续加载原 url。

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
    if ([request.URL.absoluteString hasPrefix:@"jxaction://scan"]) {
        // 调用原生扫描二维码
       return NO;
    }
    return YES;
}

扫描二维码结束后,需要把扫描结果返回给 web 页,直接调用 UIWebView 的 stringByEvaluatingJavaScriptFromString: 方法,或者 WKWebView 的 evaluateJavaScript:completionHandler: 方法。

[self.webView stringByEvaluatingJavaScriptFromString:@"scanResult('我是扫描结果~')"];
JavaScriptCore

方法一 Web 调用原生只适合简单的调用,如果要传递参数,虽然也可以拼接在 url 上,如 jxaction://scan?method=aaa,但是需要我们自行对字符串进行分割解析,并且特殊字符需要编码。在 iOS7 系统提供了 JavaScriptCore,可以更优雅地实现 js 与原生的交互。


// step1: 新建一个类AppJSObject 集成 JSExport 协议

@protocol AppJSObjectDelegate <JSExport>
-(void)scan:(NSString *)message;
@end

@interface AppJSObject : NSObject<AppJSObjectDelegate>
@property(nonatomic,weak) id<AppJSObjectDelegate> delegate;
@end


// step 2 注入原生方法

-(void)webViewDidFinishLoad:(UIWebView *)webView {
    JSContext *context=[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    AppJSObject *jsObject = [AppJSObject new];
    jsObject.delegate = self;
    context[@"app"] = jsObject;

    // --- 也可以通过 block 实现 ---
    context[@"openAlbum"] = ^(){
      NSLog(@"js调用oc打开相册");
    };

    // 获取当前document的Title,给native导航栏设置标题
    JSValue *value = [self.jsContext evaluateScript:@"document.title"];
    self.navigationItem.title = value.toString;
}

<!DOCTYPE html>
<html>
<body>
    <input type="button" name="" value="扫一扫" onclick="scan()">
    <p id="result">扫描结果:</p>

    <script type="text/javascript">
        // 调用APP的扫描方法 h5->app
        function scan(){
            app.scan('scanResult');
        }

        // 扫描结果回调方法 app->h5
        function scanResult(result){
            document.getElementById("result").innerHTML = '扫描结果:' + result;
        }
    </script>
</body>
</html>

Native 创建一个 webview 页面过程大致如下:新建一个 UIViewController,初始化一个 WebView 控件,设置一些基本属性,比如:长宽、颜色、边距等等。添加到当前页面上,然后调用 load 方法加载需要打开网页的 URL。通过系统提供的 JavaScriptCore 这个 framework,我们可以轻易拿到 webview 内的 JS 运行环境,如同 Chrome 浏览器打开了控制台模式,在 console 可以随意的输入 JS 对当前的上下文产生一些影响。

有点类似于 JSONP 如何实现 HTTP 跨域,但是使用场景又比它丰富。

"JSONP 跨域":前端通过 Script 标签访问一个 URL 并附上 Callback 方法,服务端接收请求,返回一段拼接好的 JS 代码,触发回调函数完成跨域请求。

Native 获取到 JSContext 上下文之后既可以读取 JS 方法,同时也可以对其写入方法以供 JS 调用。

附上经典的Hybird框架: Cordova 架构图

cordova

miniprogram (微信小程序)

miniprogram

微信小程序并非只是一个单纯的 WebView 来加载页面,而是在上面的基础上做了改进。我们都知道在浏览器中,UI 线程与 JS 线程是互斥的,因为 JS 运行结果会影响到 UI 线程的结果,当 JS 线程运行的时候,UI 线程处于冻结状态。如果 JS 线程发生了阻塞,就会导致页面卡顿,体验非常不好。小程序采用了双线程模型,避开了这个问题。

双线程模型

小程序的渲染层和逻辑层分别由两个线程管理:渲染层的界面使用 WebView 进行渲染;逻辑层采用 JSCore 运行 JavaScript 代码。一个小程序存在多个界面,所以渲染层存在多个 WebView。这两个线程间的通信经由小程序 Native 侧中转,逻辑层发送网络请求也经由 Native 侧转发,小程序的通信模型下图所示。

miniprogram

  • 逻辑层 App Service

    业务逻辑层运行在同一个 JSCore 线程中,逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈;具体 ios 是 JavaScriptCore ,android 是 X5 JSCore ,开发者工具是 webview 中;小程序框架的逻辑层并非运行在浏览器中,因此 JavaScript 在 web 中一些能力都无法使用,如 window,document 等。

  • 视图层

    视图 view 层在 webview 中渲染,一个页面对应一个 webview,

一个小 demo

H5页面上运行一段阻塞的代码,页面的输入框在JS线程阻塞期间不会接收页面的输入,同样的代码放到小程序上面,页面输入框不会产生任何的影响


function calculation() {
    for (let i = 0; i < 1000100; i++) {
    for (let j = 0; j < 10000; j++) {
        let ret = i * j
    }
    }
}

为什么要这么设计呢?

管控和安全,微信小程序阻止开发者使用一些浏览器提供的,诸如跳转页面、操作 DOM、动态执行脚本的开放性接口。将逻辑层与视图层进行分离,视图层和逻辑层之间只有数据的通信,可以防止开发者随意操作界面,更好的保证了用户数据安全。

微信小程序视图层是 WebView,逻辑层是 JS 引擎。三端的脚本执行环境以及用于渲染非原生组件的环境是各不相同的:

运行环境逻辑层渲染层
AndroidV8Chromium 定制内核
iOSJavaScriptCoreWKWebView
小程序开发者工具NWJSChrome WebView

我们看一下单 WebView 实例与小程序双线程多实例下代码执行的差异点。

miniprogram-2

单 WebView 模式下,Page 视图与 App 逻辑共享同一个 JSContext,这样所有的页面可以共享全局的数据和方法,能够实现全局的状态管理。多 WebView 模式下,每一个 WebView 都有一个独立的 JSContext,虽然可以通过窗口通信实现数据传递,但是无法共享数据和方法,对于全局的状态管理也相对比较复杂,抽离一个通用的 WebView 或者 JS Engine 作为应用的 JSContext 就可以解决这些问题。

双线程交互的生命周期图示:

miniprogram-2

ReactNative

我们先回顾一下浏览器工作原理

  • 浏览器通过Dom Render来渲染所有的元素.
  • 浏览器有一整套的 UI 控件,样式和功能都是按照 html 标准实现的
  • 浏览器能读懂 HTML 和 CSS。
  • HTML 告诉浏览器绘制什么控件(html tag),CSS 告诉浏览器每个类型的控件(html tag)具体长什么样。
  • 浏览器的主要作用就是通过解析 HTML 来形成 Dom 树,然后通过 CSS 来点缀和装饰树上的每一个节点

UI 的描述和呈现分离开了

  1. html 文本描述了页面应该有哪些功能,css 告诉浏览器该长什么样。

  2. 浏览器引擎通过解析 html 和 css,翻译成一些列的预定义 UI 控件,

  3. 然后 UI 控件去调用操作系统绘图指令去绘制图像展现给用户。

  4. Javascript 可有可无,主要用于 html 里面一些用户事件响应,DOM 操作、异步网络请求和一些简单的计算

rn

  • 绿色的是我们应用开发的部分。我们写的代码基本上都是在这一层
  • 蓝色代表公用的跨平台的代码和工具引擎,一般我们不会动蓝色部分的代码
  • 黄色代码平台相关的代码,做定制化的时候会添加修改代码。不跨平台,要针对平台写不同的代码。iOS 写 OC, android 写 java,web 写 js. 每个 bridge 都有对应的 js 文件,js 部分是可以共享的
  • 红色部分是系统平台的东西。红色上面有一个虚线,表示所有平台相关的东西都通过 bridge 隔离开来了
  • 大部分情况下我们只用写绿色的部分,少部分情况下会写黄色的部分。红色部分是独立于 React Native 的

JavaScriptCore + ReactJS + Bridges 就成了 React Native

  • JavaScriptCore负责 JS 代码解释执行
  • ReactJS负责描述和管理VirtualDom,指挥原生组件进行绘制和更新,同时很多计算逻辑也在 js 里面进行。ReactJS 自身是不直接绘制 UI 的,UI 绘制是非常耗时的操作,原生组件最擅长这事情。
  • Bridges用来翻译 ReactJS 的绘制指令给原生组件进行绘制,同时把原生组件接收到的用户事件反馈给ReactJS。要在不同的平台实现不同的效果就可以通过定制Bridges来实现
  • Bridge 的作用就是给 RN 内嵌的 JS Engine 提供原生接口的扩展供 JS 调用。所有的本地存储、图片资源访问、图形图像绘制、3D 加速、网络访问、震动效果、NFC、原生控件绘制、地图、定位、通知等都是通过 Bridge 封装成 JS 接口以后注入 JS Engine 供 JS 调用。理论上,任何原生代码能实现的效果都可以通过 Bridge 封装成 JS 可以调用的组件和方法, 以 JS 模块的形式提供给 RN 使用。
  • 每一个支持 RN 的原生功能必须同时有一个原生模块和一个 JS 模块,JS 模块是原生模块的封装,方便 Javascript 调用其接口。Bridge 会负责管理原生模块和对应 JS 模块之间的沟通, 通过 Bridge, JS 代码能够驱动所有原生接口,实现各种原生酷炫的效果。
  • RN 中 JS 和 Native 分隔非常清晰,JS 不会直接引用 Native 层的对象实例,Native 也不会直接引用 JS 层的对象实例(所有 Native 和 JS 互掉都是通过 Bridge 层会几个最基础的方法衔接的)。
  • Bridge 原生代码负责管理原生模块并生成对应的 JS 模块信息供 JS 代码调用。每个功能 JS 层的封装主要是针对 ReactJS 做适配,让原生模块的功能能够更加容易被用 ReactJS 调用。MessageQueue.jsBridge在 JS 层的代理,所有 JS2N 和 N2JS 的调用都会经过MessageQueue.js来转发。JS 和 Native 之间不存在任何指针传递,所有参数都是字符串传递。所有的 instance 都会被在 JS 和 Native 两边分别编号,然后做一个映射,然后那个数字/字符串编号会做为一个查找依据来定位跨界对象。

总结

每种跨端方案都有自己的优缺点,可以根据实际情况选择采用哪种方案。Hybird 方案虽然实现简单,但是交互体验上也不如另外两种,适用于嵌套一些简单的页面。小程序方案看似很爽,但是研发成本也是高昂的,目前 App 支持小程序的都是一些大厂,一旦框架研发出来可谓一劳永逸。RN 方案虽然体验最好,但是学习成本相对较高,遇到一些刁钻的需求还是需要 native 的同学用插件的方式解决。

参考

五大主流浏览器及四大内核

深入剖析 WebKit

深入理解 JSCore

微信小程序技术原理分析

React Native 之原理浅析