Weex Android源码分析(二)—— WXDomManager

版本: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来进行管理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值