微信小程式
專案結構
上圖為微信小程式的專案結構,pages下面包含了小程式中的每一個頁面,每一個頁面由頁面結構,頁面樣式,頁面配置和邏輯程式碼四部分組成。
- 頁面結構
頁面結構檔案為index.wxml,通過微信自定義的標籤來寫。
- 頁面邏輯
頁面邏輯通過JavaScript來書寫。
- 頁面樣式表
類似CSS檔案,來定義頁面內元素的樣式。
- 頁面配置
頁面內的許可權等配置資訊。
微信小程式的技術選型
小程式的定位特點是輕,快,針對這兩個特點,在技術選型上,微信進行了一些考量。
渲染介面的技術
- 用純客戶端原生技術來渲染
缺點:無法動態打包,動態下發。
- 用純 Web 技術來渲染
缺點:如果我們用純 Web 技術來渲染小程式,在一些有複雜互動的頁面上可能會面臨一些效能問題,這是因為在 Web 技術中,UI渲染跟 JavaScript 的指令碼執行都在一個單執行緒中執行,這就容易導致一些邏輯任務搶佔UI渲染的資源。
- 介於客戶端原生技術與 Web 技術之間的,互相結合各自特點的技術來渲染
從渲染底層來看,PhoneGap與微信 JS-SDK 是類似的,它們最終都還是使用瀏覽器核心來渲染介面。而 RN 則不同,雖然是用 Web 相關技術來編寫,同樣是利用了 JavaScript 解釋執行的特性,但 RN 在渲染底層是用客戶端原生渲染的。我們選擇類似於微信 JSSDK 這樣的 Hybrid 技術,即介面主要由成熟的 Web 技術渲染,輔之以大量的介面提供豐富的客戶端原生能力。同時,每個小程式頁面都是用不同的WebView去渲染,這樣可以提供更好的互動體驗,更貼近原生體驗,也避免了單個WebView的任務過於繁重。
微信沒有選擇RN的原因
-
RN 所支援的樣式是 CSS 的子集,會滿足不了 Web 開發者日漸增長的需求,而對 RN 的改造具有不小的成本和風險。
-
RN 現有能力下還存在的一些不穩定問題,比如效能、Bug等。RN 是把渲染工作全都交由客戶端原生渲染,實際上一些簡單的介面元素使用 Web 技術渲染完全能勝任,並且非常穩定。
-
RN 存在一些不可預期的因素,比如之前出現的許可協議問題
原生元件的渲染方式
在安卓則是往 WebView 的 window 物件注入一個原生方法,最終會封裝成 WeiXinJSBridge 這樣一個相容層,主要提供了呼叫(invoke)和監聽(on)這兩種方法。開發者插入一個原生元件,一般而言,元件執行的時候被插入到 DOM 樹中,會呼叫客戶端介面,通知客戶端在哪個位置渲染一塊原生介面。在後續開發者更新元件屬性時,同樣地,也會呼叫客戶端提供的更新介面來更新原生介面的某些部分。
Web渲染帶來的問題與解決
- 提供乾淨純粹的JavaScript執行環境
由於JavaScript的靈活性和瀏覽器的功能豐富,會導致很多不可控的隱私,因此,微信提供了一個單純的JS執行環境,通過對於其中的控制元件也進行了自定義。因此完全採用這個沙箱環境不能有任何瀏覽器相關介面,只提供純JavaScript 的解釋執行環境,那麼像HTML5中的ServiceWorker、WebWorker特性就符合這樣的條件,這兩者都是啟用另一執行緒來執行 JavaScript。但是考慮到小程式是一個多 WebView 的架構,每一個小程式頁面都是不同的WebView 渲染後顯示的,在這個架構下我們不好去用某個WebView中的ServiceWorker去管理所有的小程式頁面。得益於客戶端系統有JavaScript 的解釋引擎(在iOS下是用內建的 JavaScriptCore框架,在安卓則是用騰訊x5核心提供的JsCore環境),我們可以建立一個單獨的執行緒去執行 JavaScript,在這個環境下執行的都是有關小程式業務邏輯的程式碼,也就是我們前面一直提到的邏輯層。而介面渲染相關的任務全都在WebView執行緒裡執行,通過邏輯層程式碼去控制渲染哪些介面,那麼這一層當然就是所謂的渲染層。這就是小程式雙執行緒模型的由來。
- 標籤自定義
為了防止標籤定義帶來的一些問題,微信自定義了一套標籤語言,WXML,這套標籤語言經過編譯之後,最終會生成Html。
渲染與邏輯的分離
上面是小程式的渲染技術的選型,在選型之後,由於渲染和邏輯不再同一個瀏覽器執行,一個在純JS環境中,一個通過WebView渲染,因此小程式的執行環境分成渲染層和邏輯層,WXML 模板和 WXSS 樣式工作在渲染層,JS 指令碼工作在邏輯層。
小程式的渲染層和邏輯層分別由2個執行緒管理:渲染層的介面使用了WebView 進行渲染;邏輯層採用JsCore執行緒執行JS指令碼。一個小程式存在多個介面,所以渲染層存在多個WebView執行緒,這兩個執行緒的通訊會經由微信客戶端做中轉,邏輯層傳送網路請求也經由Native轉發,小程式的通訊模型如圖所示。
資料驅動檢視變化
在開發UI介面過程中,程式需要維護很多變數狀態,同時要操作對應的UI元素。隨著介面越來越複雜,我們需要維護很多變數狀態,同時要處理很多介面上的互動事件,整個程式變得越來越複雜。通常介面檢視和變數狀態是相關聯的,如果有某種“方法”可以讓狀態和檢視繫結在一起(狀態變更時,檢視也能自動變更),那我們就可以省去手動修改檢視的工作。
小程式的邏輯層和渲染層是分開的兩個執行緒。在渲染層,宿主環境會把WXML轉化成對應的JS物件,在邏輯層發生資料變更的時候,我們需要通過宿主環境提供的setData方法把資料從邏輯層傳遞到渲染層,再經過對比前後差異,把差異應用在原來的Dom樹上,渲染出正確的UI介面。
通過setData把msg資料從“Hello World”變成“Goodbye”,產生的JS物件對應的節點就會發生變化,此時可以對比前後兩個JS物件得到變化的部分,然後把這個差異應用到原來的Dom樹上,從而達到更新UI的目的,這就是“資料驅動”的原理。
事件的處理
UI介面的程式需要和使用者互動,例如使用者可能會點選你介面上某個按鈕,又或者長按某個區域,這類反饋應該通知給開發者的邏輯層,需要將對應的處理狀態呈現給使用者。由於WebView現在具備的功能只是進行渲染,因此對於事件的分發處理,微信進行了特殊的處理,將所有的事件攔截後,丟到邏輯層交給JavaScript進行處理。
事件的派發處理,具備事件捕獲和冒泡兩種機制。通過native傳遞給JSCore,通過JS來響應響應的事件之後,對Dom進行修改,改動會體現在虛擬Dom上,然後再進行真實的渲染。
資料通訊
小程式是基於雙執行緒模型,那就意味著任何資料傳遞都是執行緒間的通訊,也就是都會有一定的延時。這不像傳統Web那樣,當介面需要更新時,通過呼叫更新介面UI就會同步地渲染出來。在小程式架構裡,這一切都會變成非同步。
非同步會使得各部分的執行時序變得複雜一些。比如在渲染首屏的時候,邏輯層與渲染層會同時開始初始化工作,但是渲染層需要有邏輯層的資料才能把介面渲染出來,如果渲染層初始化工作較快完成,就要等邏輯層的指令才能進行下一步工作。因此邏輯層與渲染層需要有一定的機制保證時序正確,
在每個小程式頁面的生命週期中,存在著若干次頁面資料通訊。邏輯層向檢視層傳送頁面資料(data和setData的內容),檢視層向邏輯層反饋使用者事件。
通過Json的方式進行資料的傳遞,提高效能的方式就是減少互動的資料量。
快取機制
小程式宿主環境會管理不同小程式的資料快取,不同小程式的本地快取空間是分開的,每個小程式的快取空間上限為10MB,如果當前快取已經達到10MB,再通過wx.setStorage寫入快取會觸發fail回撥。
小程式的本地快取不僅僅通過小程式這個維度來隔離空間,考慮到同一個裝置可以登入不同微信使用者,宿主環境還對不同使用者的快取進行了隔離,避免使用者間的資料隱私洩露。
由於本地快取是存放在當前裝置,使用者換裝置之後無法從另一個裝置讀取到當前裝置資料,因此使用者的關鍵資訊不建議只存在本地快取,應該把資料放到伺服器端進行持久化儲存。
支付寶小程式
支付寶小程式簡介
支付寶小程式的實現和微信小程式的實現方式大致是相同的,因此這裡主要針對兩者的差異性的地方。
支付寶小程式目錄結構
支付寶小程式業務架構圖
在渲染引擎上面,支付寶小程式不僅提供 JavaScript+Webview 的方式,還提供 JavaScript+Native 的方式,在對效能要求較高的場景,可以選擇 Native 的渲染模式,給使用者更好的體驗。
執行時架構
小程式程式設計模型是分為多個頁面,每個頁面有自己的 template、CSS 和 JS,實際在執行的時候,業務邏輯的 JS 程式碼是執行在獨立的 JavaScript 引擎中,每個頁面的 template 和 CSS 是執行在各自獨立的 webview 裡面,頁面之間是通過函式 navigateTo 進行頁面的切換。
每個 webview 裡面的頁面和公共的 JavaScript 引擎裡面的邏輯的互動方式是通過訊息服務,頁面的一些事件都會通過這個訊息通道傳給 JavaScript 引擎執行環境,這個執行環境會響應這個事件,做一些 API 呼叫,可調到客戶端支付寶小程式提供的一些能力,處理之後會把這個資料再重新傳送給對應的頁面渲染容器來處理,把資料和模板結合在一起來,在產生最終的使用者介面。
支付寶小程式虛擬機器隔離
通常的做法是在 WebView 裡面執行 render 的程式碼,然後另起一個執行緒執行 serviceworker,當 serviceworker 需要更新 dom 的時候把事件和資料通過 messagechannel 傳送給 render 執行緒來執行,當業務需要傳遞到 render 層資料量較大,物件較複雜時,互動的效能就會比較差,因此針對這種情況我們提出一個優化的解決方案。
該方案將原始的 JS 虛擬機器例項 (即 Isolate) 重新設計成了兩個部分:Global Runtime 和 Local Runtime。
-
Global Runtime 部分是存放共享的裝置和資料,全域性一個例項。
-
Local Runtime 是存放例項自身相關的模組和私有資料,這些不會被共享。
在新的隔離模型下,webview 裡面的 v8 例項就是一個 Local Runtime,worker 執行緒裡面的 v8 例項也是一個 Local Runtime,在 worker 層和 render 層互動時,setData 物件的會直接建立在 Shared Heap 裡面,因此 render 層的 Local Runtime 可以直接讀到該物件,並且用於 render 層的渲染,減少了物件的序列化和網路傳輸,極大的提升了啟動效能和渲染效能。
首屏速度優化
由於小程式啟動是受到生命週期的控制,從 onLaunch -> onLoad -> onShow -> onReady -> 使用者操作 -> 離開首頁這個流程,在這個過程中的任意一個環節都有可能被客觀或者主觀的原因打斷,也就有可能導致儲存的離線頁面不準確,在啟動的時候給使用者呈現錯誤的頁面。
所以對於首頁離線快取渲染的效果,儲存頁面的時機很重要,我們提供讓開發者可以配置的時機,配置的時機有兩個:渲染完成和離開首頁前。對於渲染完成就是首頁渲染完成,使用者還未執行任何的操作前把頁面儲存下來作為離線快取的頁面。離開首頁前就是指使用者在首頁執行了一系列的操作後,跳轉到其他頁面前使用者看到的頁面儲存下來作為離線快取的頁面。
對於閃屏問題發生的場景是因為快取頁面和真實渲染的頁面是分離的,是兩個獨立的頁面,快取頁面是靜態的頁面,真實的頁面是通過 js 動態建立的頁面,所以常規的做法就是當真實頁面建立完成後替換快取的頁面,這樣的情況下就會發生閃屏。
針對這個問題,我們是採用虛擬 dom 來解決,在載入快取頁面的時候把快取頁面放入初始的虛擬 dom 裡面,真實頁面建立後產生的虛擬 dom 跟快取頁面的虛擬 dom 進行 dom diff,把變化的內容通過 patch 傳給瀏覽器核心,渲染對應的頁面,這樣就可以只更新區域性有變化的頁面內容,避免了整個頁面的更新,也保證內容的準確性和實時性。
支付寶採用UC瀏覽器核心優勢
1.圖片記憶體:針對低端機,做了更嚴格的圖片快取限制,在保持效能體驗的情況下,進一步限制圖片快取的使用;多個 webview 共用圖片快取池;全面支援 webp、apng 這種更節省記憶體和 size 的圖片格式。
2.渲染記憶體:Webview 在不可見的狀態下,原生的記憶體管理沒有特殊處理,UC 核心會將不可見 webview 的渲染記憶體釋放;渲染記憶體的合理設定與調優,避免滾動效能的下降和佔用過多記憶體。
3.JS 記憶體:更合理地處理 v8 記憶體 gc,在啟動時延時執行 full gc,避免影響啟動的耗時。
4.峰值記憶體管理:系統在記憶體緊張時,會通知核心,UC 核心能夠在系統低記憶體時釋放非關鍵記憶體佔用的模組,避免出現 oom,也避免過度釋放帶來的渲染黑塊;在部分 oom 的情況,規避原生核心主動崩潰的邏輯,在記憶體極低的情況,部分功能不可用,而不是崩潰。