VirtualView是天猫出品的组件级别的动态化方案,通过动态下发xml模板到客户端,客户端完成模板解析、数据绑定、事件处理等实现动态化。实际常用的应用场景如下:
SDK接入及实时预览开发工具使用参照我之前写的文章:
VirtualView接入及开发环境搭建
模板描述比较简单,SDK中提供了一些原子组件和布局组件,也可以自定义组件。xml描述和Android中的xml很像,数据和事件绑定都是通过属性赋值的方式实现,表达式类似简版的DataBinding。子模板引用也是通过表达式来实现。
原理和Android解析xml类似,.out文件格式参考官方文档http://tangram.pingguohe.net/docs/virtualview/bin-format
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方法代码较长就不贴出来了,有兴趣可以去看源码。该方法主要逻辑:
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;
}
主要是更新该组件的映射关系:
@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;
}
主要是更新该字符串的映射关系:
@5.预解析表达式区。通过ExprCodeLoader.loadFromBuffer来解析表达式区。代码和解析字符串类似不再列出,因为表达式也是字符串描述。主要更新表达式的映射关系:
先从构建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并将解析的属性赋值。
数据绑定,先看下代码实现:
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();
}
}
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中只有三个方法:
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