这篇文章主要简单介绍weex的渲染机制,将重点放在V8引擎在解析完.js文件后,客户端这边如何反序列化生成各种组件,最后组成界面的过程。
如上图,是一张网络普传的Weex工作原理图。介绍一下工作机制:
1. 首先通过编写.vue文件,完成界面组件设计以及逻辑代码编写;
2. Transformer :转换的意思,我们会借助Weex-Toolkit这个工具将.vue文件转换成.js文件
Weex-Toolkit
==$weex compile your_best_weex.[we or vue] . ==
3.deploy:意味着将这个.js文件上传到服务器,后面可以通过CMS下发到客户端
4.客户端收到后,由JS Framework处理,对IOS和Android,会通过V8引擎解析,以json的形式返回给客户端
5.客户端最后根据json完成自己的渲染
本文着墨在最后两步,重点放在最后一步,如何渲染成本地native组件。
先放出时序图,后面的步骤大家可以跟踪时序图走,方便对着看交互顺序。
这个时序图是按照从客户端一个Activity主动发起渲染请求开始,注册了RenderListener监听器,然后等待Weex框架处理完,在回调的时候从监听处获取成品view。Activity可以执行setContentView(view)添加到自己容器内。
结合时序图,先介绍整体的一个流程:
1. Activity发起渲染请求,参数带上渲染的js文件
2. Weex 接收到请求,完成初始化后,将js内容传到JS 引擎
3. 由JS 引擎完成 V DOM的操作,之后将V DOM对应的json字串回传给Weex
4. Weex 解析json,parse成一个个WxDomObject,然后创建对应的WxComponent,创建渲染任务
5. WxRenderManager具体处理渲染任务,渲染完成后,通知Activity渲染结果,完毕
这边首先需要介绍一下相关类的作用以及类之间的结构关系。Weex框架主要包括三大部分。主体是WXSDKManager,以组合的形式带上WXBridgeManager、WXRendermanager和WXDomManager。三个管理类如名称一样,负责各自的功能区域。WXBridgeManager主要负责 和 JS 引擎交互 ,发送native端java的请求到jni层,接收js引擎处理后从jni上返回的消息。WxDomManager负责构建客户端的dom结构,在WXBridgeManager接收到消息后,会交给WxDomManager处理,WxDomManager根据消息创建自己的Dom结构,添加、删除、修改元素。然后将对应的Dom的节点渲染成组件,这一步渲染的工作就需要WXRendermanager负责。当然,三者的管理类并不直接交互,管理类下面还有对应的功能类负责具体的通信、数据处理和管理。
下面会按照请求步骤,解读整个步骤。
客户端发送WXSDKInstance.render()请求后,会走重载然后调用自身方法:renderInternal():
private void renderInternal(String pageName, String template, Map options, String jsonInitData, WXRenderStrategy flag){
//......省略代码
/**
* 初始化容器
*/
ensureRenderArchor();
//......省略代码
/**
* 通知WXSDKManager进行渲染
*/
WXSDKManager.getInstance().createInstance(this, template, renderOptions, jsonInitData);
mRendered = true;
}
上面主要两件事情,第一ensureRenderArchor()做了初始化容器mRenderContainer的工作,WXSDKInstance有个比较重要的属性mRenderContainer,代表的是渲染完成后的根view,最后渲染完成后回调的也是这个view;之后就调用createInstance()。
WXSDKInstance作用其实相当于activity页面的代理,负责和页面进行交互。
void createInstance(WXSDKInstance instance, String code, Map options, String jsonInitData) {
/**
* 1.通知mWXRenderManager注册这个WXSDKInstance实例
* 2.通知mBridgeManager去解析这个js文件
*/
mWXRenderManager.registerInstance(instance);
mBridgeManager.createInstance(instance.getInstanceId(), code, options, jsonInitData);
}
public void registerInstance(WXSDKInstance instance) {
/**
* mRegistries以key,value的形式保存WXSDKInstance,对应的WXRenderStatement
* 这个WXRenderStatement作用是保存WXSDKInstance对应的所有的组件
*/
mRegistries.put(instance.getInstanceId(), new WXRenderStatement(instance));
}
代码比较简单,说明一下mRegistries是一个map,以WXSDKInstance的id和WXSDKInstance对应的WXRenderStatement作为key,value。而WXRenderStatement里面也有一个map,用来存放这个WXSDKInstance对应的所有组件
class WXRenderStatement {
private Map mRegistry;
private WXSDKInstance mWXSDKInstance;
}
接回上面mBridgeManager.createInstance(),看看到底发生了什么事情:
public void createInstance(final String instanceId, final String template, final Map options, final String data) {
final WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(instanceId);
WXModuleManager.createDomModule(instance);
post(new Runnable() {
@Override
public void run() {
invokeCreateInstance(instance, template, options, data);
}
}, instanceId);
}
主要还是两件事情,第一,为我们的WXSDKInstance创建了一个对象WxDomModule,这个WxDomModule的工作相当于一个跑腿,WXBridgeManager和JS 引擎交互得到的消息会交给WxDomModule传递到WXModuleManager。之后post了一个invokeCreateInstance()方法,这个方法执行在工作线程
private void invokeCreateInstance(@NonNull WXSDKInstance instance, String template, Map<String, Object> options, String data) {
initFramework("");
try {
WXJSObject instanceIdObj = new WXJSObject(WXJSObject.String, instance.getInstanceId());
WXJSObject instanceObj = new WXJSObject(WXJSObject.String, template);
WXJSObject optionsObj = new WXJSObject(WXJSObject.JSON, options == null ? "{}" : WXJsonUtils.fromObjectToJSONString(options));
WXJSObject dataObj = new WXJSObject(WXJSObject.JSON, data == null ? "{}" : data);
WXJSObject[] args = {instanceIdObj, instanceObj, optionsObj, dataObj};
invokeExecJS(instance.getInstanceId(), null, METHOD_CREATE_INSTANCE, args, false);
} catch (Throwable e) {
}
}
省略部分代码,这部分代码还是主要两件事,第一initFramework(),是初始化main.js。然后将前面获取到的所有参数,封装成WXJSObject,调用invokeExecJS()继续传递
private void invokeExecJS(String instanceId, String namespace, String function, WXJSObject[] args,boolean logTaskDetail){
mWXBridge.execJS(instanceId, namespace, function, args);
}
mWXBridge是接口IWXBridge的实现类,负责具体跟jni层交互。execJS()由java层调用传递到jni ,在jni层 负责 和 JS 引擎交互,完成后会通过callNative()返回到java层。
所以后面直接跟踪callNative()对应的代码:
```
public int callNative(String instanceId, String tasks, String callback) {
int errorCode = IWXBridge.INSTANCE_RENDERING;
try {
errorCode = WXBridgeManager.getInstance().callNative(instanceId, tasks, callback);
} catch (Throwable e) {
}
return errorCode;
}
```
IWXBridge接到callNative的调用,将所有的信息扔给WXBridgeManager来处理,而WXBridgeManager会根据instanceId,也就是WxSDKInstance对应的instanceId交给对应的WxDomModule来处理
WXDomModule dom = getDomModule(instanceId);
dom.callDomMethod(task);
public void callDomMethod(JSONObject task) {
if (task == null) {
return;
}
String method = (String) task.get(WXBridgeManager.METHOD);
JSONArray args = (JSONArray) task.get(WXBridgeManager.ARGS);
callDomMethod(method, args);
}
public Object callDomMethod(String method, JSONArray args) {
if (method == null) {
return null;
}
try {
switch (method) {
case CREATE_BODY:
if (args == null) {
return null;
}
createBody((JSONObject) args.get(0));
break;
case UPDATE_ATTRS:
if (args == null) {
return null;
}
updateAttrs((String) args.get(0), (JSONObject) args.get(1));
break;
//...省略其他cases
}
} catch (IndexOutOfBoundsException e) {
} catch (ClassCastException cce) {
}
return null;
}
WxDomModule的处理过程是这样的,首先将这个JSONObject拆开成method和args,然后根据不同的method创建不同的任务,如上图, case CREATE_BODY的情况下创建createBody()这个任务
public void createBody(JSONObject element) {
if (element == null) {
return;
}
Message msg = Message.obtain();
WXDomTask task = new WXDomTask();
task.instanceId = mWXSDKInstance.getInstanceId();
task.args = new ArrayList<>();
task.args.add(element);
msg.what = WXDomHandler.MsgType.WX_DOM_CREATE_BODY;
msg.obj = task;
WXSDKManager.getInstance().getWXDomManager().sendMessage(msg);
}
封装好后通过handler发送出去,而这个handler是WxDomManager的一个变量,是一个WXDomHandler类,负责接收WxDomModule发过来的任务,处理完之后才会交给WxDomManager的
public void sendMessage(Message msg) {
if (msg == null || mDomHandler == null || mDomThread == null
|| !mDomThread.isWXThreadAlive() || mDomThread.getLooper() == null) {
return;
}
mDomHandler.sendMessage(msg);
}
所以这个时候,我们的逻辑直接跳到这个handler对应的handleMessage()消息处理方法就可以了。而且这个WXDomHandler有一个比较重要的部分,必须强调。
public boolean handleMessage(Message msg) {
if (msg == null) {
return false;
}
int what = msg.what;
Object obj = msg.obj;
WXDomTask task = null;
if (obj instanceof WXDomTask) {
task = (WXDomTask) obj;
}
if (!mHasBatch) {
mHasBatch = true;
mWXDomManager.sendEmptyMessageDelayed(WXDomHandler.MsgType.WX_DOM_BATCH, DELAY_TIME);
}
switch (what) {
case MsgType.WX_DOM_CREATE_BODY:
mWXDomManager.createBody(task.instanceId, (JSONObject) task.args.get(0));
break;
case MsgType.WX_DOM_UPDATE_ATTRS:
mWXDomManager.updateAttrs(task.instanceId, (String) task.args.get(0), (JSONObject) task.args.get(1));
break;
case MsgType.WX_DOM_BATCH:
mWXDomManager.batch();
mHasBatch = false;
break;
default:
break;
}
return true;
}
这部分代码非常重要,是渲染的重要一环节,因为之后跟踪代码可以发现,createBody()这个方法只是创建对应的WxDomObject和WxDomComponent,然后将这个渲染任务放到队列去,这样如果任务并没有执行的话,是不会有界面渲染的结果的,必须有一个方法去获取这个渲染队列的所有任务并执行,才会最终反应到界面上来,而这个重要的组成部分就是mHasBatch。看上图,当执行到if (!mHasBatch)判断时,mHasBatch初始值是false,所以会执行发送延迟16ms的消息WXDomHandler.MsgType.WX_DOM_BATCH,然后还是由自己的handler接收处理,将mHasBatch = false重新设置,然后调用mWXDomManager.batch()执行渲染任务。而这两者可以构成一个简单的循环,这意味着每当有新的消息通知时,都会触动Batch消息从而刷新界面。这边先预留到,先把渲染界面的任务createBody()完成。
void createBody(String instanceId, JSONObject element) {
WXDomStatement statement = new WXDomStatement(instanceId, mWXRenderManager);
mDomRegistries.put(instanceId, statement);
statement.createBody(element);
}
WXDomManager的createBody(),会new 出新的WXDomStatement,如参数可知,这个WXDomStatement和instanceId也是一一对应的,同时也将这对关系存到mDomRegistries上,然后WXDomStatement开始执行自己的createBody()任务
void createBody(JSONObject dom) {
addDomInternal(dom,true,null,-1);
}
下面介绍的addDomInternal()也是非常重要的一环,这步骤里面决定了Dom里面所有元素的创建,组件的创建,渲染任何的创建,所以加重着墨在这个方法上。先补充一点,前面说到的createBody()方法,属于创建根root的方法,而其他节点一般走addDom()这个方法,具体可以看回源码。
先分析这个方法的参数提供了什么数据供我们使用:
addDomInternal(JSONObject dom,boolean isRoot, String parentRef, final int index);
下面先大体介绍对应的部分:
private void addDomInternal(JSONObject dom,boolean isRoot, String parentRef, final int index){
if (mDestroy) {
return;
}
WXSDKInstance instance = WXSDKManager.getInstance().getSDKInstance(mInstanceId);
if (instance == null) {
return;
}
WXErrorCode errCode = isRoot ? WXErrorCode.WX_ERR_DOM_CREATEBODY : WXErrorCode.WX_ERR_DOM_ADDELEMENT;
if (dom == null) {
instance.commitUTStab(IWXUserTrackAdapter.DOM_MODULE, errCode);
}
//only non-root has parent.
WXDomObject parent;
/**
* 这里会将对应的JSONObject 对象和WXSDKInstance 对象传递进去构造WXDomObject
*/
WXDomObject domObject = WXDomObject.parse(dom,instance);
/**
* 如下,判断是否根节点,如果是根节点,做了一些缺省值配置工作
* 如果是普通节点,获取其父节点WXDomObject,并添加这个儿子
*/
if (isRoot) {
WXDomObject.prepareRoot(domObject,
WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexHeight(mInstanceId),WXSDKManager.getInstanceViewPortWidth(mInstanceId)),
WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexWidth(mInstanceId),WXSDKManager.getInstanceViewPortWidth(mInstanceId)));
} else if ((parent = mRegistry.get(parentRef)) == null) {
instance.commitUTStab(IWXUserTrackAdapter.DOM_MODULE, errCode);
return;
} else {
//non-root and parent exist
parent.add(domObject, index);
}
/**
* 对特殊属性fixed的处理
*/
domObject.traverseTree(mAddDOMConsumer, ApplyStyleConsumer.getInstance());
//Create component in dom thread
/**
* 这里判断是否根节点,如果是根节点,执行createBodyOnDomThread()否则createComponentOnDomThread()创建
* 对应的WXComponent组件,到了这一步,组件已经创建完毕了,还没有渲染到界面
*/
WXComponent component = isRoot ?
mWXRenderManager.createBodyOnDomThread(mInstanceId, domObject) :
mWXRenderManager.createComponentOnDomThread(mInstanceId, domObject, parentRef, index);
/**
* 这次新增加的component会保存到mAddDom里面去,保留下次刷新
*/
AddDomInfo addDomInfo = new AddDomInfo();
addDomInfo.component = component;
mAddDom.put(domObject.getRef(), addDomInfo);
/**
* 这里面会创建对应component的渲染方法,然后add到mNormalTasks队列去,所有后面我们需要关注的是这个
* mNormalTasks什么时候会执行,否则界面不会刷新
*/
IWXRenderTask task = isRoot ? new CreateBodyTask(component) : new AddDOMTask(component, parentRef, index);
mNormalTasks.add(task);
addAnimationForDomTree(domObject);
//这个属性表明,这个界面现在脏了,可以执行刷新界面的工作
mDirty = true;
}
上面基本上已经对大部分方法分类说明了,整体思路其实很明确,先取对应的json创建我自己的WxDomObject,然后拿这个WxDomObject对象去创建对应的组件,然后将这个组件添加到准备要渲染的队列上面,后面等待合适的时机取出任何去渲染就可以了
如上面的的parse()方法:
WXDomObject parse(JSONObject json, WXSDKInstance wxsdkInstance) {
if (json == null || json.size() <= 0) {
return null;
}
String type = (String) json.get(TYPE);
WXDomObject domObject = WXDomObjectFactory.newInstance(type);
domObject.setViewPortWidth(wxsdkInstance.getViewPortWidth());
if (domObject == null) {
return null;
}
domObject.parseFromJson(json);
domObject.mDomContext = wxsdkInstance;
Object children = json.get(CHILDREN);
if (children != null && children instanceof JSONArray) {
JSONArray childrenArray = (JSONArray) children;
int count = childrenArray.size();
for (int i = 0; i < count; ++i) {
domObject.add(parse(childrenArray.getJSONObject(i), wxsdkInstance), -1);
}
}
return domObject;
}
public class WXDomObject extends CSSNode implements Cloneable, ImmutableDomObject {
/**
* package
**/
WXStyle mStyles;
/**
* package
**/
WXAttr mAttributes;
/**
* package
**/
WXEvent mEvents;
private List mDomChildren;
@Deprecated
public WXDomObject parent;
}
上面的第一步,判断类型,然后找出对应的WXDomObject,这一步其实在初始化Weex的时候就完成了,WXSDKEngine这个类有专门的register方法,会对系统里面常用WxComponent和WXDomObject进行注册。
这里不再具体展示。回到介绍的addDomInternal(),完成了parse任务后,需要通过这个WXDomObject创建对应的组件.
WXComponent component = isRoot ?
mWXRenderManager.createBodyOnDomThread(mInstanceId, domObject) :
mWXRenderManager.createComponentOnDomThread(mInstanceId,domObject, parentRef, index);
后面进来的方法generateComponentTree(),也只是通过工厂去找对应的组件,方法和WxDomObject类似,具体不表了
最后还是来到创建渲染任务的环节,这边我们直接看creatBody()的环节就可以了,普通节点的添加task如此类推
private class CreateBodyTask implements IWXRenderTask {
final WXComponent mComponent;
CreateBodyTask(WXComponent component) {
mComponent = component;
}
@Override
public void execute() {
try {
mWXRenderManager.createBody(mInstanceId, mComponent);
} catch (Exception e) {
}
}
这个任务也简单,只有一个execute()方法起实际作用,还记得前面add进去的队列吧,后面我们需要观察这个队列什么时候执行就可以确定具体渲染时机了
这边我们先进入CreateBodyTask的execute()方法,观察WXRenderManager如何createBody()
上面还是图示比较清晰,最后来到核心方法WXRenderStatement.createBody()上:
void createBody(WXComponent component) {
component.createView();
component.applyLayoutAndEvent(component);
component.bindData(component);
if (component instanceof WXScroller) {
WXScroller scroller = (WXScroller) component;
if (scroller.getInnerView() instanceof ScrollView) {
mWXSDKInstance.setRootScrollView((ScrollView) scroller.getInnerView());
}
}
mWXSDKInstance.onRootCreated(component);
if (mWXSDKInstance.getRenderStrategy() != WXRenderStrategy.APPEND_ONCE) {
mWXSDKInstance.onCreateFinish();
}
}
上面几个方法都是按顺序执行:
1.createView(),我们的组件类并非直接继承于view,而是利用了泛型持有,每个WxComponent有成员变量mHost,执行createView(),会创建对应的view
2.applyLayoutAndEvent(),方法作用如名字,将对应的WxDomObject取出,设置到Component上,部分属性会设置到刚才生成的mHost上。包括位置、边距、事件
3.bindData() ,将对应的WxDomObject取出,设置到组件上,包括style、attrs、extra
4.onRootCreated() 上面的根view已经初始化完毕了,这个时候可以设置到WXSDKInstance上了,将component返回,后面会提取component对应的View成员变量mHost.
public void onRootCreated(WXComponent root) {
this.mRootComp = root;
mRenderContainer.addView(root.getHostView());
}
mRenderContainer成功add到根View
5.onCreateFinish() 通知根元素创建完毕,会回调给所有的监听者,一般Activity会注册到这里,然后取出View设置到自己的ContentView中,展示界面
public void onCreateFinish() {
if (mContext != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if ( mContext != null) {
View wxView= mRenderContainer;
if(mRenderListener != null) {
mRenderListener.onViewCreated(WXSDKInstance.this, wxView);
}
}
}
});
}
}
基本上到了这里,根View就成功渲染出来了,后面其他的子节点也会走类似的流程一个个add到根view中,不再一一展示。
前面漏了1个内容一直没有提及,大家还记得我们的渲染任务是add到列表里头吗?这样并没有执行到具体的渲染工作,那么什么时候会调取这个列表执行呢?希望大家还记得我一开始提醒的Batch这个东西,WXDomHandler每次收到消息后,都会发一个batch消息,给16ms后的自己处理。我们的所有渲染任务,其实是在这里才被执行到!!
void batch() {
if (!mDirty || mDestroy) {
return;
}
WXDomObject rootDom = mRegistry.get(WXDomObject.ROOT);
layout(rootDom);
}
判断脏了和没有被销毁才有执行后面的layout()
void layout(WXDomObject rootDom) {
updateDomObj();
int count = mNormalTasks.size();
for (int i = 0; i < count && !mDestroy; ++i) {
mWXRenderManager.runOnThread(mInstanceId, mNormalTasks.get(i));
}
mNormalTasks.clear();
mAddDom.clear();
mDirty = false;
}
部分和布局有关的逻辑已经被删除了,前面一直保留着的mNormalTasks,保存所有渲染的任务,终于在这里被执行了,对应的mDirty重新设置为false ,标识已经“干净”.
至此,整个渲染流程完毕。