讀源碼-VirtualView源碼解析

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文件格式參考官方文檔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編譯流程:
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

最後總結下事件處理邏輯:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章