读源码-VirtualView源码解析

2,484 阅读12分钟

1-基本原理

VirtualView是天猫出品的组件级别的动态化方案,通过动态下发xml模板到客户端,客户端完成模板解析、数据绑定、事件处理等实现动态化。实际常用的应用场景如下:

VirtualView基本原理

  1. 按照VirtualView SDK中的原生或拓展组件编写Xml模板,和Android中的布局xml类似
  2. 将Xml模板解析为二进制文件.out,和Android中xml文件解析原理类似,最终产物的格式规范不一样。将产物上传cdn
  3. 客户端从cdn下载模板.out文件,然后就是文件MD5校验,版本校验然后添加到本地缓存。
  4. .out文件进行预解析,将一些辅助信息添加到缓存,提高后续解析效率
  5. 解析.out文件构建Native组件
  6. 根据xml中的属性或表达式绑定数据到组件,并注册相关事件监听
  7. 渲染显示到界面VirtualView容器中

SDK接入及实时预览开发工具使用参照我之前写的文章:
VirtualView接入及开发环境搭建

2-源码解析

2.1-xml模板描述

模板描述比较简单,SDK中提供了一些原子组件和布局组件,也可以自定义组件。xml描述和Android中的xml很像,数据和事件绑定都是通过属性赋值的方式实现,表达式类似简版的DataBinding。子模板引用也是通过表达式来实现。

模板描述

2.2-xml模板编译

原理和Android解析xml类似,.out文件格式参考官方文档tangram.pingguohe.net/docs/virtua…

xml模板编译的工作就是通过xml解析器将文件中的信息按照固定格式填充到.out文件中

xml编译成.out文件这部分代码在virtualview_tools开发工具包里的VirtualViewCompileTool.java。将制定目录下的xml目标编译成.out文件。 VirtualViewCompileTool.main-->VirtualViewCompileTool.compileInProject-->VirtualViewCompileTool.compile

这些代码略过,主要是一些解析准备工作,包括各组件解析器的注册、产物文件创建、解析参数配置等。VirtualViewCompileTool.compile

static private void compile(String readDir, List<Template> paths, String buildPath) {
    。。。//代码省略
    //遍历需要编译的xml文件
    for (Template resourceNode : paths) {
        。。。//代码省略
        
        //@1.构建.out文件及文件头信息
        if (compiler.newOutputFile(path, 1, resourceNode.version)) {
                //@2.通过xml解析器解析DOM信息,并填入.out文件
                if (!compiler.compile(resourceNode.type, resourceNode.templatePath)) {
                    System.out.println("compile file error --> " + path);
                    continue;
                }
                ret = compiler.compileEnd();
                if (!ret) {
                    System.out.println("compile file end error --> " + path);
                } else {
                     //输出二进制java文件。VV除了可以加载.out二进制,也可以加载java二进制文件。
                    //java类形式不做详解,原理类似
                    compileByProduce(path, resourceNode.type, bytePath, textPath, signPath);
                }
        } else {
                System.out.println("new output file failed --> " + path);
        }   
    }
}

@1.构建.out文件及文件头信息。这部分逻辑就是创建一个.out文件,并填入固定的头部内容,魔数+版本号,组件区、字符串区、表达式区占位。

public boolean newOutputInit(int pageId, int[] depPageIds, int patchVersion) {
        mPageId = pageId;//模板名
        mStringStore.setPageId(mPageId);//字符串区辅助存储
        mExprCodeStore.setPageId(mPageId);//表达式区辅助存储
        //RandomAccessMemByte存储解析后的二进制内容
        mMemByte = new RandomAccessMemByte();
        if (null != mMemByte) {
            // 开头固定5个字节的魔数ALIVV
            mMemByte.write(Common.TAG.getBytes());
            // 2字节主版本号+2字节次版本号+2字节业务版本号
            mMemByte.writeShort(Common.MAJOR_VERSION);
            mMemByte.writeShort(Common.MINOR_VERSION);
            mMemByte.writeShort(patchVersion);
            // 组件区起始位置
            mMemByte.writeInt(0);
            // 组件区长度
            mMemByte.writeInt(0);
            // 字符串区起始位置
            mMemByte.writeInt(0);
            // 字符串区长度
            mMemByte.writeInt(0);
            // 表达式区起始位置
            mMemByte.writeInt(0);
            // 表达式区长度
            mMemByte.writeInt(0);
            // 预留extra区起始位置,暂未用到
            mMemByte.writeInt(0);
            // extra区长度
            mMemByte.writeInt(0);
            // 模板ID
            mMemByte.writeShort(pageId);
            // 依赖模板ID
            if (null != depPageIds) {
                mMemByte.writeShort(depPageIds.length);
                for (int i = 0; i < depPageIds.length; ++i) {
                    mMemByte.writeShort(depPageIds[i]);
                }
            } else {
                mMemByte.writeShort(0);
            }
            //文件头长度
            mCodeStartOffset = (int) mMemByte.length();
            //组件个数,用0占位
            mMemByte.writeInt(0);
            return true;
        } else {
            return false;
        }
}

@2.通过XmlPullParser解析xml文件,提取DOM节点信息。并写入.out文件对应的区域。ViewCompiler.compile方法代码较长就不贴出来了,有兴趣可以去看源码。该方法主要逻辑:

  • (1)构建XmlPullParser解析器初始化解析参数
  • (2)创建辅助存储类,分别存储组件区、字符串区、表达式区、组件个数
  • (3)开始DOM解析,依次解析出各组件并将组件信息填入辅助存储类,直至xml结束标记。
  • (4)将辅助存储类中的信息更新到.out文件对应的数据区域,同时更新头文件中各区域的起始位置及长度信息。

总结下整个XML编译流程:

XML解析流程图

2.3-.out预解析

xml编译成.out产物后,将.out文件发布到CDN,客户端下载后进行校验,校验完毕再缓存到本地,然后进行异步的预解析。

解析过程只负责提取原始数据和组织格式,并未构建组件对象。反序列化字符串、表达式 建立索引位置与组件、位置与字符串、位置与表达式的映射关系。这些工作可以大大提高后面解析构建组件的效率。

在客户端初始化VirtualView后,通过ViewManager来进行预解析。直接⏩到关键代码 ViewManger.loadBinFileSync-->ViewFactory.loadBinFile-->BinaryLoader.loadFromFile-->BinaryLoader.loadFromBuffer。

public int loadFromBuffer(byte[] buf, boolean override) {
    int ret = -1;
    if (null != buf) {
        mDepPageIds = null;
        //字节长度必须超过27才是有效的,27即文件头部分
        if (buf.length > 27) {
            // 校验ALIVV魔数
            byte[] tagArray = Arrays.copyOfRange(buf, 0, Common.TAG.length());
            if (Arrays.equals(Common.TAG.getBytes(), tagArray)) {   
                //通过CodeReader辅助代码解析
                CodeReader reader = new CodeReader();
                reader.setCode(buf);
                //跳过魔数区
                reader.seekBy(Common.TAG.length());
                // 校验主+副+修订版本号
                int majorVersion = reader.readShort();
                int minorVersion = reader.readShort();
                int patchVersion = reader.readShort();
                reader.setPatchVersion(patchVersion);
                if ((Common.MAJOR_VERSION == majorVersion) && (Common.MINOR_VERSION == minorVersion)) {
                    //组件区起始位置
                    int uiStartPos = reader.readInt();
                    reader.seekBy(4);
                    //字符串区起始位置
                    int strStartPos = reader.readInt();
                    reader.seekBy(4);
                    //表达式区起始位置
                    int exprCodeStartPos = reader.readInt();
                    reader.seekBy(4);
                    //拓展区起始位置
                    int extraStartPos = reader.readInt();
                    reader.seekBy(4);
                    //模板ID
                    int pageId = reader.readShort();
                    //依赖模板数
                    int depPageCount = reader.readShort();
                    //获取依赖模板ID数组
                    if (depPageCount > 0) {
                        mDepPageIds = new int[depPageCount];
                        for (int i = 0; i < depPageCount; ++i) {
                            mDepPageIds[i] = reader.readShort();
                        }
                    }
                    if (reader.seek(uiStartPos)) {
                        // @3.预解析组件区
                        boolean result = false;
                        if (!override) {
                            result = mUiCodeLoader.loadFromBuffer(reader, pageId, patchVersion);
                        } else {
                            result = mUiCodeLoader.forceLoadFromBuffer(reader, pageId, patchVersion);
                        }

                        // @4.预解析字符串区
                        if (reader.getPos() == strStartPos) {
                            if (null != mStringLoader) {
                                result = mStringLoader.loadFromBuffer(reader, pageId);
                            } else {
                                Log.e(TAG, "mStringManager is null");
                            }
                        } else {
                            if (BuildConfig.DEBUG) {
                                Log.e(TAG, "string pos error:" + strStartPos + "  read pos:" + reader.getPos());
                            }
                        }

                        // @5.预解析表达式区
                        if (reader.getPos() == exprCodeStartPos) {
                            if (null != mExprCodeLoader) {
                                result = mExprCodeLoader.loadFromBuffer(reader, pageId);
                            } else {
                                Log.e(TAG, "mExprCodeStore is null");
                            }
                        } else {
                            if (BuildConfig.DEBUG) {
                                Log.e(TAG, "expr pos error:" + exprCodeStartPos + "  read pos:" + reader.getPos());
                            }
                        }

                        // 解析拓展区
                        if (reader.getPos() == extraStartPos) {
                        } else {
                            if (BuildConfig.DEBUG) {
                                Log.e(TAG, "extra pos error:" + extraStartPos + "  read pos:" + reader.getPos());
                            }
                        }

                        if (result) {
                            ret = pageId;
                        }
                    }
                } else {
                    Log.e(TAG, "version dismatch");
                }
            } else {
                Log.e(TAG, "loadFromBuffer failed tag is invalidate.");
            }
        } else {
            Log.e(TAG, "file len invalidate:" + buf.length);
        }
    } else {
        Log.e(TAG, "buf is null");
    }
    return ret;
}

@3.预解析组件区。对应UiCodeLoader.loadFromBuffer

public boolean loadFromBuffer(CodeReader reader, int pageId, int patchVersion) {
    boolean ret = true;

    int count = reader.readInt();
    //count should be 1
    short nameSize = reader.readShort();
    //将组件名反序列化解析出字符串
    String name = new String(reader.getCode(), reader.getPos(), nameSize, Charset.forName("UTF-8"));
    。。。//代码省略
    //存储解析出来的信息映射关系
    ret = loadFromBufferInternally(reader, nameSize, name);
    return ret;
}

主要是更新该组件的映射关系:

  • mTypeToCodeReader 组件名-字节码
  • mTypeToPos 组件名-字节码该组件索引

@4.预解析字符串区。通过StringLoader.loadFromBuffer来解析

public boolean loadFromBuffer(CodeReader reader, int pageId) {
    boolean ret = true;

    mCurPage = pageId;

    int totalSize = reader.getMaxSize();//字符串区总长度
    int count = reader.readInt();//字符串个数
    for (int i = 0; i < count; ++i) {
        int id = reader.readInt();//字符串HashCode
        int len = reader.readShort();//字符串长度
        int pos = reader.getPos();//字符串索引
        if (pos + len <= totalSize) {
            //反序列化出字符串
            String str = new String(reader.getCode(), reader.getPos(), len);
            //字符串HashCode-字符串String的映射
            mIndex2String.put(id, str);
            //字符串String-字符串HashCode的映射
            mString2Index.put(str, id);
            reader.seekBy(len);
        } else {
            Log.e(TAG, "read string over");
            ret = false;
            break;
        }
    }
    return ret;
}

主要是更新该字符串的映射关系:

  • mIndex2String 字符串HashCode-字符串String的映射
  • mString2Index 字符串String-字符串HashCode的映射

@5.预解析表达式区。通过ExprCodeLoader.loadFromBuffer来解析表达式区。代码和解析字符串类似不再列出,因为表达式也是字符串描述。主要更新表达式的映射关系:

  • mCodeMap 表达式字符串的hashCode-表达式封装类ExprCode

总结下.out文件的预解析流程:

在这里插入图片描述

2.4-.out解析构建组件

先从构建VirtualView代码入手:

View container = vafContext.getContainerService().getContainer(name, true);
mLinearLayout.addView(container);

快进到关键代码,VafContext.getContainerService-->ContainerService.getContainer-->ViewManager.getView-->ViewFactory.newView

public ViewBase newView(String type, SparseArray<ViewBase> uuidContainers) {
    ViewBase ret = null;
    //mLoader即预编译过程中的BinaryLoader
    if (null != mLoader) {
        CodeReader cr = null;
        synchronized (LOCK) {
            //尝试从内存中获取CodeReader,即预编译结果
            cr = mUiCodeLoader.getCode(type);
            if (cr == null) {
                //获取失败,则执行同步预编译方法获取预编译CodeReader
                Log.d(TAG, "load " + type + " start when createView ");
                mTmplWorker.executeTask(type);
                cr = mUiCodeLoader.getCode(type);
            }
        }
        if (null != cr) {
            mComArr.clear();//组件栈清空,用于存储父布局
            ViewBase curView = null;
            
            int tag = cr.readByte();
            int state = STATE_continue;
            ViewCache viewCache = new ViewCache();//用于缓存同一组件的属性item
            while (true) {
                switch (tag) {
                    //组件描述开始tag
                    case Common.CODE_START_TAG:
                        short comID = cr.readShort();//组件名
                        //@6.根据组件名创建对应的View并缓存到viewCache
                        ViewBase view = createView(mAppContext, comID, viewCache);
                        if (null != view) {
                            Layout.Params p;
                            if (null != curView) {
                                p = ((Layout) curView).generateParams();
                                //将前一个组件入栈,父布局
                                mComArr.push(curView);
                            } else {
                                //根布局
                                p = new Layout.Params();
                            }
                            //设置布局参数
                            view.setComLayoutParams(p);
                            curView = view;

                            // 解析int类型属性并设置
                            byte attrCount = cr.readByte();
                            while (attrCount > 0) {
                                int key = cr.readInt();
                                int value = cr.readInt();
                                view.setValue(key, value);
                                --attrCount;
                            }

                            // 解析rp单位类型属性并设置
                            // rp是相对视觉稿宽度单位
                            // 实际值 = rp * 屏幕宽度 / 750
                            attrCount = cr.readByte();
                            while (attrCount > 0) {
                                int key = cr.readInt();
                                int value = cr.readInt();
                                view.setRPValue(key, value);
                                --attrCount;
                            }

                            。。。//省略解析其他类型属性

                            int uuid = view.getUuid();
                            if (uuid > 0 && null != uuidContainers) {
                                //添加View到缓存
                                uuidContainers.put(uuid, view);
                            }
                            //待解析的属性item列表为空
                            //表明该组件解析完毕
                            List<Item> pendingItems = viewCache.getCacheItem(view);
                            if (pendingItems == null || pendingItems.isEmpty()) {
                                view.onParseValueFinished();
                            }
                        } else {
                            state = STATE_failed;
                            Log.e(TAG, "can not find view id:" + comID);
                        }
                        break;
                    //组件描述结束tag
                    case Common.CODE_END_TAG:
                        //组件栈中有父组件
                        if (mComArr.size() > 0) {
                            //如果父组件是布局组件,将当前组件添加到父组件
                            ViewBase c = mComArr.pop();
                            if (c instanceof Layout) {
                                ((Layout) c).addView(curView);
                            } else {
                                state = STATE_failed;
                                Log.e(TAG, "com can not contain subcomponent");
                            }
                            curView = c;
                        } else {
                            // can break;
                            state = STATE_successful;
                        }
                        break;

                    default:
                        Log.e(TAG, "invalidate tag type:" + tag);
                        state = STATE_failed;
                        break;
                }

                if (STATE_continue != state) {
                    break;
                } else {
                    tag = cr.readByte();
                }
            }
            //解析模板版本号
            if (STATE_successful == state) {
                ret = curView;
                cr.seek(Common.TAG.length() + 4);
                int version = cr.readShort();
                ret.setVersion(version);
            }
        } else {
            Log.e(TAG, "can not find component type:" + type);
        }
    } else {
        Log.e(TAG, "loader is null");
    }

    return ret;
}

