微信小程序
項目結構

上圖為微信小程序的項目結構,pages下面包含了小程序中的每一個頁面,每一個頁面由頁面結構,頁面樣式,頁面配置和邏輯代碼四部分組成。
- 頁面結構
頁面結構文件為index.wxml,通過微信自定義的標簽來寫。
- 頁面邏輯
頁面邏輯通過JavaScript來書寫。
- 頁面樣式表
類似CSS文件,來定義頁面內(nèi)元素的樣式。
- 頁面配置
頁面內(nèi)的權限等配置信息。
微信小程序的技術選型
小程序的定位特點是輕,快,針對這兩個特點,在技術選型上,微信進行了一些考量。
渲染界面的技術
- 用純客戶端原生技術來渲染
缺點:無法動態(tài)打包,動態(tài)下發(fā)。
- 用純 Web 技術來渲染
缺點:如果我們用純 Web 技術來渲染小程序,在一些有復雜交互的頁面上可能會面臨一些性能問題,這是因為在 Web 技術中,UI渲染跟 JavaScript 的腳本執(zhí)行都在一個單線程中執(zhí)行,這就容易導致一些邏輯任務搶占UI渲染的資源。
- 介于客戶端原生技術與 Web 技術之間的,互相結合各自特點的技術來渲染
從渲染底層來看,PhoneGap與微信 JS-SDK 是類似的,它們最終都還是使用瀏覽器內(nèi)核來渲染界面。而 RN 則不同,雖然是用 Web 相關技術來編寫,同樣是利用了 JavaScript 解釋執(zhí)行的特性,但 RN 在渲染底層是用客戶端原生渲染的。我們選擇類似于微信 JSSDK 這樣的 Hybrid 技術,即界面主要由成熟的 Web 技術渲染,輔之以大量的接口提供豐富的客戶端原生能力。同時,每個小程序頁面都是用不同的WebView去渲染,這樣可以提供更好的交互體驗,更貼近原生體驗,也避免了單個WebView的任務過于繁重。
微信沒有選擇RN的原因
RN 所支持的樣式是 CSS 的子集,會滿足不了 Web 開發(fā)者日漸增長的需求,而對 RN 的改造具有不小的成本和風險。
RN 現(xiàn)有能力下還存在的一些不穩(wěn)定問題,比如性能、Bug等。RN 是把渲染工作全都交由客戶端原生渲染,實際上一些簡單的界面元素使用 Web 技術渲染完全能勝任,并且非常穩(wěn)定。
RN 存在一些不可預期的因素,比如之前出現(xiàn)的許可協(xié)議問題
原生組件的渲染方式
在安卓則是往 WebView 的 window 對象注入一個原生方法,最終會封裝成 WeiXinJSBridge 這樣一個兼容層,主要提供了調用(invoke)和監(jiān)聽(on)這兩種方法。開發(fā)者插入一個原生組件,一般而言,組件運行的時候被插入到 DOM 樹中,會調用客戶端接口,通知客戶端在哪個位置渲染一塊原生界面。在后續(xù)開發(fā)者更新組件屬性時,同樣地,也會調用客戶端提供的更新接口來更新原生界面的某些部分。
Web渲染帶來的問題與解決
- 提供干凈純粹的JavaScript執(zhí)行環(huán)境
由于JavaScript的靈活性和瀏覽器的功能豐富,會導致很多不可控的隱私,因此,微信提供了一個單純的JS執(zhí)行環(huán)境,通過對于其中的控件也進行了自定義。因此完全采用這個沙箱環(huán)境不能有任何瀏覽器相關接口,只提供純JavaScript 的解釋執(zhí)行環(huán)境,那么像HTML5中的ServiceWorker、WebWorker特性就符合這樣的條件,這兩者都是啟用另一線程來執(zhí)行 JavaScript。但是考慮到小程序是一個多 WebView 的架構,每一個小程序頁面都是不同的WebView 渲染后顯示的,在這個架構下我們不好去用某個WebView中的ServiceWorker去管理所有的小程序頁面。得益于客戶端系統(tǒng)有JavaScript 的解釋引擎(在iOS下是用內(nèi)置的 JavaScriptCore框架,在安卓則是用騰訊x5內(nèi)核提供的JsCore環(huán)境),我們可以創(chuàng)建一個單獨的線程去執(zhí)行 JavaScript,在這個環(huán)境下執(zhí)行的都是有關小程序業(yè)務邏輯的代碼,也就是我們前面一直提到的邏輯層。而界面渲染相關的任務全都在WebView線程里執(zhí)行,通過邏輯層代碼去控制渲染哪些界面,那么這一層當然就是所謂的渲染層。這就是小程序雙線程模型的由來。
- 標簽自定義
為了防止標簽定義帶來的一些問題,微信自定義了一套標簽語言,WXML,這套標簽語言經(jīng)過編譯之后,最終會生成Html。
渲染與邏輯的分離
上面是小程序的渲染技術的選型,在選型之后,由于渲染和邏輯不再同一個瀏覽器執(zhí)行,一個在純JS環(huán)境中,一個通過WebView渲染,因此小程序的運行環(huán)境分成渲染層和邏輯層,WXML 模板和 WXSS 樣式工作在渲染層,JS 腳本工作在邏輯層。
小程序的渲染層和邏輯層分別由2個線程管理:渲染層的界面使用了WebView 進行渲染;邏輯層采用JsCore線程運行JS腳本。一個小程序存在多個界面,所以渲染層存在多個WebView線程,這兩個線程的通信會經(jīng)由微信客戶端做中轉,邏輯層發(fā)送網(wǎng)絡請求也經(jīng)由Native轉發(fā),小程序的通信模型如圖所示。

