作者:阿里 - 移动云 - 大前端团队
传统的移动端开发,一个完整的业务需要维护三份终端代码:Android,iOS,H5,这带来了极大的开发成本以及维护成本.尤其是对处于业务初创期需要快速试错的业务以及需要支持定期运营活动的业务.所以业界也一直在探索跨平台方案,旨在通过一套代码完成各个终端的业务逻辑.相关方案经过不断演化,从早期的 H5,Hybrid 到如今的 Cloud Native(云原生),在开发效率和用户体验上都在一点点逼近最初的设想.
早期 H5 和 Hybrid 方案的核心是利用终端的内置浏览器(webview)功能,通过开发 web 应用满足跨平台需求.该方案可以解决跨平台问题,同时可以提升发版效率.但其最大的弊端在于用户体验相较于 native 开发的 app 存在较大差距,经常出现页面卡顿,加载慢等问题.
于是后来业界开始探索依旧利用 web 技术栈开发出媲美原生体验 app 的方案,于是以 WEEX 为代表云原生开发框架开始出现.所谓云原生 (Cloud Native) 指可以通过云端快速发布(与远程 web 应用发布流程类似),同时还可以达到媲美原生 App 体验的方案.WEEX 依旧采取传统的 web 开发技术栈进行开发,同时 app 在终端的运行体验不输 native app.其同时解决了开发效率,发版速度以及用户体验三个核心问题.那么 WEEX 是如何实现的?目前 WEEX 已经完全开源,并捐给 Apache 基金会,我们可以通过分析其源码来一探究竟.
WEEX 框架主要分为两部分:
前端 JavaScript 框架
Native SDK
本文主要探讨 Native SDK 的核心原理,其前端 JavaScript 框架会在后续的文章中进行介绍.
1 整体架构
首先来看下 WEEX 开发的整体架构:
从上图中可以看到 weex 的大致工作流程:
研发人员利用 web 技术栈开发 weex file,打包成 JS Bundle,然后部署到服务器上
终端通过网络获取 JS Bundle,然后在本地执行该 JS Bundle
终端上提供了 JS 的执行引擎 (JSCore) 用于执行远程加载到 JS Bundle
JS 执行引擎执行 JS Bundle,并将相关渲染指令以及其他需要利用 native 能力的指令通过 JS-Native Bridge 透出
JS-Native Bridge 将渲染指令分发到 native(Andorid,iOS)渲染引擎,由 native 渲染引擎完成最终的页面渲染
看完上述整体架构后,可以大致理解为何 WEEX 可以达到媲美原生的体验,因为其页面渲染并不是像 H5 方案一样接入浏览器的渲染能力,而是原生渲染,所以本质上渲染出来的页面就是一个 native 页面.
接下来我们再来将端上的模块进行详细的拆分:
如上图所示,WEEX NATIVE SDK 大致可以分为如下几个层级:
JS 执行层:
JS 执行引擎:JSCore,解释并执行 JS Bundle
main.js:提供 WEEX runtime,SDK 初始化,JS Core 会首先加载 main.js, 为 js bundle 提供 weex runtime
Bridge 层:提供 JS 和 Native 的双向通信能力
Dom 层:维护页面 Dom 结构
Render 层:完成页面渲染
native 组件库:本地 UI 组件库,每一个组件对应一个 html 标签,所以当我们在 weex 开发过程中使用到的各种标签:div,text,image 等等,最终都被转化成为了一个 native 的控件
module manager,module 库:功能模块管理层
WXSDKManger,WXSDKEngine:SDK 全局环境维护
WXSDKInstance:weex 实例,一个 js bundle 对应一个 weex 实例
2 WEEX SDK 初始化
有了上述大致架构和功能划分后,我们以一个实际的例子来分析 WEEX NATIVE SDK 的运行逻辑.首先来看下 WEEX SDK 在初始化阶段都做了哪些准备工作.
这里以 Andorid 代码为例进行分析: WEEX 的初始化通常放在 Application 中,其初简化的初始化逻辑入如下:
从代码中可以看到,weex 的初始化比较简单,主要完成两件事:
public class WXApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initWeex();
......
}
private void initWeex() {
// 自定义相关配置
InitConfig config=new InitConfig.Builder()
.setImgAdapter(new ImageAdapter()) // 自定义图片适配器
.build();
WXSDKEngine.initialize(this,config);
// register module
try {
WXSDKEngine.registerModule("testmodule", TestModule.class); // 注册自定义模块
WXSDKEngine.registerModule("event", WXEventModule.class);
WXSDKEngine.registerComponent("richtext", RichText.class); // 注册自定义 UI 组件
......
} catch (WXException e) {
e.printStackTrace();
}
}
}
完成初始化配置:比如指定相关适配器,比如图片请求适配器
注册自定义的 UI 组件和功能模块
剩下的事情都交给 WEEX SDK 来完成了,那么接下来就来看下 WEEX SDK 都做了些什么?
具体代码在
这是 WEEX SDK 的初始化逻辑,其主要做了以下几件事:
WXSDKEngine.doInitInternal
:
private static void doInitInternal(final Application application,final InitConfig config){
WXEnvironment.sApplication = application;
WXEnvironment.JsFrameworkInit = false;
WXBridgeManager.getInstance().post(new Runnable() {
@Override
public void run() {
long start = System.currentTimeMillis();
WXSDKManager sm = WXSDKManager.getInstance();
sm.onSDKEngineInitialize();
if(config != null ) {
sm.setInitConfig(config);
if(config.getDebugAdapter()!=null){
config.getDebugAdapter().initDebug(application);
}
}
WXSoInstallMgrSdk.init(application,
sm.getIWXSoLoaderAdapter(),
sm.getWXStatisticsListener());
boolean isSoInitSuccess = WXSoInstallMgrSdk.initSo(V8_SO_NAME, 1, config!=null?config.getUtAdapter():null);
if (!isSoInitSuccess) {
return;
}
sm.initScriptsFramework(config!=null?config.getFramework():null);
WXEnvironment.sSDKInitExecuteTime = System.currentTimeMillis() - start;
WXLogUtils.renderPerformanceLog("SDKInitExecuteTime", WXEnvironment.sSDKInitExecuteTime);
}
});
register();
}
初始化 WXBridge,同时启动 WXBridge 线程,待接收指令.WXBridge 在 Android 的实现本质上是一个基于 HandlerThread 的异步任务处理线程
initSo:加载 so 文件,即 JS 执行引擎
initScriptsFramework:加载 SDK 中的 main.js, 完成 weex runtime 的初始化
register:注册 SDK 自带的 UI 组件和功能模块
3 页面渲染
WEEX SDK 在完成了初始化之后,即可开始渲染页面了.接下来我们以如下这 JS 代码为例,来介绍页面的渲染逻辑:
JS 代码比较简单,逻辑就不介绍了.接下来重点介绍,当终端获取到如上图右侧的 js bundle 后,如何进行加载,渲染以及后续的相关逻辑执行.
3.1 weex 实例创建
实际上当 WEEX SDK 获取到 JS Bundle 后,第一时间并不是立马渲染页面,而是先创建 WEEX 的实例:
这幅时序图中有两个主要逻辑:
创建 createInstance:创建一个 weex 实例,每一个 JS bundle 对应一个实例,同时每一个实例都有一个 instance id.由于所有的 js bundle 都是放入到同一个 JS 执行引擎中执行,那么当 js 执行引擎通过 WXBridge 将相关渲染指令传出的时候,需要通过 instance id 才能知道该指定要传递给哪个 weex 实例
execJs:在创建实例完成后,接下来才是真正将 js bundle 交给 js 执行引擎执行
3.2 页面渲染
在实例创建完成后,接下来就是页面渲染了.首先来看下页面渲染的整体流程:
js bundle 涉及 dom 操作的执行都会被 weex-vue-framework 转化成 native dom api, 前端框架 vue 是基于 virtual dom api, 而 weex 的前端框架: weex-vue-framework 的核心逻辑就是将 vue 的 virtual-dom 转换成 Native DOM API
weex 终端的执行引擎在执行到 Native DOM API 后,则会将其转化为 Platform API, 说白了就是通过 WXBridge 将 Native DOM API 以约定的方式转发给 native 渲染引擎,完成页面渲染
可以看到,在 js 执行引擎创建好 weex 实例后,会执行对应的 JS Bundle,并在执行到 platform api 的时候将其通过 wxbridge,发送给 DomManager.相关代码可参考:
com.taobao.weex.bridge.WXBridge
3.2.1 createbody
一个页面的 DOM 结构最外层是 body,所以创建页面一开始就是 createbody,整个 create body 的过程大致可以分为以下几个步骤:
WXBridge 将 create body 指令发送给 WXDom 模块.WXDom 是另一个异步线程,负责维护页面的 Dom 树
WXDom 创建一个新的 dom 树,同时创建 body 节点
WXDom 将 create body 指令传递给 WXRenderManager 渲染引擎,渲染引擎主要完成如下几件事:
初始化一个组件实例,称为 mGodComponent
generateComponentTree:由于一个 WEEX 页面就是由多个 UI 组件 (Component) 构成的一棵树,所以渲染引擎会初始化组件树
创建 view
3.3.2 addElement
创建完 body 后,需要在 body 中添加一个 text 组件,指向该操作的 Native DOM API 为 addElement, 其具体操作为:
WXDomManager: 更新本地 dom 树,添加 text 节点
WXRenderManager:本地渲染引擎添加相关组件:
从已注册的组件中找到 text 对应的组件,并实例化
将初始化完成的 text 组件添加到 body 所对应的 view 之上
给 text 组件设定布局,添加监听事件
加入数据绑定
在此一个带有一个 text 标签的简单页面才算是渲染完成.值得一提的是,在 WXRenderManager 创建组件时,需要在本地已注册的组件中需要标签对应的组件,此处 标签对应的组件为
com.taobao.weex.ui.component.WXText
,其本质上是一个 TextView.从这里可以发现,其实我们在 JS Bundle 中指定的各种标签,其实都最终被转化为了一个 native 的控件.这也就是为什么用 WEEX 开发出来的 app,本质上还是一个 Native App.
其他的对应关系还有:
div 对应 WXDiv
image 对应 WXImage
list 对应 WXListComponent
a 对应 WXA
......
4 总结
通过前文的介绍,相信大家对 WEEX 有了一个初步的系统认识.简单来说,WEEX 放弃了传统的 Webview,而是搭建了一个
native 化的浏览器
,因为用 native 的方式实现了一个浏览器的大部分核心组成成分:
JS 执行引擎
渲染引擎
DOM 树管理
网络请求,持久层存储等等能力
...
另外为了保证整个 SDK 的运行效率,SDK 维护了三个线程:
bridge 线程:完成 js 到 native 之间的通信
dom 线程:完成 dom 结构的构建
渲染线程:完成 UI 渲染,也就是 UI 线程
以上就是 WEEX SDK 的大致框架和核心逻辑,篇幅有限,无法面面俱到,只是希望通过该文想大家展示 WEEX 基于 WEB 技术栈开发 native app 的原理.文章内容如有偏颇,欢迎大家指正.
来源: http://click.aliyun.com/m/41094/