作者:阿里-移动云-大前端团队
传统的移动端开发,一个完整的业务需要维护三份终端代码: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中,其初简化的初始化逻辑入如下:
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();
}
}
}
从代码中可以看到,weex的初始化比较简单,主要完成两件事:
- 完成初始化配置:比如指定相关适配器,比如图片请求适配器
- 注册自定义的UI组件和功能模块
剩下的事情都交给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();
}
这是WEEX SDK的初始化逻辑,其主要做了以下几件事:
- 初始化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
创建组件时,需要在本地已注册的组件中需要标签对应的组件,此处<text>
标签对应的组件为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的原理。文章内容如有偏颇,欢迎大家指正。