數(shù)據(jù)驅動視圖變化
在開發(fā)UI界面過程中,程序需要維護很多變量狀態(tài),同時要操作對應的UI元素。隨著界面越來越復雜,我們需要維護很多變量狀態(tài),同時要處理很多界面上的交互事件,整個程序變得越來越復雜。通常界面視圖和變量狀態(tài)是相關聯(lián)的,如果有某種“方法”可以讓狀態(tài)和視圖綁定在一起(狀態(tài)變更時,視圖也能自動變更),那我們就可以省去手動修改視圖的工作。
小程序的邏輯層和渲染層是分開的兩個線程。在渲染層,宿主環(huán)境會把WXML轉化成對應的JS對象,在邏輯層發(fā)生數(shù)據(jù)變更的時候,我們需要通過宿主環(huán)境提供的setData方法把數(shù)據(jù)從邏輯層傳遞到渲染層,再經(jīng)過對比前后差異,把差異應用在原來的Dom樹上,渲染出正確的UI界面。

通過setData把msg數(shù)據(jù)從“Hello World”變成“Goodbye”,產(chǎn)生的JS對象對應的節(jié)點就會發(fā)生變化,此時可以對比前后兩個JS對象得到變化的部分,然后把這個差異應用到原來的Dom樹上,從而達到更新UI的目的,這就是“數(shù)據(jù)驅動”的原理。


事件的處理
UI界面的程序需要和用戶互動,例如用戶可能會點擊你界面上某個按鈕,又或者長按某個區(qū)域,這類反饋應該通知給開發(fā)者的邏輯層,需要將對應的處理狀態(tài)呈現(xiàn)給用戶。由于WebView現(xiàn)在具備的功能只是進行渲染,因此對于事件的分發(fā)處理,微信進行了特殊的處理,將所有的事件攔截后,丟到邏輯層交給JavaScript進行處理。

事件的派發(fā)處理,具備事件捕獲和冒泡兩種機制。通過native傳遞給JSCore,通過JS來響應響應的事件之后,對Dom進行修改,改動會體現(xiàn)在虛擬Dom上,然后再進行真實的渲染。

數(shù)據(jù)通信
小程序是基于雙線程模型,那就意味著任何數(shù)據(jù)傳遞都是線程間的通信,也就是都會有一定的延時。這不像傳統(tǒng)Web那樣,當界面需要更新時,通過調用更新接口UI就會同步地渲染出來。在小程序架構里,這一切都會變成異步。
異步會使得各部分的運行時序變得復雜一些。比如在渲染首屏的時候,邏輯層與渲染層會同時開始初始化工作,但是渲染層需要有邏輯層的數(shù)據(jù)才能把界面渲染出來,如果渲染層初始化工作較快完成,就要等邏輯層的指令才能進行下一步工作。因此邏輯層與渲染層需要有一定的機制保證時序正確,
在每個小程序頁面的生命周期中,存在著若干次頁面數(shù)據(jù)通信。邏輯層向視圖層發(fā)送頁面數(shù)據(jù)(data和setData的內(nèi)容),視圖層向邏輯層反饋用戶事件。

