1-基本原理
VirtualView是天貓出品的組件級別的動態化方案,通過動態下發xml模板到客戶端,客戶端完成模板解析、數據綁定、事件處理等實現動態化。實際常用的應用場景如下:
- 按照VirtualView SDK中的原生或拓展組件編寫Xml模板,和Android中的佈局xml類似
- 將Xml模板解析爲二進制文件.out,和Android中xml文件解析原理類似,最終產物的格式規範不一樣。將產物上傳cdn
- 客戶端從cdn下載模板.out文件,然後就是文件MD5校驗,版本校驗然後添加到本地緩存。
- .out文件進行預解析,將一些輔助信息添加到緩存,提高後續解析效率
- 解析.out文件通過反射構建Native組件
- 根據xml中的屬性或表達式綁定數據到組件,並註冊相關事件監聽
- 渲染顯示到界面VirtualView容器中
SDK接入及實時預覽開發工具使用參照我之前寫的文章:
VirtualView接入及開發環境搭建
2-源碼解析
2.1-xml模板描述
模板描述比較簡單,SDK中提供了一些原子組件和佈局組件,也可以自定義組件。xml描述和Android中的xml很像,數據和事件綁定都是通過屬性賦值的方式實現,表達式類似簡版的DataBinding。子模板引用也是通過表達式來實現。
2.2-xml模板編譯
原理和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方法代碼較長就不貼出來了,有興趣可以去看源碼。該方法主要邏輯:
- (1)構建XmlPullParser解析器初始化解析參數
- (2)創建輔助存儲類,分別存儲組件區、字符串區、表達式區、組件個數
- (3)開始DOM解析,依次解析出各組件並將組件信息填入輔助存儲類,直至xml結束標記。
- (4)將輔助存儲類中的信息更新到.out文件對應的數據區域,同時更新頭文件中各區域的起始位置及長度信息。
總結下整個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
最後總結下事件處理邏輯: