前言
- 淘寶、天貓一直致力于解決 頁面動態化的問題
- 在2017年的4月發布了
v1.0
解決方案:Tangram
模型 及其對應的Android
庫vlayout
,該解決方案在手機淘寶、天貓Android
版 內廣泛使用
若還不了解
Tangram
模型 和vlayout
,具體請看文章
- 在同年的12月,阿里團隊對此作了重大更新:發布了
Tangram2.0
版本,主要是補充了Android
庫VirtualView
,也廣泛應用于淘寶、天貓客戶端
示意圖
- 今天,我將帶大家全面了解
Tangram 2.0
版本的新成員:Virtualview
Virtualview的Github地址
Carson帶你學Android開源庫系列文章:
Carson帶你學Android:主流開源圖片加載庫對比(UIL、Picasso、Glide、Fresco)
Carson帶你學Android:主流開源網絡請求庫對比(Volley、OkHttp、Retrofit)
Carson帶你學Android:網絡請求庫Retrofit使用教程
Carson帶你學Android:網絡請求庫Retrofit源碼分析
Carson帶你學Android:圖片加載庫Glide使用教程
Carson帶你學Android:圖片加載庫Glide源碼分析
Carson帶你學Android:V-Layout,淘寶、天貓都在用的UI框架,趕緊用起來吧!
目錄
1. 為什么要向 Tangram模型 加入 VirtualView
即 為什么要更新
Tangram2.0
版本
- 結論
- 提升組件動態性,實現動態更新
- 提升了組件的渲染性能
-
具體描述
示意圖
而上述解決方案的承載方案,則是 VirtualView
2. VirtualView介紹
-
簡介
示意圖 特點
3. 實現原理
3.1 核心思路
根據Tangram v1.0
中 出現的問題:UI
組件無法動態更新 & 加載性能低,VirtualView
的具體解決方案如下
3.2 實現方案
- 根據其原理,
VirtualView
的實現方案是:虛擬化開發 - 虛擬化開發的本質:
之所以稱為虛擬化,是因為
Canvas
繪制的視圖不存在一一對應的實體View
3.3 總結
從上可知,VirtualView
的創新在于:
- 通過
XML
模板實現組件的動態性 - 通過 虛擬化技術(本質 =
Canvas
)開發組件,提升了組件的渲染性能
4. 工作流程
- 在了解了
VirtualView
的本質原理 & 整體架構后 - 下面,我將開始講解
VirtualView
的工作流程
4.1 流程概述
- 根據上述方案,
VirtualView
的工作流程分為3大部分:創建UI組件、創建界面模板 & 客戶端加載界面 - 具體如下
4.2 流程詳細分析
下面我將對每個流程的原理 & 過程詳細分析
流程1:創建UI組件
- 具體描述
根據業務需求,創建所需要的UI
組件
有2種創建方式:使用框架內置(封裝好)的UI
組件 / 自定義
1.1 使用框架內置(封裝好)的UI
組件
- 即 可直接使用封裝好的
UI
組件而不需自身創建 - 具體如下(含組件基礎屬性)
注:
a. 自定義組件應繼承基礎組件
b. 系統封裝UI
組件的原理 同 “自定義UI
組件,下面將具體講解
1.2 自定義UI組件
若框架內置的UI
組件無法滿足需求,則開發者可自定義UI
組件
- 自定義流程
VirtualView
抽象 & 封裝了Canvas
繪制視圖的流程,使得開發者只需按指定的接口協議實現1個組件的繪制邏輯:測量、繪制 & 繪制,即能實現在宿主容器通過Canvas
直接繪制UI
內容,從而創建虛擬化組件
即 上述則是虛擬化創建組件的過程
- 具體過程
- 實現基礎組件需遵循一個接口的規范:定義了渲染過程中所需的3個流程:測量尺寸階段、布局階段 & 繪制階段
a. 定義這3個階段是為了符合
Android
系統的使用,即View
繪制的三大流程:measure
過程、layout
過程、draw
過程。若不了解,請看文章
(2)自定義View Measure過程 - 最易懂的自定義View原理系列
(3)自定義View Layout過程 - 最易懂的自定義View原理系列
(4)自定義View Draw過程- 最易懂的自定義View原理系列
b. 在iOS
平臺下也需按照本方案的規范去處理
- 這3個過程具體如下:(與
Android View
繪制的三大流程相似)
示意圖
不論是虛擬 / 原生組件,都采用上述模型 & 流程定義
a. 對于虛擬組件:在這些接口里實現相關邏輯 / 通過封裝原生組件實現
b. 對于原生組件:在這些接口的實現里 調用原生組件的對應邏輯
結論:可混合使用虛擬控件 & 實體控件
至此,對于宿主的布局容器來說,包裝在內部的組件不分虛擬化 /
原生,暴露在外的接口相同,只要將宿主容器像普通的 View
一樣添加到的視圖界面上,就可在后續的渲染過程中顯示出來。
- 特別注意
此處即可解釋 為何渲染性能高:因虛擬組件使用得越多,View
個數就越少,即層級越扁平
如下所示的組件:
a. 普通的原生開發:2層(宿主容器層 + 圖片組件層)
b. 虛擬化開發:采用虛擬化開發后,最終呈現的 View層級只有一個宿主容器(實際上,圖片組件被繪制在Canvas
里了)
1.3 總結
創建UI
組件有2種方式:
- 直接使用框架內置的
UI
組件 - 自定義組件:通過封裝好的
Canvas
流程,按照指定接口協議實現繪制邏輯 / 封裝原生組件
流程2:創建界面模板 & 下發
- 該步驟包括多個步驟:創建
XML
界面模板、編譯成二進制數據、下發等 - 具體如下
2.1 創建XML界面模板
- 具體描述
根據業務需求,使用XML編寫模板
注:需使用專門的工具
virtualview_tools
編寫,其
使用說明見文章virtualview_tools使用指南
- 此方式類似:
Android
平臺上通過XML
搭建界面的方式 - 區別在于:
- 脫離了平臺限制,即一套模板可同時在
Android
、iOS
上使用 - 運行時動態加載
XML
模板數據,動態更新界面結構
- 脫離了平臺限制,即一套模板可同時在
// 引用的組件通過流程1中獲取
// 動態數據通過表達式從 JSON 數據里獲取
<?xml version="1.0" encoding="utf-8"?>
<VHLayout
flag="flag_exposure|flag_clickable"
orientation="H"
layoutWidth="match_parent"
layoutHeight="wrap_content">
<NImage
id="1"
src="${logoUrl}"
layoutMarginLeft="8"
layoutMarginRight="8"
layoutMarginTop="8"
layoutMarginBottom="8"
layoutWidth="32"
layoutHeight="32"/>
<NText
id="2"
text="${title}"
layoutGravity="v_center"
gravity="${style.text-align}"
textSize="${style.font-size}"
textColor="${style.color}"
layoutWidth="match_parent"
layoutHeight="wrap_content"/>
</VHLayout>
// JSON數據
{
"style": {
"text-align": "h_center",
"font-size": "20",
"color": "#FF5000"
},
"title": "超高性 99.9% 的用戶覺得很快",
"logoUrl": "https://gw.alicdn.com/tfs/TB1yGIdkb_I8KJjy1XaXXbsxpXa-72-72.png"
}
2.2 編譯成二進制數據
2.2.1 具體描述
使用專門的工具virtualview_tools
將編寫好的XML
界面模板編譯成二進制數據,編譯后的文件的后綴名是.out
使用說明見文章virtualview_tools使用指南
注:為什么通過 XML 編寫的業務組件 不直接在客戶端里運行使用,而是先進行一次二進制序列化操作?
2.2.2 二進制文件描述
借鑒了 Android
系統編譯模板文件的思路,格式 & 描述具體如下
2.2.2 編譯流程
- 一個業務組件對應著一份
XML
模板 = 單獨編譯成二進制數據
編譯數據 含除內置字符串資源外 它依賴的所有字符串、表達式資源
- 編譯規則
編譯時,模板里涉及的資源包括顏色值、各種枚舉、基礎組件的類型等都會被序列化映射成整數;不能序列化成整數的資源如字符串,就分配一個索引Id
指向它 & 將它們單獨存儲到一塊區域里
- 原因:當模板在線發布、字符串有變動的情況下,能夠不影響原來的字符串資源索引;否則若按照帶有順序約定的協議來分配資源索引,很容易在模板變更時 同一索引值在變更前后指向的資源內容是不一樣,影響穩定性和動態性
- 序列化的規則如下:
- 編譯流程
2.3 模板數據 下發到客戶端
即 客戶端獲取編譯后的二進制數據
獲取有2種路徑:
- 直接將編譯后的模板打包到客戶端里,開發者通過代碼加載
- 框架先發布到模板管理后臺,客戶端在線更新到模板數據(即實現了動態更新)
流程3:客戶端加載界面
- 客戶端獲取到編譯后的界面模板后,進行加載 & 解析,最終渲染出視圖界面
- 步驟流程如下圖
3.1 解析模板數據
- 具體描述
客戶端獲得編譯后的模板數據(二進制數據)后,立即 進行解析
- 如校驗版本號,合法性,讀取頭信息等
- 客戶端渲染組件 從解析 編譯后的模板數據開始
- 流程解析
解析過程 = 二進制編譯的逆過程
但解析流程只負責提取原始數據 & 組織格式,并無構建出組件對象
3.2 加載組件視圖
具體描述
當用戶傳入一個模板名稱,框架內部就會根據名稱去之前解析XML界面模板的數據里找到 與此名稱匹配的模板數據,然后加載 & 創建出真正的組件流程解析
3.3 綁定業務數據
- 具體描述
開發者在組件屬性里可通過 表達式 指定使用哪個數據字段,即將業務數據綁定到組件上
因業務數據是動態的,故從模板創建的組件不含業務數據
- 流程解析
在創建組件的過程中,當解析屬性碰到表達式時,會將該屬性的key、表達式值、所屬的基礎組件等關系存儲起來,等真實數據到達后再通過 表達式里的定義 訪問數據 & 將真實值設置給組件的屬性,即將真實的數據綁定到基礎組件的屬性上
- 通過表達式解析、訪問得到的屬性值,會緩存起來,當原始數據引用不變時,每次訪問都會獲取到緩存值
- 此處接收的數據是
JSON
格式
4.3 總結
5. 整體架構設計
- 根據上述方案 & 工作流程,
VirtualView
的整體框架分為2部分:核心功能模塊(5個模塊) + 配套工具 & 服務。具體如下:
- 下面,我將對每部分進行詳細分析
模塊1:加載模塊
-
示意圖
示意圖
-
說明
示意圖
模塊2:構造模塊
-
示意圖
示意圖 -
說明
示意圖
此處詳細分析 基礎組件模型 & 虛擬組件
a. 基礎組件模型
含基礎組件 & 基礎屬性,具體如下
注:自定義的基礎組件應繼承基礎定義 & 擴展
模塊3:輔助模塊
-
示意圖
示意圖 -
說明
示意圖 特別注意:引入用戶數據綁定的表達式的原因
開發業務組件時,基礎屬性 / 樣式不能在模板里直接寫死,而是需從數據里動態獲取
/**
* 訪問數據屬性的表達式
* 語法說明
* a. 以 “${” 開頭、以 “}” 結束
* b. 對于Map,通過“.”操作符訪問
* c. 對于 Array / List,通過 “[]” 操作符訪問
* 示例如下
*/
${benefitImgUrl};
${data[0].benefitImgUrl};
/**
* 條件表達式
* 作用:根據數據中某個字段 來設置值的屬性
* 語法說明
* a. 以 “@{” 開頭、以 “}” 結束,
* b. 中間部分 = 表達式的具體內容: 條件表達式 ? 結果表達式[1] : 結果表達式[2]
* 注:1. 當條件表達式成立的時,使用結果表達式[1],否則使用結果表達式[2]
* 2. 條件表達式支持布爾類型、字符串類型、JSONObject、JSONArray
* c. 對于 Array / List,通過 “[]” 操作符訪問
* 示例如下
*/
@{${logoUrl} ? visible : invisible };
模塊4:管理模塊
-
示意圖
示意圖 -
說明
示意圖
模塊5:更新模塊
-
示意圖
示意圖 -
說明
示意圖
配套使用的工具 & 服務
-
示意圖
示意圖 -
說明
示意圖
總結
6. 使用教程
- 根據上述工作流程,其使用流程同樣分為3步:創建
UI
組件、創建界面模板 & 客戶端加載界面 - 下面,我將根據上述3個步驟進行詳細解析
6.1 創建UI組件
從一文可知,創建UI
組件有2種方式:
- 直接使用框架內置的
UI
組件 - 自定義組件:通過封裝好的
Canvas
流程,按照指定接口協議實現繪制邏輯 / 封裝原生組件
此處為方便講解,直接使用框架內置的UI
組件
6.2 創建界面模板
此步驟包括:創建XML界面模板、編譯成二進制數據、模板下發
6.2.1 創建XML界面模板
根據業務需求,使用XML編寫模板
注:需使用專門的工具
virtualview_tools
編寫,其
使用說明見文章virtualview_tools使用指南
- 示例布局
/**
* 使用說明:
* 1. 控件引用:通過XML引用控件為方便講解,XML內引用的VHLayout、NImage、NText 都是框架內置的控件:2個橫向線性布局;每個布局 = 1個圖 + 1個文本
* 2. 屬性設置:可寫死 / 通過表達式綁定一個數據字段(JSON)引用
* 布局說明:
* 1. 引用的控件VHLayout、NImage、NText等都是框架內置的控件
* 2. 整個布局 = 2個橫向線性布局,每個布局 = 1個圖 + 1個文本
*/
<?xml version="1.0" encoding="utf-8"?>
<VHLayout
flag="flag_exposure|flag_clickable"
orientation="V"
layoutWidth="match_parent"
layoutHeight="wrap_content">
<VHLayout
flag="flag_exposure|flag_clickable"
orientation="H"
layoutWidth="match_parent"
layoutHeight="wrap_content">
<NImage
id="1"
src="${logoUrl}"
layoutMarginLeft="8"
layoutMarginRight="8"
layoutMarginTop="8"
layoutMarginBottom="8"
layoutWidth="32"
layoutHeight="32"/>
<NText
id="2"
text="${title}"
layoutGravity="v_center"
gravity="${style.text-align}"
textSize="${style.font-size}"
textColor="${style.color}"
layoutWidth="match_parent"
layoutHeight="wrap_content"/>
</VHLayout>
<VHLayout
flag="flag_exposure|flag_clickable"
orientation="H"
layoutWidth="match_parent"
layoutHeight="wrap_content">
<VImage
id="1"
src="${logoUrl}"
layoutMarginLeft="8"
layoutMarginRight="8"
layoutMarginTop="8"
layoutMarginBottom="8"
layoutWidth="32"
layoutHeight="32"/>
<VText
id="2"
text="${title}"
layoutGravity="v_center"
gravity="${style.text-align}"
textSize="${style.font-size}"
textColor="${style.color}"
layoutWidth="match_parent"
layoutHeight="wrap_content"/>
</VHLayout>
</VHLayout>
- 屬性數據來源:
JSON
{
"style": {
"text-align": "h_center",
"font-size": "20",
"color": "#FF5000"
},
"title": "超高性 99.9% 的用戶覺得很快",
"logoUrl": "https://gw.alicdn.com/tfs/TB1yGIdkb_I8KJjy1XaXXbsxpXa-72-72.png"
}
6.2.2 編譯成二進制數據
使用專門的工具virtualview_tools
將編寫好的XML
界面模板編譯成二進制數據,編譯后的文件的后綴名是.out
使用說明見文章virtualview_tools使用指南
6.2.3 模板下發到客戶端
有2種路徑:
- 直接將編譯后的模板打包到客戶端里,開發者通過代碼加載
- 框架先發布到模板管理后臺,客戶端在線更新到模板數據(即實現了動態更新)
此處選擇方式1
6.3 客戶端解析 & 加載界面模板
具體使用如下
// 1. 初始化圖片加載器
VafContext.loadImageLoader(mContext.getApplicationContext());
// 2. 初始化 ViewManager 對象
ViewManager viewManager = vafContext.getViewManager();
viewManager.init(mContext.getApplicationContext());
// 3. 加載編譯后的模板數據(二進制文件)
// 方式1:直接加載二進制字節數組(推薦使用)
viewManager.loadBinBufferSync(TMALLCOMPONENT1.BIN);
viewManager.loadBinBufferSync(TMALLCOMPONENT2.BIN);
// 方式2:通過二進制文件路徑加載
viewManager.loadBinFileSync(TMALLCOMPONENT1_PATH);
viewManager.loadBinFileSync(TMALLCOMPONENT2_PATH);
// 4. 注冊事件處理器,如常用的點擊、曝光處理
vafContext.getEventManager().register(EventManager.TYPE_Click, new IEventProcessor() {
@Override
public boolean process(EventData data) {
//handle here
return true;
}
});
vafContext.getEventManager().register(EventManager.TYPE_Exposure, new IEventProcessor() {
@Override
public boolean process(EventData data) {
//handle here
return true;
}
});
// 5. 通過組件名參數 name 生成組件實例
View container = vafContext.getContainerService().getContainer(name, true);
mLinearLayout.addView(container);
// 6. 為組件綁定真實的數據
// 假若您在組件模板里寫了數據綁定的表達式
IContainer iContainer = (IContainer)container;
JSONObject json = getJSONDataFromAsset(data);
if (json != null) {
iContainer.getVirtualView().setVData(json);
}
- 測試結果
下圖展示的“超高性 99.9% 的用戶覺得很快”即為
VirtualView
的展示效果
至此,關于VirtualView
的使用講解完畢
更加詳細使用,請參考文章VirtualView的使用文檔
7.VirtualView 的意義
對于個人的看法,VirtualView
的補充其重大意義在于2個方面:對于 阿里Tangram
模型 & 整個原生開發技術(Android、iOS)
7.1 對于 Tangram 模型
VirtualView
的解決的問題 在于:
- 實現組件的動態性:可在端上綁定動態下發的
XML
界面模板 & 數據 - 提升了組件的渲染性能:通過 虛擬化技術(本質 =
Canvas
)開發組件
7.2 對于整個原生開發技術(Android、iOS)
VirtualView
的創新在于:解決了 原生開發中一直被詬病 而 常被叫喧會被 前端、RN
技術取代的問題:
開發周期長 & 成本大
VirtualView
采用XML
描述視圖,XML
界面模板具備跨平臺使用的特性無法熱更新
VirtualView
可在端上綁定動態下發的XML
界面模板 & 數據,從而實現熱更新
相比于前幾年產品開發的一味求快,如今互聯網行業發展暫緩、用戶需求基本滿足的情況下,更加 講求的是用戶體驗
所以,實際上對比于 前端、
RN
技術在客戶端的實現,VirtualView
的優勢或許會更明顯:在解決了原生開發效率慢、周期長的前提下,保證了原生開發的優勢:性能好
7.3 呼吁
- 雖然
VirtualView
推動了原生開發的發展,但目前來說,VirtualView
還是存在不少問題 - 希望大家能一起在Github - alibaba - VirtualView 上進行完善,共同為開源事業做貢獻吧!
8. 總結
- 看完本文,你應該非常了解阿里出品的
VirtualView
的使用 & 原理 - 關于
Tangram
的使用,建議看文章:
- 下面我將繼續對
Android
其他優秀的開源庫 進行詳細分析,感興趣的同學可以繼續關注Carson帶你學Android開源庫系列文章:
Carson帶你學Android:主流開源圖片加載庫對比(UIL、Picasso、Glide、Fresco)
Carson帶你學Android:主流開源網絡請求庫對比(Volley、OkHttp、Retrofit)
Carson帶你學Android:網絡請求庫Retrofit使用教程
Carson帶你學Android:網絡請求庫Retrofit源碼分析
Carson帶你學Android:圖片加載庫Glide使用教程
Carson帶你學Android:圖片加載庫Glide源碼分析
Carson帶你學Android:V-Layout,淘寶、天貓都在用的UI框架,趕緊用起來吧!
歡迎關注Carson_Ho的簡書
不定期分享關于安卓開發的干貨,追求短、平、快,但卻不缺深度。
請點贊!因為你的鼓勵是我寫作的最大動力!
參考文章:
https://juejin.im/post/5a2a7160f265da432c23c4ca
http://tangram.pingguohe.net/docs/virtualview/about-virtualview