微信小程序
項目結構
上圖為微信小程序的項目結構,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 的情況,規避原生內核主動崩潰的邏輯,在內存極低的情況,部分功能不可用,而不是崩潰。
對我們的啟示
- 小程序存儲管理
增加小程序的存儲,包括內存和磁盤,可以緩存部分數據,增加頁面直出速度。同時對于磁盤的管理,按照小程序賬號雙重維度進行劃分。
- 第三方業務接入能力限制
在支持第三方的接入之后,按照現有方式將會導致對于安全和第三方的行為完全不可控,可以參考微信,支付寶方式采用自定義標記語言的方式對標記語言做限制,并提供純凈的JS環境來進行JS環境的執行,WebView只負責渲染。
- 首屏速度
參考支付寶方案,在加載的時候,現將老的頁面呈現給用戶,然后在新頁面完成之后,計算差值,再進行顯示。
- Native繪制結合
Native繪制采用通過JS和Native通信的方式,將Native控價加入到布局的制定區域。
- 網絡請求發送托管
網絡請求等全部交由Native托管,更好的控制網絡請求,監控網絡請求。