通過Json的方式進行數(shù)據(jù)的傳遞,提高性能的方式就是減少交互的數(shù)據(jù)量。
緩存機制
小程序宿主環(huán)境會管理不同小程序的數(shù)據(jù)緩存,不同小程序的本地緩存空間是分開的,每個小程序的緩存空間上限為10MB,如果當前緩存已經(jīng)達到10MB,再通過wx.setStorage寫入緩存會觸發(fā)fail回調。
小程序的本地緩存不僅僅通過小程序這個維度來隔離空間,考慮到同一個設備可以登錄不同微信用戶,宿主環(huán)境還對不同用戶的緩存進行了隔離,避免用戶間的數(shù)據(jù)隱私泄露。
由于本地緩存是存放在當前設備,用戶換設備之后無法從另一個設備讀取到當前設備數(shù)據(jù),因此用戶的關鍵信息不建議只存在本地緩存,應該把數(shù)據(jù)放到服務器端進行持久化存儲。
支付寶小程序
支付寶小程序簡介
支付寶小程序的實現(xiàn)和微信小程序的實現(xiàn)方式大致是相同的,因此這里主要針對兩者的差異性的地方。
支付寶小程序目錄結構

支付寶小程序業(yè)務架構圖

在渲染引擎上面,支付寶小程序不僅提供 JavaScript+Webview 的方式,還提供 JavaScript+Native 的方式,在對性能要求較高的場景,可以選擇 Native 的渲染模式,給用戶更好的體驗。
運行時架構
小程序編程模型是分為多個頁面,每個頁面有自己的 template、CSS 和 JS,實際在運行的時候,業(yè)務邏輯的 JS 代碼是運行在獨立的 JavaScript 引擎中,每個頁面的 template 和 CSS 是運行在各自獨立的 webview 里面,頁面之間是通過函數(shù) navigateTo 進行頁面的切換。
每個 webview 里面的頁面和公共的 JavaScript 引擎里面的邏輯的交互方式是通過消息服務,頁面的一些事件都會通過這個消息通道傳給 JavaScript 引擎運行環(huán)境,這個運行環(huán)境會響應這個事件,做一些 API 調用,可調到客戶端支付寶小程序提供的一些能力,處理之后會把這個數(shù)據(jù)再重新發(fā)送給對應的頁面渲染容器來處理,把數(shù)據(jù)和模板結合在一起來,在產(chǎn)生最終的用戶界面。

支付寶小程序虛擬機隔離
通常的做法是在 WebView 里面運行 render 的代碼,然后另起一個線程運行 serviceworker,當 serviceworker 需要更新 dom 的時候把事件和數(shù)據(jù)通過 messagechannel 發(fā)送給 render 線程來執(zhí)行,當業(yè)務需要傳遞到 render 層數(shù)據(jù)量較大,對象較復雜時,交互的性能就會比較差,因此針對這種情況我們提出一個優(yōu)化的解決方案。
該方案將原始的 JS 虛擬機實例 (即 Isolate) 重新設計成了兩個部分:Global Runtime 和 Local Runtime。
Global Runtime 部分是存放共享的裝置和數(shù)據(jù),全局一個實例。
Local Runtime 是存放實例自身相關的模塊和私有數(shù)據(jù),這些不會被共享。
在新的隔離模型下,webview 里面的 v8 實例就是一個 Local Runtime,worker 線程里面的 v8 實例也是一個 Local Runtime,在 worker 層和 render 層交互時,setData 對象的會直接創(chuàng)建在 Shared Heap 里面,因此 render 層的 Local Runtime 可以直接讀到該對象,并且用于 render 層的渲染,減少了對象的序列化和網(wǎng)絡傳輸,極大的提升了啟動性能和渲染性能。

首屏速度優(yōu)化

