版本:0.18.0
自己对于Weex源码学习的一些记录,帮助理解,如有错误请指出
这里是源码分析的第二篇,上一篇我们分析了Bridge模块WXBridgeManager,这一篇我们从WXDomManager入手开始,分析Dom中createBody的初始化流程。
WXDomManager:
**1、主要作用:**Dom树的创建、操作
**2、线程模型:**Dom Thread(注意,Handler的改变)
//初始化
public WXDomManager(WXRenderManager renderManager) {
mWXRenderManager = renderManager;
mDomRegistries = new ConcurrentHashMap<>();
mDomThread = new WXThread("WeeXDomThread", new WXDomHandler(this));
mDomHandler = mDomThread.getHandler();
}
可以看到,和Bridge一样,也是使用了一个HandlerThread,但是这里创建了一个新的Handler:WXDomHandler,为了后面的过程,我们先了解一下其主要特点:(对于不了解Handler机制的,可以先查阅相关资料)
(1)持有Manager对象
(2)重写了handleMessage函数
可以看出来,DomHandler在收到消息之后都会交由Manager处理(在Dom Thread中)
public boolean handleMessage(Message msg) {
if (msg == null) {
return false;
}
int what = msg.what;
Object obj = msg.obj;
WXDomTask task = null;
...
switch (what) {
case MsgType.WX_EXECUTE_ACTION:
mWXDomManager.executeAction(task.instanceId, (DOMAction) task.args.get(0), (boolean) task.args.get(1));
break;
case MsgType.WX_DOM_UPDATE_STYLE:
//keep this for direct native call
mWXDomManager.executeAction(task.instanceId, Actions.getUpdateStyle((String) task.args.get(0),
(JSONObject) task.args.get(1),
task.args.size() > 2 && (boolean) task.args.get(2)),false);
break;
case MsgType.WX_DOM_BATCH:
mWXDomManager.batch();
mHasBatch = false;
break;
case MsgType.WX_CONSUME_RENDER_TASKS:
mWXDomManager.consumeRenderTask(task.instanceId);
break;
default:
break;
}
return true;
}
3、过程分析
3.1 结论总结
(1)对于一个页面,需要为其创造一个WeexInstance,这个在使用api中就能体现出来
(2)对于DomManager,每一个WeexInstance对应一个DOMActionContextImpl(这个对于之前的版本有一些不同,实际上之前的版本成为一个Statement),这个在代码变量名中还能看出来
(3)DomManager负责管理instance到DOMActionContext的映射关系,使用一个HashMap:
private ConcurrentHashMap
public void postActionDelay(String instanceId,DOMAction action,
boolean createContext, long delay){
if(action == null){
return;
}
Message msg = Message.obtain();
msg.what = WXDomHandler.MsgType.WX_EXECUTE_ACTION;
WXDomTask task = new WXDomTask();
task.instanceId = instanceId;
task.args = new ArrayList<>();
task.args.add(action);
task.args.add(createContext);
msg.obj = task;
sendMessageDelayed(msg, delay);
}
而我们之前知道Handler中重写了handleMessage函数,根据WX_EXECUTE_ACTION,实际上是执行了Manager的executeAction函数,我们接着一步一步看。
步骤一:调用mWXDomManager.executeAction函数
(DOMAction) task.args.get(0), (boolean) task.args.get(1));
其中参数:
(1)instanceId,当前实例的id
(2)一个DomAction对象,我们可以看一下是从哪里传递过来的:
WXDomModule domModule = getDomModule(instanceId);
Action action = Actions.getCreateBody(domObject);
domModule.postAction((DOMAction) action, true);
实际上你可以看出来,在createBody这个操作中,创建了一个新的Action,由getCreateBody函数来创建,可以看一下其实现:
public static DOMAction getCreateBody(JSONObject data) {
return new CreateBodyAction(data);
}
实际上就是返回了一个新的CreateBodyAction对象,并且将domObject当做data传了进去,那么这个domObject来自哪里呢?实际上就是通过bridge,也就是js调用时传入的tasks数据进行创建的:
JSONObject domObject = JSON.parseObject(tasks);
(3)boolean值,表示是否createContext,在createBody操作中为true
**步骤二:**mWXDomManager.executeAction函数执行
(1)创建对应instance的DOMActionContextImpl对象,因为createContext为true,并且Manager中此时还没有这个instance对应的那个DOMActionContext对象:
DOMActionContext context = mDomRegistries.get(instanceId);
if(context == null){
if(createContext){
DOMActionContextImpl oldStatement = new DOMActionContextImpl(instanceId, mWXRenderManager);
mDomRegistries.put(instanceId, oldStatement);
context = oldStatement;
}else{
//错误处理
return;
}
}
(2)action的执行
action.executeDom(context);
我们上面已经说到了action的实际是一个CreateBodyAction对象,所以执行的实际上是CreateBodyAction的executeDom函数。
**步骤三:**CreateBodyAction.executeDom函数
其中参数:
(1)context,就是我们之前说的instance对应的DOMActionContext对象
执行步骤:
(1)调用addDomInternal
@Override
public void executeDom(DOMActionContext context) {
if (WXTracing.isAvailable()) {
//Tracing操作
}
addDomInternal(context, mData);
}
传入参数:
(1)context
(2)mData,实际上就是创建时传入的domObject:
CreateBodyAction(JSONObject data) {
mData = data;
}
(2)addDomInternal执行
2.1 获取了instance对象
WXSDKInstance instance = context.getInstance();
2.2 递归解析生成dom树,获取WXDomObject对象
WXDomObject domObject = WXDomObject.parse(dom, instance, null);
由于解析过程很复杂,单独一个步骤说,首先明确传入的参数
(1)dom,就是刚才的JSONObject对象
(2)instance,实例对象
(3)parentDomObject,第一次调用传入null,说明是根节点
步骤四:递归Dom树的解析——函数parse的调用
首先了解一下dom的JSONObject对象结构:
{
"attr":{"spmId":"spma"},
"ref":"_root",
"style":{},
"type":"div"
}
然后我们来看parse函数的调用过程
1、创建WXDomObject对象,这里采用了工厂模式,通过type设置其类型
WXDomObject domObject = WXDomObjectFactory.newInstance(type);
domObject.setViewPortWidth(wxsdkInstance.getInstanceViewPortWidth());
我们要了解一下WXDomObject,区分清楚它与后面提到的Component以及View,在源码中给出了很好的介绍:
/**
* WXDomObject contains all the info about the given node, including style, attribute and event.
* Unlike {@link com.taobao.weex.ui.component.WXComponent}, WXDomObject only contains info about
* the dom, has nothing to do with rendering.
* Actually, {@link com.taobao.weex.ui.component.WXComponent} hold references to
* {@link android.view.View} and {@link WXDomObject}.
*/
public class WXDomObject extends CSSNode implements Cloneable,ImmutableDomObject
实际上就是说,WXDomObject包含了一个节点的全部信息,但其和渲染没有关系,只是保留了信息,渲染的事情交给View来做,而Component更像是一个老大,有View和DomObject的引用。我们也能看到WXDomObject继承自CSSNode,而CSSNode在这里还用不到。
2、从json中解析信息
domObject.parseFromJson(json);
domObject.mDomContext = wxsdkInstance;
domObject.parent = parentDomObject;
parseFromJson做的事情很简单,就是从Json中把相应的属性解析出来赋值给domObject:
//主要包括type、ref、style、attr以及event
String type = (String) map.get("type");
this.mType = type;
this.mRef = (String) map.get("ref");
...//省略部分代码
Object event = map.get("event");
if (event != null && event instanceof JSONArray) {
WXEvent events = new WXEvent();
JSONArray eventArray = (JSONArray) event;
int count = eventArray.size();
for (int i = 0; i < count; i++) {
Object value = eventArray.get(i);
events.addEvent(value);
}
this.mEvents = events;
}
之后再设置DomContext以及parent,这样当前节点就创建完毕了。
3、递归开始创建子节点,最终生成完整Dom树
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, domObject),-1);
}
}
通过add函数把子节点添加到树结构中,这样递归就可以生成一个完整的Dom树。
**步骤五:**Dom树生成完成,返回步骤三的addDomInternal函数,调用appendDomToTree
这里已经返回了步骤三继续执行下一步,下一步执行了appendDomToTree(context, domObject)函数,其中参数仍然要强调一下:
(1)context:这里仍是instance对应的DOMActionContext对象
(2)domObject:生成的Dom树的根节点
appendDomToTree主要就是针对根节点进行一些设置,具体调用函数如下:
String instanceId = context.getInstanceId();
WXDomObject.prepareRoot(domObject,
WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexHeight(instanceId), WXSDKManager.getInstanceViewPortWidth(instanceId)),
WXViewUtils.getWebPxByWidth(WXViewUtils.getWeexWidth(instanceId), WXSDKManager.getInstanceViewPortWidth(instanceId)));
所以使用的是prepareRoot函数:(这里的domObj是根节点)
public static void prepareRoot(WXDomObject domObj,float defaultHeight,float defaultWidth) {
domObj.mRef = WXDomObject.ROOT;
WXStyle domStyles = domObj.getStyles();
Map<String, Object> style = new HashMap<>(5);
if (!domStyles.containsKey(Constants.Name.FLEX_DIRECTION)) {
style.put(Constants.Name.FLEX_DIRECTION, "column");
}
style.put(Constants.Name.DEFAULT_WIDTH, defaultWidth);
style.put(Constants.Name.DEFAULT_HEIGHT, defaultHeight);
domObj.updateStyle(style);
}
所以这里也证实了为什么weex最外层节点默认是flex列布局,这里就进行了设置。
步骤六:遍历Dom节点,递归调用traverseTree函数
domObject.traverseTree(
context.getAddDOMConsumer(),
context.getApplyStyleConsumer()
);
这里传入了两个Consumer,都是由Context得到的,首先能看出这里采用了一个Consumer模式,具体来说就是传入一个数组,针对每一个Consumer都调用其accept进行消费,具体代码如下:
public void traverseTree(Consumer...consumers){
...
for (Consumer consumer:consumers){
consumer.accept(this); //进行消费,传入对象时自身(当前节点对象)
}
//递归调用
int count = childCount();
WXDomObject child;
for (int i = 0; i < count; ++i) {
child = getChild(i);
child.traverseTree(consumers);
}
}
这里我们可以看到后面的递归调用,也就是说对于当前dom树的所有dom节点,都进行相应的consumer的accept操作,而这里我们传入了两个consumer,来看一看具体操作:
- AddDOMConsumer
那么我们首先看第一个Consumer,实现在DOMActionContextImpl中,也就是我们之前说的一个instance相对应的DOMActionContext的实现类。它的主要作用是负责当前instance的Dom对象创建过程以及相关操作的Context作用,这个从名字就可以看出来,它实现了第一个Consumer:AddDOMConsumer,我们来看一下accept函数:
@Override
public void accept(WXDomObject dom) {
//register dom
dom.young();
mRegistry.put(dom.getRef(), dom);
//find fixed node
WXDomObject rootDom = mRegistry.get(WXDomObject.ROOT);
if (rootDom != null && dom.isFixed()) {
rootDom.add2FixedDomList(dom.getRef());
}
}
完成以下两个操作:
(1)首先把所有dom节点放入mRegistry中并置为young
(2)然后找到root节点,如果当前dom节点为fixed,就把他放入root节点的Fixed节点列表中
这里也可以反映出DOMActionContext作为管理者的作用,对于当前instance的所有Dom节点,它使用mRegistry来进行持有:
final ConcurrentHashMap<String, WXDomObject> mRegistry;
- ApplyStyleConsumer
我们来看第二个Consumer,它使用的是单例模式实现,其accept函数如下:
@Override
public void accept(WXDomObject dom) {
WXStyle style = dom.getStyles();
/** merge default styles **/
//把default style设置上去,合并style
Map<String, String> defaults = dom.getDefaultStyle();
if (defaults != null) {
for (Map.Entry<String, String> entry : defaults.entrySet()) {
if (!style.containsKey(entry.getKey())) {
style.put(entry.getKey(), entry.getValue());
}
}
}
//设置style到之前说的CSSNode
if (dom.getStyles().size() > 0) {
dom.applyStyleToNode();
}
}
这里同样完成两个操作
(1)如果当前Dom节点有自己的default style,进行合并
(2)对当前dom调用applyStyleToNode,实际上就是把当前dom的style应用到父类,也就是我们之前提到的CSSNode
步骤七:创建component树,调用createComponent函数,这里的操作仍然在CreateBodyAction中:
WXComponent component = createComponent(context, domObject);
if (component == null) {
return;
}
这里传入的参数还是没变:
(1)context:这里仍是instance对应的DOMActionContext对象
(2)domObject:生成的Dom树的根节点
@Override
protected WXComponent createComponent(DOMActionContext context, WXDomObject domObject) {
return generateComponentTree(context, domObject, null);
}
可以看到,调用了generateComponentTree函数,最后一个参数为null,这里我们又可以看到是一个递归调用过程,类比之前Dom树的生成:
protected WXComponent generateComponentTree(DOMActionContext context, WXDomObject dom, WXVContainer parent) {
...
WXComponent component = WXComponentFactory.newInstance(context.getInstance(), dom, parent);
...
context.registerComponent(dom.getRef(), component);
if (component instanceof WXVContainer) {
WXVContainer parentC = (WXVContainer) component;
int count = dom.childCount();
WXDomObject child = null;
for (int i = 0; i < count; ++i) {
child = dom.getChild(i);
if (child != null) {
WXComponent createdComponent = generateComponentTree(context, child, parentC);
if(createdComponent != null) {
parentC.addChild(createdComponent);
}else{
WXLogUtils.e("[generateComponentTree] " + getStatementName() + " create dom component failed name " + child.getType());
WXExceptionUtils.commitCriticalExceptionRT(context.getInstanceId(), getErrorCode().getErrorCode(), "generateComponentTree", " create dom component failed name " + child.getType(), null);
}
}
}
}
...
return component;
}
具体步骤如下
(1)首先根据dom的type类型创建相应的Component
(2)如果是WXVContainer,也就是有子对象的(其View为一个ViewGroup),添加其子Component,递归调用从而生成整个Component树
这里我们只需要简单知道一下WXComponent实际上一一对应一个虚拟的DomObject和一个实际的Android View即可,后面应该会逐步了解其更多内容
步骤八:为Context添加addDom任务
直接看代码就能理解,如果是cell-slot类型或者是WXCellDomObject,就需要将component添加到instance对应的Context的mAddDom列表中:
boolean needAddDomInfo = true;
if(domObject.getType().equals(WXBasicComponentType.CELL_SLOT)
&& domObject instanceof WXCellDomObject){
needAddDomInfo = false;
}
if(needAddDomInfo) {
context.addDomInfo(domObject.getRef(), component);
}
//addDomInfo操作如下:
AddDomInfo addDomInfo = new AddDomInfo();
addDomInfo.component = component;
mAddDom.put(ref, addDomInfo);
步骤九:为Context添加Render任务,调用postRenderTask函数
context.postRenderTask(this);
这个调用中将自身作为参数传入进去,这里我们要知道这个this对应的就是我们一直分析的CreateBodyAction,它实现了RenderAction接口(在父类AbstractAddElementAction中实现)
然后我们看DOMActionContextImpl中的实现:
public void postRenderTask(RenderAction action) {
mNormalTasks.add(new RenderActionTask(action, mWXRenderManager.getRenderContext(mInstanceId)));
mDirty = true;
}
(1)实际上这里就是创建了一个新的RenderActionTask,放入自己的Task列表中,这里传入的RenderAction就是CreateBodyAction实现的,而这里传入的RenderContext又RenderManager得到,而其也是和instance对象一一对应关系,是不是很熟悉。
(2)把当前DOMActionContext置为dirty
4、总结
到这里我们对于Dom中createBody的整个操作就已经了解清晰了。通过createBody这一初始化操作,我们能够了解到DomManager在这一过程中的主要作用:从js代码中,建立Dom树以及Component树,完成一系列节点的设置以及初始化过程。并且应该清楚这些操作都是在Dom单独的Dom线程中完成的。
其根本是做了两件事:
(1)Json数据 —> Dom树
(2)Dom树 —> Component树
但在了解这个过程的同时,我们也应该清楚关于WXDomManager的相关类的关系:
(1)WXDomManager作为Dom模块的管理者,掌管着每个页面的Dom
(2)每个页面一一对应于一个instance实例,而在Dom中,每一个instance又对应于一个DomContext,DomContext统一由WXDomManager来管理
(3)对于每一个页面的Dom节点,由其对应的DomContext来进行管理