@6.根据组件名创建对应的View并缓存到viewCache。通过调用ViewBase.build方法返回对应的View。ViewBase是所有VirtualView组件的父类。例如看下原子组件NText是怎么创建View的。标签在配置文件中注册的实现组件是NativeText

public class NativeText extends TextBase {
    private final static String TAG = "NativeText_TMTEST";
    protected NativeTextImp mNative;

    。。。//代码省略

    public NativeText(VafContext context, ViewCache viewCache) {
        super(context, viewCache);
        //创建TextView
        mNative = new NativeTextImp(context.forViewConstruction());
    }
    public static class Builder implements ViewBase.IBuilder {
        @Override
        public ViewBase build(VafContext context, ViewCache viewCache) {
            return new NativeText(context, viewCache);
        }
    }

NativeText其实是个代理类,具体实现是NativeTextImp,NativeTextImp就是继承Android原生组件TextView。NativeText的工作就是解析VV协议中的属性,然后赋值给NativeTextImp,并代理了NativeTextImp的measure、layout、setText等方法。

所以调用NativeText.build构建组件也就是创建了一个TextView并将解析的属性赋值。

总结下.out文件解析构建组件的流程:

在这里插入图片描述

2.5-数据绑定

数据绑定,先看下代码实现:

IContainer iContainer = (IContainer)container;
JSONObject json = getJSONDataFromAsset(data);
if (json != null) {
    iContainer.getVirtualView().setVData(json);
}

核心代码是ViewBase.setVData

final public void setVData(Object data, boolean isAppend) {
    if (VERSION.SDK_INT >= 18) {
        Trace.beginSection("ViewBase.setVData");
    }
    mViewCache.setComponentData(data);
    if (data instanceof JSONObject) {
        boolean invalidate = false;
        if (((JSONObject) data).optBoolean(FLAG_INVALIDATE)) {
            invalidate = true;
        }
        //cacheView是上一步组件构建时产生的,当前模板所有组件缓存
        List<ViewBase> cacheView = mViewCache.getCacheView();
        if (cacheView != null) {
            for (int i = 0, size = cacheView.size(); i < size; i++) {
                ViewBase viewBase = cacheView.get(i);
                //获取需要绑定数据的属性item列表
                List<Item> items = mViewCache.getCacheItem(viewBase);
                if (null != items) {
                    for (int j = 0, length = items.size(); j < length; j++) {
                        Item item = items.get(j);
                        if (invalidate) {
                            //清除缓存值
                            item.invalidate(data.hashCode());
                        }
                        //通过表达式来解析json中对应的值并赋值
                        item.bind(data, isAppend);
                    }
                    viewBase.onParseValueFinished();
                    if (!viewBase.isRoot() && viewBase.supportExposure()) {
                       //如果非根布局,且设置Exposure监听,则触发Exposure事件
                       mContext.getEventManager().emitEvent(EventManager.TYPE_Exposure,
                                EventData
                                        .obtainData(mContext, viewBase));
                    }

                }
            }
        }
        ((JSONObject) data).remove(FLAG_INVALIDATE);
    } else if (data instanceof com.alibaba.fastjson.JSONObject) {
        。。。//FastJson方式,原理同上
    }
    if (VERSION.SDK_INT >= 18) {
        Trace.endSection();
    }
}

总结下数据绑定流程:

在这里插入图片描述

2.6-事件处理

VirtualView默认支持四种事件,点击、长按、触摸、曝光。

这里的曝光在2.5节中数据绑定出现过,可以得知组件设置了flag="flag_exposure"后,在组件数据绑定完成时会触发“曝光”事件

点击、长按、触摸原理类似,都是通过解析flag属性后,对构建出的View设置onClick、onLongClick、onTouch监听。

以监听点击事件为例:

vafContext.getEventManager().register(EventManager.TYPE_Click, new IEventProcessor() {
    @Override
    public boolean process(EventData data) {
        //handle here
        return true;
    }
});

代码比较简单易懂,VirtualView这些事件都是通过EventManger来管理及事件分发的。EventManger中维护了一个数组,数组中存储的是对应事件的监听者列表。EventManager中只有三个方法:

  • register 根据事件类型将监听对象添加到对应的列表
  • unregister 将监听对象从列表中移除
  • emitEvent 分发事件
public class EventManager {
    private final static String TAG = "EventManager_TMTEST";
    //事件类型
    public final static int TYPE_Click = 0;
    public final static int TYPE_Exposure = 1;
    public final static int TYPE_Load = 2;
    public final static int TYPE_FlipPage = 3;
    public final static int TYPE_LongCLick = 4;
    public final static int TYPE_Touch = 5;
    public final static int TYPE_COUNT = 6;
    //监听者列表数组
    private Object[] mProcessor = new Object[TYPE_COUNT];
    //根据事件类型将监听对象添加到对应的列表
    public void register(int type, IEventProcessor processor) {
        if (null != processor && type >= 0 && type < TYPE_COUNT) {
            List<IEventProcessor> pList = (List<IEventProcessor>)mProcessor[type];
            if (null == pList) {
                pList = new ArrayList<>();
                mProcessor[type] = pList;
            }
            pList.add(processor);
        } else {
            Log.e(TAG, "register failed type:" + type + "  processor:" + processor);
        }
    }
    //将监听对象从列表中移除
    public void unregister(int type, IEventProcessor processor) {
        if (null != processor && type >= 0 && type < TYPE_COUNT) {
            List<IEventProcessor> pList = (List<IEventProcessor>)mProcessor[type];
            if (null != pList) {
                pList.remove(processor);
            }
        } else {
            Log.e(TAG, "unregister failed type:" + type + "  processor:" + processor);
        }
    }
    //分发事件
    public boolean emitEvent(int type, EventData data) {
        boolean ret = false;
        if (type >= 0 & type < TYPE_COUNT) {
            //根据事件类型取出对应的监听者列表
            List<IEventProcessor> pList = (List<IEventProcessor>)mProcessor[type];
            if (null != pList) {
                //遍历监听者列表,调用其process方法处理事件EventData
                for (int i = 0, size = pList.size(); i < size; i++) {
                    IEventProcessor p = pList.get(i);
                    ret = p.process(data);
                }
            }
        }
        if (null != data) {
            data.recycle();
        }

        return ret;
    }
}

从事件分发的代码来看,还有一些不完善的地方需要注意:

(1) 当某个VirtualView组件触发了onClick事件,将事件参数交由EventManager分发时,EventManger会分发给该事件所有监听者。也就是说其他设置了onClick事件的View也会回调process方法。所以需要通过EventData中的模板或组件tag来区分是否处理该事件。

(2) 事件分发没有线程切换操作,即回调处理是在监听方法执行的线程中,因此若是在子线程监听,则回调中无法操作UI

最后总结下事件处理逻辑:

在这里插入图片描述