由于小程序啟動是受到生命周期的控制,從 onLaunch -> onLoad -> onShow -> onReady -> 用戶操作 -> 離開首頁這個流程,在這個過程中的任意一個環(huán)節(jié)都有可能被客觀或者主觀的原因打斷,也就有可能導致保存的離線頁面不準確,在啟動的時候給用戶呈現(xiàn)錯誤的頁面。
所以對于首頁離線緩存渲染的效果,保存頁面的時機很重要,我們提供讓開發(fā)者可以配置的時機,配置的時機有兩個:渲染完成和離開首頁前。對于渲染完成就是首頁渲染完成,用戶還未執(zhí)行任何的操作前把頁面保存下來作為離線緩存的頁面。離開首頁前就是指用戶在首頁執(zhí)行了一系列的操作后,跳轉到其他頁面前用戶看到的頁面保存下來作為離線緩存的頁面。

對于閃屏問題發(fā)生的場景是因為緩存頁面和真實渲染的頁面是分離的,是兩個獨立的頁面,緩存頁面是靜態(tài)的頁面,真實的頁面是通過 js 動態(tài)創(chuàng)建的頁面,所以常規(guī)的做法就是當真實頁面創(chuàng)建完成后替換緩存的頁面,這樣的情況下就會發(fā)生閃屏。

針對這個問題,我們是采用虛擬 dom 來解決,在加載緩存頁面的時候把緩存頁面放入初始的虛擬 dom 里面,真實頁面創(chuàng)建后產(chǎn)生的虛擬 dom 跟緩存頁面的虛擬 dom 進行 dom diff,把變化的內(nèi)容通過 patch 傳給瀏覽器內(nèi)核,渲染對應的頁面,這樣就可以只更新局部有變化的頁面內(nèi)容,避免了整個頁面的更新,也保證內(nèi)容的準確性和實時性。

支付寶采用UC瀏覽器內(nèi)核優(yōu)勢
1.圖片內(nèi)存:針對低端機,做了更嚴格的圖片緩存限制,在保持性能體驗的情況下,進一步限制圖片緩存的使用;多個 webview 共用圖片緩存池;全面支持 webp、apng 這種更節(jié)省內(nèi)存和 size 的圖片格式。
2.渲染內(nèi)存:Webview 在不可見的狀態(tài)下,原生的內(nèi)存管理沒有特殊處理,UC 內(nèi)核會將不可見 webview 的渲染內(nèi)存釋放;渲染內(nèi)存的合理設置與調優(yōu),避免滾動性能的下降和占用過多內(nèi)存。
3.JS 內(nèi)存:更合理地處理 v8 內(nèi)存 gc,在啟動時延時執(zhí)行 full gc,避免影響啟動的耗時。
4.峰值內(nèi)存管理:系統(tǒng)在內(nèi)存緊張時,會通知內(nèi)核,UC 內(nèi)核能夠在系統(tǒng)低內(nèi)存時釋放非關鍵內(nèi)存占用的模塊,避免出現(xiàn) oom,也避免過度釋放帶來的渲染黑塊;在部分 oom 的情況,規(guī)避原生內(nèi)核主動崩潰的邏輯,在內(nèi)存極低的情況,部分功能不可用,而不是崩潰。
對我們的啟示
- 小程序存儲管理
增加小程序的存儲,包括內(nèi)存和磁盤,可以緩存部分數(shù)據(jù),增加頁面直出速度。同時對于磁盤的管理,按照小程序賬號雙重維度進行劃分。
- 第三方業(yè)務接入能力限制
在支持第三方的接入之后,按照現(xiàn)有方式將會導致對于安全和第三方的行為完全不可控,可以參考微信,支付寶方式采用自定義標記語言的方式對標記語言做限制,并提供純凈的JS環(huán)境來進行JS環(huán)境的執(zhí)行,WebView只負責渲染。
- 首屏速度
參考支付寶方案,在加載的時候,現(xiàn)將老的頁面呈現(xiàn)給用戶,然后在新頁面完成之后,計算差值,再進行顯示。
- Native繪制結合
Native繪制采用通過JS和Native通信的方式,將Native控價加入到布局的制定區(qū)域。
- 網(wǎng)絡請求發(fā)送托管
網(wǎng)絡請求等全部交由Native托管,更好的控制網(wǎng)絡請求,監(jiān)控網(wǎng)絡請求。