本文由QQ技術(shù)團(tuán)隊(duì)分享,本文收錄時(shí)有內(nèi)容修訂和大量排版優(yōu)化。
1、引言
QQ 作為國(guó)民級(jí)應(yīng)用,從互聯(lián)網(wǎng)興起就一直陪伴著大家,是很多用戶(hù)剛接觸互聯(lián)網(wǎng)就開(kāi)始使用的應(yīng)用。
而 QQ 桌面版最近一次技術(shù)架構(gòu)升級(jí)還是在移動(dòng)互聯(lián)網(wǎng)興起之前,在多年迭代過(guò)程中,QQ 桌面版也積累了不少技術(shù)債務(wù),隨著業(yè)務(wù)的發(fā)展和技術(shù)的進(jìn)步,當(dāng)前的架構(gòu)已經(jīng)無(wú)法很好支撐對(duì) QQ 的發(fā)展了。
在 2022 年初,我們下定決心對(duì) QQ 進(jìn)行全面的技術(shù)架構(gòu)升級(jí),對(duì)于這樣一個(gè)國(guó)民級(jí)應(yīng)用的重構(gòu),挑戰(zhàn)無(wú)疑是巨大的。
新版桌面 QQ 自?xún)?nèi)測(cè)以來(lái)也受到許多熱心網(wǎng)友和行業(yè)人士的關(guān)注,非常感謝大家在內(nèi)測(cè)過(guò)程中提的各種有建設(shè)性的建議和反饋。其中,也有一小部分有開(kāi)發(fā)背景的用戶(hù)對(duì)我們采用 Electron 框架表達(dá)擔(dān)心:高內(nèi)存占用、超大安裝包、啟動(dòng)緩慢等。究其原因還是擔(dān)心新版本 QQ 資源占用大、體驗(yàn)變差,針對(duì)用戶(hù)的擔(dān)心,我們?cè)趦?nèi)存上進(jìn)行了專(zhuān)項(xiàng)優(yōu)化,也取得了一些階段性的進(jìn)展,過(guò)程中也積累了不少經(jīng)驗(yàn),也借此機(jī)會(huì)分享給大家。
本文我們將和大家分享新版 QQ 在內(nèi)存優(yōu)化方面的探索和階段性?xún)?yōu)化進(jìn)展。雖然本文的討論主要集中在 Windows 平臺(tái),但由于 Electron 的跨平臺(tái)特性,大部分優(yōu)化措施也同樣適用于 macOS 和 Linux 平臺(tái)。
2、系列文章
本文是系列文章中的第9篇,本系列總目錄如下:
3、新版 QQ 在內(nèi)存上的挑戰(zhàn)
新版 QQ 在內(nèi)存上的挑戰(zhàn)主要表現(xiàn)在以下 4 個(gè)方面:
1)產(chǎn)品形態(tài):
由 1 個(gè)復(fù)雜的大面板(100+ 復(fù)雜程度不等的模塊)和一系列獨(dú)立功能窗口構(gòu)成。窗口與渲染進(jìn)程一一對(duì)應(yīng),窗口進(jìn)程數(shù)很大程度影響 Electron 的內(nèi)存占用。
對(duì)于那個(gè)復(fù)雜的大面板, 一旦沒(méi)有精細(xì)控制就很容易導(dǎo)致內(nèi)存持續(xù)走高。
▲ Electron 窗口多進(jìn)程示意
2)使用習(xí)慣:
用戶(hù)長(zhǎng)時(shí)間掛機(jī)。相比用完即走的 Web 頁(yè)面,QQ 用戶(hù)在一次登錄后,可能會(huì)掛機(jī)一個(gè)月以上。這段期間,如果沒(méi)有控制好 QQ 內(nèi)存使用,那么結(jié)果可能是內(nèi)存越占越大、用戶(hù)交互響應(yīng)變慢、甚至發(fā)生閃退。
3)版本迭代:
已經(jīng) 24 歲的 QQ 擁有眾多的功能和特性。
過(guò)去一年我們一直做這件事——從核心特性開(kāi)始快速補(bǔ)齊 Windows 版本的功能,同時(shí)也有一些高優(yōu)先級(jí)的新功能要上。持續(xù)且快速的版本迭代,很可能產(chǎn)生新問(wèn)題,使性能劣化。
4)應(yīng)用架構(gòu):
新版 QQ 依賴(lài)一個(gè) NT 核心數(shù)據(jù)模塊(C++ addon),為 UI 提供本地化的數(shù)據(jù)服務(wù)。QQ 的加載體驗(yàn)?zāi)茏龅饺绱私z滑,這個(gè)模塊起到了至關(guān)重要的作用。
同時(shí),與 NT 的聯(lián)動(dòng)優(yōu)化,也需要拉通客戶(hù)端 C++ 開(kāi)發(fā)同學(xué)共同完成。當(dāng)然,會(huì)存在一些溝通成本,但不可否認(rèn),能把內(nèi)存占用壓下來(lái),客戶(hù)端同學(xué)也付出了非常多的努力。
▲ 新桌面端 QQ 整體架構(gòu)
4、新版 QQ 的內(nèi)存現(xiàn)狀與優(yōu)化目標(biāo)
在著手優(yōu)化之前,我們結(jié)合舊版 QQ 以及其他優(yōu)秀的桌面應(yīng)用,給新版 QQ 設(shè)定了優(yōu)化目標(biāo):
- 1)第一階段目標(biāo):單個(gè)進(jìn)程內(nèi)存 < 300M。
- 2)第二階段目標(biāo):單進(jìn)程 <100M,整體 < 300M。
針對(duì)第1)階段目標(biāo):早先因?yàn)闆](méi)有騰出手處理內(nèi)存問(wèn)題,代碼中存在一些泄漏。長(zhǎng)時(shí)間掛機(jī)后比較容易出現(xiàn)單個(gè)進(jìn)程超過(guò) 300M 的情況。我們?cè)谌ツ?9 月份系統(tǒng)地處理過(guò)一波內(nèi)存問(wèn)題,基本可以保證單個(gè)進(jìn)程的內(nèi)存占用 < 300M。
針對(duì)第2)階段目標(biāo):整體是指啟動(dòng) QQ 聊天面板后,6 個(gè)進(jìn)程內(nèi)存占用之和。內(nèi)存達(dá)標(biāo)之后才允許交付新版 QQ Windows 版本。
▲ Windows 任務(wù)管理器的 QQ 內(nèi)存占用詳情
這些進(jìn)程會(huì)隨著 QQ 的啟動(dòng)一直存在。我們重點(diǎn)看下這 3 類(lèi)進(jìn)程,這也是內(nèi)存優(yōu)化的大頭:
- 1)node:Electron 的主進(jìn)程,負(fù)責(zé)窗口管理、跨進(jìn)程通信等。包含 NT 核心數(shù)據(jù)模塊,負(fù)責(zé)與服務(wù)端交互,為 UI 提供數(shù)據(jù)服務(wù);
- 2)renderer:Chromium 內(nèi)核的渲染進(jìn)程,負(fù)責(zé)渲染 UI、提供用戶(hù)交互等。QQ 啟動(dòng)后,會(huì)有 2 個(gè)渲染進(jìn)程:一個(gè)是 QQ 大面板,另一個(gè)是主進(jìn)程的窗口池。窗口池是預(yù)創(chuàng)建的一個(gè)渲染進(jìn)程。在新開(kāi)窗口時(shí),可以減少等待時(shí)間;
- 3)gpu:Chromium 內(nèi)核的 GPU 進(jìn)程。它的主要作用是處理與圖形相關(guān)的任務(wù),例如渲染網(wǎng)頁(yè)、播放視頻、執(zhí)行動(dòng)畫(huà)等。
設(shè)定了目標(biāo)后:我們先對(duì) QQ 的內(nèi)存占用情況進(jìn)行了摸底。我們從用戶(hù)的角度出發(fā),使用 Windows 任務(wù)管理器來(lái)觀察 QQ 的內(nèi)存占用情況。我們先從最簡(jiǎn)單的 “Hello World” 開(kāi)始,看看 Electron 應(yīng)用的最低內(nèi)存需求是多少,以及上限在哪里。結(jié)果顯示,只需要 68M,并沒(méi)有達(dá)到傳說(shuō)中的幾百 M 那么大。
然而:隨著使用的深入,比如在 QQ 聊天場(chǎng)景中進(jìn)行一些操作之后,主進(jìn)程、GPU 進(jìn)程和渲染進(jìn)程三個(gè)進(jìn)程的內(nèi)存占用就已經(jīng)達(dá)到了 600M。這意味著我們距離目標(biāo)還有超過(guò) 50% 的優(yōu)化空間。
(注:AIO 是聊天面板的簡(jiǎn)稱(chēng))
這個(gè)初步的觀察讓我們看到了目前的挑戰(zhàn),同時(shí)也讓我們看到了優(yōu)化的可能性。我們有信心,通過(guò)精心設(shè)計(jì)和持續(xù)優(yōu)化,逐步接近甚至超越我們?cè)O(shè)定的目標(biāo)。
5、內(nèi)存優(yōu)化我們都做了什么
接下來(lái),將重點(diǎn)介紹我們是如何掌控和優(yōu)化 Electron 的內(nèi)存的。
我們的工作主要包括以下幾個(gè)方面。
1)工具分析:首先,我們需要使用不同維度的內(nèi)存分析工具,從 V8 引擎到進(jìn)程,再到整個(gè)應(yīng)用程序,打通整個(gè)鏈路進(jìn)行多角度的細(xì)節(jié)分析,以此來(lái)定位內(nèi)存使用的瓶頸。
2)定向優(yōu)化:在通過(guò)工具定位到問(wèn)題之后,我們會(huì)采取一系列的針對(duì)性?xún)?yōu)化策略,包括緩存策略、按需加載、優(yōu)雅降級(jí)等。具體的優(yōu)化工作我們將在后面進(jìn)行詳細(xì)介紹。
3)線(xiàn)上監(jiān)控:在本地或小范圍內(nèi)驗(yàn)證通過(guò)之后,我們需要廣大用戶(hù)的驗(yàn)證來(lái)確認(rèn)我們的優(yōu)化措施是否適用于所有場(chǎng)景。然而,如何獲取用戶(hù)在 Windows 任務(wù)管理器中看到的內(nèi)存使用量是一個(gè)挑戰(zhàn),我們已經(jīng)做了大量的研究和驗(yàn)證。
4)防止性能退化和自動(dòng)化測(cè)試:為了保護(hù)我們辛苦得來(lái)的優(yōu)化成果,并避免頻繁的版本迭代影響 QQ 的內(nèi)存目標(biāo),我們會(huì)借助開(kāi)發(fā)框架、工具建設(shè)、代碼審查等手段來(lái)預(yù)防性能退化。
6、選擇合適的分析工具
在進(jìn)行性能優(yōu)化之前,我們需要選擇合適的工具來(lái)幫助我們分析問(wèn)題。
QQ 的代碼不僅包含 V8 的 JS 部分,還包括許多 Native 的 C++ 模塊。僅依靠 Chromium 開(kāi)發(fā)者工具進(jìn)行性能分析是不夠的,因此我們需要組合使用多種工具來(lái)共同解決問(wèn)題。
這些工具如何使用,由于篇幅的關(guān)系我們?cè)谶@里不做詳細(xì)介紹。
▲ 部分內(nèi)存分析工具截圖
7、定向優(yōu)化1:最大化資源使用率
7.1代碼及靜態(tài)資源
桌面版 QQ 的功能邏輯非常復(fù)雜,代碼量龐大。雖然代碼不需要通過(guò)網(wǎng)絡(luò)請(qǐng)求加載,本地加載速度通常較快,但加載如此龐大的代碼會(huì)占用大量?jī)?nèi)存。
因此,仍然需要進(jìn)行代碼瘦身、靜態(tài)資源優(yōu)化、分包和按需加載等優(yōu)化措施。
▲ Devtools > Memory 分析 QQ 主窗口內(nèi)存占用
首先是代碼瘦身:對(duì)于第三方包或 SDK,它們往往包含了完備的 Web 兼容性及能力,而這些對(duì)于 Electron 客戶(hù)端來(lái)說(shuō)并不是必需的。因此,我們會(huì)對(duì)它們進(jìn)行定制裁剪或獨(dú)立實(shí)現(xiàn),以減少代碼的加載。
對(duì)于 QQ 的業(yè)務(wù)代碼:分包策略不完全按照每個(gè)頁(yè)面(窗口)以及模塊復(fù)用次數(shù)來(lái)進(jìn)行制訂,更多的情況是按照?qǐng)鼍澳K來(lái)進(jìn)行細(xì)粒度的定制。
以打開(kāi)一個(gè)窗口到進(jìn)入使用場(chǎng)景為例:
- 1)窗口池中預(yù)啟動(dòng)的窗口頁(yè)面只加載必須執(zhí)行的基礎(chǔ)代碼;
- 2)當(dāng)打開(kāi)具體窗口時(shí)加載對(duì)應(yīng)的路由后頁(yè)面入口代碼;
- 3)當(dāng)具體使用不同功能時(shí)動(dòng)態(tài)加載(如點(diǎn)擊搜索、打開(kāi)表情面板、轉(zhuǎn)發(fā)消息激活好友選擇器的時(shí)候才會(huì)分別加載對(duì)應(yīng)功能模塊代碼)。
▲ QQ 主窗口業(yè)務(wù)模塊的拆解
此外:其他靜態(tài)資源(如 SVG、base64 圖像)在加載時(shí)也會(huì)占用不少內(nèi)存,所以我們采取了按需加載的策略(只在可見(jiàn)時(shí)加載,不可見(jiàn)時(shí)主動(dòng)銷(xiāo)毀和回收)。
▲ svg 及 base64 資源的 string 內(nèi)存占用
為了提升執(zhí)行效率和代碼保護(hù)的目的,我們將 JS 代碼轉(zhuǎn)成了字節(jié)碼。盡管跳過(guò)了源碼編譯,直接將字節(jié)碼交給 V8 執(zhí)行,但在程序報(bào)錯(cuò)還原堆棧等運(yùn)行時(shí)步驟中,V8 仍然會(huì)引用源碼字符串。為了去掉這份源碼,我們使用和源碼等長(zhǎng)的空格來(lái)占位,但通過(guò) devtool 檢查發(fā)現(xiàn)這些空格字符串仍會(huì)占用不少內(nèi)存空間。最終,我們采取修改和移除 V8 對(duì)源碼字符串引用的方式,徹底解決了源碼字符串的內(nèi)存占用問(wèn)題。
7.2圖片資源
QQ 作為一款 IM 工具,會(huì)涉及到大量的圖片收發(fā)。然而,圖片的渲染會(huì)占用相當(dāng)大的內(nèi)存。
舉個(gè)例子:一張分辨率為 4000 x 2750 的圖片,結(jié)合設(shè)備屏幕像素和聊天區(qū)設(shè)計(jì)尺寸,只需渲染寬度為 567 像素的分辨率圖像即可清晰展示。如果以寬度為 4000 像素的分辨率渲染,理論上兩者位圖所占用的內(nèi)存大小差距可達(dá) 50 倍,并且還會(huì)因?yàn)殇秩編?lái)性能損失。
▲ 圖片尺寸對(duì)內(nèi)存影響舉例
在聊天消息列表中的大部分圖片僅僅起到預(yù)覽作用,縮略圖渲染就滿(mǎn)足了需要。而僅僅在用戶(hù)真正打開(kāi)圖片查看器放大查看時(shí),才會(huì)需要用原圖渲染。
實(shí)測(cè)在聊天中多張不同大尺寸分辨率圖片在展示時(shí),渲染進(jìn)程和 GPU 進(jìn)程的內(nèi)存占用有著明顯差別。在收發(fā)圖片時(shí),我們會(huì)根據(jù)屏幕設(shè)備信息和計(jì)算展示區(qū)域所需實(shí)際渲染分辨率,當(dāng)原圖分辨率超出計(jì)算所需值,則先調(diào)用壓縮服務(wù)進(jìn)行圖片壓縮,生成渲染所需分辨率的縮略圖,并在聊天區(qū)域進(jìn)行渲染上屏。在這個(gè)策略的優(yōu)化下,一般聊天圖片場(chǎng)景測(cè)試下來(lái),使用縮略圖比原圖約有 30M ~ 50M 的內(nèi)存優(yōu)化。
▲ QQ 優(yōu)化圖片上屏策略
8、定向優(yōu)化2:可視區(qū)域按需渲染
8.1DOM 元素?cái)?shù)量
在 DOM 元素使用數(shù)量我們也有嚴(yán)格的控制,總體采用”所見(jiàn)即占用“的 DOM 渲染策略。在 QQ 大面板中只有視口所見(jiàn)的內(nèi)容才會(huì)渲染對(duì)應(yīng) DOM 元素。其他所有組件在不渲染展示時(shí),均會(huì)移除組件及其 DOM 元素來(lái)避免其內(nèi)存開(kāi)銷(xiāo)。
▲ 大虛擬列表控制 DOM 數(shù)量
尤其對(duì)于各個(gè)大列表模塊:比如聯(lián)系人列表和群成員列表,DOM 元素都非常多。最開(kāi)始的內(nèi)測(cè)版本中,使用有大量好友和群聊的 QQ 號(hào),窗口平均 DOM 數(shù)達(dá)到 13000。我們將 QQ 所有的普通分頁(yè)列表替換為虛擬滾動(dòng)列表,并且對(duì)列表滾動(dòng) buffer 進(jìn)行極限壓縮甚至是 0 buffer 。由于不再一味采取空間換時(shí)間,沒(méi)有 buffer 的情況下必然面對(duì)列表滑動(dòng)性能挑戰(zhàn),因此也需不斷優(yōu)化各類(lèi) item 組件渲染性能。
此外:我們還通過(guò)精簡(jiǎn)組件 DOM 層級(jí),移除非核心組件 keep-alive(重新優(yōu)化渲染性能)等方式,大賬號(hào)使用下整體的 DOM 數(shù)量從 13000 減少到控制在平均 4000 以?xún)?nèi),這部分優(yōu)化減少約 20M 內(nèi)存。
8.2渲染圖層
渲染圖層方面,在渲染時(shí)滿(mǎn)足某些特殊條件的渲染層,會(huì)被瀏覽器自動(dòng)提升為合成層,達(dá)到提升渲染性能的目的。但是每個(gè)合成層都占用額外的內(nèi)存,應(yīng)當(dāng)去掉過(guò)量且不必要的合成層來(lái)控制圖層帶來(lái)的內(nèi)存占用。當(dāng)然結(jié)合渲染性能考量,對(duì)于高頻且列表等核心模塊,是可以單獨(dú)提升合成層。
▲ QQ 對(duì)于渲染合成層的優(yōu)化處理
在桌面端 QQ 中通過(guò)超級(jí)調(diào)色盤(pán)可以為進(jìn)行色彩換膚,在這個(gè)場(chǎng)景中全局各模塊有不少單獨(dú)提升的合成層來(lái)實(shí)現(xiàn)毛玻璃、漸變和紋理效果。另外還有許多不經(jīng)意間被提升的隱式合成層。通過(guò)對(duì)不必要的合成層進(jìn)行移除與合并,整體也優(yōu)化了約 9.3M 內(nèi)存。
8.3結(jié)構(gòu)化消息
QQ 支持豐富的消息類(lèi)型,從簡(jiǎn)單的文本、圖文消息,到復(fù)雜的 lottie 表情、下圖所示的業(yè)務(wù)可定制的結(jié)構(gòu)化消息等。我們知道 JavaScript 是單線(xiàn)程的,這些消息同時(shí)上屏的時(shí)候可能會(huì)出現(xiàn)過(guò)長(zhǎng)的上屏任務(wù)而導(dǎo)致 UI 卡頓,給到用戶(hù)的感受就是切換消息列表卡頓,消息上屏慢等糟糕的體驗(yàn)。
新版 QQ 針對(duì)這類(lèi)復(fù)雜消息上屏,使用了 JavaScript 事件機(jī)制結(jié)合 WebWorker 來(lái)實(shí)現(xiàn)消息異步上屏,并使用 OffscreenCanvas + Worker 池繪制來(lái)提升渲染性能。
▲ QQ 結(jié)構(gòu)化消息的處理方案
為了在 Canvas 中實(shí)現(xiàn) CSS 的 Flex 布局效果,我們采用了跨平臺(tái)的布局解決方案,將 Yoga 編譯成 WebAssembly 運(yùn)行在 WebWorker 中。Yoga 官方編譯采用的是 asm.js 的方案, 這種方案不支持動(dòng)態(tài)分配內(nèi)存,可以看到它默認(rèn)分配了一個(gè)較高的內(nèi)存,達(dá)到了 128M。
▲ Yoga 渲染引擎的原始內(nèi)存占用
為了優(yōu)化 WebAssembly 的內(nèi)存占用,我們調(diào)整了編譯方式,將 Yoga 編譯成獨(dú)立的 wasm 文件,這種方式相比 asm.js 支持動(dòng)態(tài)內(nèi)存分配。
同時(shí)結(jié)合聊天窗口的消息卸載策略,經(jīng)過(guò)不斷的測(cè)試調(diào)優(yōu),在既要保證初始內(nèi)存較少又要盡可能避免內(nèi)存爆發(fā)式增長(zhǎng)帶來(lái)的性能損耗的前提下,我們把 WebAssembly 的初始內(nèi)存分配優(yōu)化到 2M,再加上對(duì)象共享、享元模式等策略,WebWorker 的內(nèi)存占用有了非常可觀的優(yōu)化。
▲ QQ 結(jié)構(gòu)化消息渲染引擎優(yōu)化前后的內(nèi)存占用對(duì)比
復(fù)雜的聊天消息雖然是必不可少的功能,但是實(shí)際的消息量還是遠(yuǎn)少于普通的圖文消息,因此在保證用戶(hù)體驗(yàn)的前提下,在合適的條件下適時(shí)銷(xiāo)毀 WebWorker 是一個(gè)合理的策略,而隨著 WebWorker 被銷(xiāo)毀這個(gè)線(xiàn)程所占用的內(nèi)存也能被完全釋放。
9、定向優(yōu)化3:性能與體驗(yàn)的平衡
9.1Lottie 及動(dòng)畫(huà)方案選型
超級(jí)表情采用 Lottie 動(dòng)畫(huà)技術(shù)方案,有高清高幀率高質(zhì)量特點(diǎn),但同時(shí)也為我們帶來(lái)了渲染的高成本。
為了保證 Lottie 的高幀率和減少 CPU 占用,我們緩存了 Lottie 渲染器生成的動(dòng)畫(huà)幀,內(nèi)存消耗成為了首要問(wèn)題。
▲ QQ Lottie 動(dòng)畫(huà)示例
對(duì)其進(jìn)行定量分析:超級(jí)表情 Lottie 資源繼承自手機(jī) QQ,尺寸是 512 × 512,動(dòng)畫(huà)幀以 int8 數(shù)組存儲(chǔ),所以一幀動(dòng)畫(huà)為 512 × 512 × 4 / 1024 bit= 1024 Kb = 1Mb。一個(gè)普通大小的超級(jí)表情,例如慶祝表情,有 160 幀動(dòng)畫(huà),依據(jù)緩存 9/10 幀動(dòng)畫(huà)的策略,慶祝表情會(huì)占用 144Mb 內(nèi)存。雖然是可回收的,但也無(wú)疑是巨大的內(nèi)存消耗。
關(guān)注到 Lottie 渲染的內(nèi)存消耗后,我們主要從以下 2 步入手:
1)緩存的動(dòng)畫(huà)幀尺寸:桌面端 lottie 渲染大小為 120 × 120,考慮到需要保持 Lottie 動(dòng)畫(huà)的高質(zhì)量,緩存的動(dòng)畫(huà)幀尺寸調(diào)整為實(shí)際尺寸大小的兩倍,即 240 × 240,降低內(nèi)存消耗 72%。經(jīng)設(shè)計(jì)確認(rèn),清淅度上也沒(méi)有明顯的差異;
2)緩存策略:緩存 9/10 的動(dòng)畫(huà)幀減少到緩存 3/4,降低內(nèi)存消耗 35%,而且調(diào)整之后幀率還能得到保障。
通過(guò)以上 2 步,一共降低內(nèi)存消耗 81.8%,慶祝表情從 144 Mb 降低到 35 Mb。
最后:舊策略對(duì)于渲染過(guò)且暫時(shí)不用的 Lottie 表情,會(huì) buffer 它的第一幀,總共 31 個(gè) Lottie 表情(2.3k * 31 = 7M(最多)),經(jīng)評(píng)估之后,我們暫時(shí)也拿掉了該策略。
▲ QQ Lottie 動(dòng)畫(huà)緩存首幀對(duì)內(nèi)存的影響
另外:桌面 QQ 左側(cè)導(dǎo)航欄目,為了與移動(dòng)端統(tǒng)一體驗(yàn),使用 Lottie 動(dòng)畫(huà)來(lái)實(shí)現(xiàn),從 memory 面板來(lái)看, 4 個(gè) icon 導(dǎo)航條會(huì)占用約 6M 的內(nèi)存。改用 CSS 實(shí)現(xiàn),不僅效果與 Lottie 的幾乎一致,而且這 6M 的內(nèi)存占用就完全省掉了。
▲ QQ 導(dǎo)航條動(dòng)畫(huà)對(duì)內(nèi)存的影響
9.2APNG 動(dòng)畫(huà)優(yōu)化
APNG 是一個(gè)基于 PNG 的位圖動(dòng)畫(huà)格式,后綴名也是.png,在一些類(lèi)似背景圖的場(chǎng)景下會(huì)使用。
在早期的超級(jí)調(diào)色盤(pán)中,為了實(shí)現(xiàn)最佳炫彩效果,選用了由 300 張 15KB 靜態(tài)圖合成的配色漸變的 apng 圖片,其大小達(dá)到了 4.2M。
不過(guò)帶來(lái)的問(wèn)題是渲染的延遲感。經(jīng)過(guò)和設(shè)計(jì)討論,在不影響效果體驗(yàn)的基礎(chǔ)上,進(jìn)行了大量的壓縮,壓縮到了 157KB(壓縮率超過(guò) 96%)。
9.3聊天列表與消息
聊天列表 AIO,作為 QQ IM 模塊中最主要的承載消息數(shù)據(jù)展示模塊,其滾動(dòng)體驗(yàn)必然離不開(kāi)用戶(hù)體驗(yàn)與內(nèi)存的權(quán)衡。
聊天列表在靜態(tài)與滾動(dòng)過(guò)程中,維持消息組件的數(shù)量多少?zèng)Q很大程度決定整個(gè) QQ 的內(nèi)存占用。消息數(shù)據(jù)從服務(wù)端拉取后會(huì)存儲(chǔ)在本地 DB,根據(jù)策略會(huì)將當(dāng)前會(huì)話(huà)的消息數(shù)據(jù)緩存在內(nèi)存中。
隨著滾動(dòng)加載,消息緩存占用的內(nèi)存也越多。所以也有一定動(dòng)態(tài)閾值的策略,丟棄滾動(dòng)方向相反的舊消息,從而將內(nèi)存控制在可接受范圍。如果用戶(hù)重新操作又需要加載時(shí),這請(qǐng)求底層向本地磁盤(pán) DB 重新拉取。
▲ QQ 聊天消息列表的加載策略
消息組件實(shí)例是內(nèi)存占用的大戶(hù),每條消息組件內(nèi)部包含頭像 / 昵稱(chēng) / 狀態(tài) / 內(nèi)容等多個(gè)實(shí)例,如果不對(duì)消息實(shí)例進(jìn)行回收銷(xiāo)毀,每百條消息約能帶來(lái) 20M+ 的內(nèi)存增量,因此消息實(shí)例的回收策略尤為關(guān)鍵。
最早版本中對(duì)消息上屏沒(méi)有丟棄策略,內(nèi)存增量沒(méi)有很好控制。于是采用分頁(yè)列表,屏內(nèi)保持固定幾頁(yè)消息(約 30 ~ 50 條消息,視屏幕尺寸決定),超過(guò)范圍的消息進(jìn)行丟棄,列表高度由屏內(nèi)消息直接撐起,用戶(hù)通過(guò)觸頂或觸底進(jìn)行上下一頁(yè)消息的加載。
但這頁(yè)帶來(lái)些點(diǎn)問(wèn)題:一方面隨著觸頂觸底,滾動(dòng)條頻繁跳動(dòng)的體驗(yàn)并不好。另一方面列表高度由不定高的組件渲染消息來(lái)維持,不得不始終保留 30 ~ 50 條消息以撐起滾動(dòng)高度,不可見(jiàn)消息的那部分便造成內(nèi)存的浪費(fèi)。
使用虛擬列表維持計(jì)算高度后,列表不再依賴(lài)保持真實(shí)消息內(nèi)容的渲染,理論上我們可以將可視區(qū)域以外的消息實(shí)例全部銷(xiāo)毀,僅保留用戶(hù)可見(jiàn)的消息,最大程度地壓縮消息實(shí)例數(shù)量,指保留很少的 buffer 消息實(shí)例。在實(shí)際滾動(dòng)中由于消息實(shí)例在滾動(dòng)過(guò)程被不斷創(chuàng)建和銷(xiāo)毀,占用主線(xiàn)程,影響 UI 繪制和用戶(hù)輸入。
因此我們還做了:
- 1)對(duì)創(chuàng)建銷(xiāo)毀做一定聚合,批量處理消息上屏;
- 2)精簡(jiǎn)優(yōu)化單條組件的渲染性能;
- 3)不同滾動(dòng)方向調(diào)整上下不同 buffer 大小 等等措施;
- 4)會(huì)話(huà)切換和窗口聚失焦最小化等操作時(shí)對(duì)不再使用的消息資源內(nèi)存進(jìn)行主動(dòng)回收。
▲ QQ 聊天消息列表的上屏策略
滾動(dòng)性能和內(nèi)存占用之間需要取得平衡,既要最大程度壓縮上屏消息數(shù)量以節(jié)省內(nèi)存,又要保證滾動(dòng)性能體驗(yàn)。然而經(jīng)過(guò)優(yōu)化后,本地測(cè)試加載 200 條混合種類(lèi)的消息場(chǎng)景下,從空狀態(tài)進(jìn)入聊天會(huì)話(huà)中,消息列表內(nèi)存增量從最多 44.2M 降至 6.1M,且滾動(dòng)靜止后內(nèi)存不會(huì)任意增長(zhǎng)。
10、定向優(yōu)化4:Electron的正確使用姿勢(shì)
Electron 給主進(jìn)程提供了不少對(duì)系統(tǒng)能力調(diào)用的 API,如托盤(pán)、系統(tǒng)通知、macOS 中 dock 欄設(shè)置等。但是如果對(duì)這些 Electron 能力的使用方式不對(duì),就可能導(dǎo)致不必要的大量?jī)?nèi)存占用甚至是泄漏。
比如 QQ 中:我們通過(guò)短間隔定時(shí)調(diào)用 Tray setImage API 來(lái)實(shí)現(xiàn) QQ 托盤(pán)的閃爍,如果不注意傳入 string Path 則會(huì)每次創(chuàng)建 Image 對(duì)象導(dǎo)致內(nèi)存占用,正確的方式應(yīng)該創(chuàng)建 NativeImage 并緩存,調(diào)用 Tray setImage 傳入指定 NativeImage,避免反復(fù)創(chuàng)建 Image 導(dǎo)致的內(nèi)存問(wèn)題。
▲ Windows 托盤(pán)圖標(biāo)內(nèi)存泄漏定位
類(lèi)似的問(wèn)題還有在 Mac OS 中調(diào)用 API dock.setIcon 也會(huì)持續(xù)占用約 20M 的 CGImage 位圖內(nèi)存,正確的方案應(yīng)該是不通過(guò) Electron API 指定,而是通過(guò)打包 plist(屬性文件) 指定 dock 欄圖標(biāo)。
▲ Mac OS dock 圖標(biāo)內(nèi)存泄漏定位
在使用 Electron 的過(guò)程中,還存在類(lèi)似會(huì)導(dǎo)致內(nèi)存問(wèn)題的使用方式,我們需要結(jié)合客戶(hù)端內(nèi)存工具進(jìn)行深度挖掘和分析,才能發(fā)現(xiàn)和處理這些問(wèn)題。
11、定向優(yōu)化5:消滅內(nèi)存泄漏
我們知道 V8 有自己的垃圾回收機(jī)制,雖然它在 GC(垃圾回收)方面有著其各種策略,并做了各種優(yōu)化從而盡可能的確保垃圾得以回收,但我們?nèi)詰?yīng)當(dāng)避免任何可能導(dǎo)致無(wú)法回收的代碼操作。
常見(jiàn)的例子包括:
- 1)未移除的監(jiān)聽(tīng)器和定時(shí)器:在監(jiān)聽(tīng)事件處理函數(shù)其中引用的不被釋放導(dǎo)致的泄漏;
- 2)游離 DOM 未釋放:移出 document 后游離 DOM 仍存在引用導(dǎo)致無(wú)法釋放。較多發(fā)生于框架的組件銷(xiāo)毀時(shí),相關(guān)監(jiān)聽(tīng)未取消導(dǎo)致組件沒(méi)有釋放的情況;
- 3)監(jiān)控 / 打點(diǎn)導(dǎo)致的泄漏:在使用 Performance.mark 打點(diǎn)監(jiān)控時(shí),產(chǎn)生 PerformanceMark 對(duì)象,在用完之后沒(méi)有手動(dòng)清除,也會(huì)導(dǎo)致內(nèi)存泄漏;
- 4)console.error 導(dǎo)致的泄漏:控制臺(tái)持有被打印對(duì)象始終不釋放,導(dǎo)致應(yīng)用的泄漏;
- 5)其他不當(dāng)?shù)拈]包及隱式的全局變量。
以上是桌面 QQ 在早期遇到的常見(jiàn)問(wèn)題。后續(xù),我們通過(guò)代碼檢測(cè)手段來(lái)防范這類(lèi)問(wèn)題的出現(xiàn)。
與一般的前端項(xiàng)目不同,由于桌面 QQ 的長(zhǎng)周期使用特性,任何緩慢而微小的內(nèi)存泄漏都可能被放大,這也是我們極力把控并阻止任何可能導(dǎo)致內(nèi)存泄漏的代碼引入的原因。
12、優(yōu)化結(jié)果與線(xiàn)上監(jiān)控
經(jīng)過(guò)一系列組合優(yōu)化之后,在我們自己的設(shè)備上來(lái)看,QQ 的內(nèi)存使用基本是達(dá)標(biāo)了,長(zhǎng)時(shí)間掛機(jī)穩(wěn)定在 300M 以下,但在廣大 QQ 用戶(hù)側(cè)能否保持這個(gè)水平?只有通過(guò)線(xiàn)上內(nèi)存及性能的采集監(jiān)控,才有數(shù)據(jù)指標(biāo)來(lái)觀測(cè),從而才能對(duì)優(yōu)化有效性進(jìn)行驗(yàn)證和決定如何調(diào)整優(yōu)化方向。
好在 Electron 提供了 app.getMetics 、 process.getMemoryInfo 等 API 來(lái)采集內(nèi)存指標(biāo)。
但需要注意的是:這些 API 所采集返回的內(nèi)存值的真實(shí)含義,如 getMetric 所采集的到 workingsetSize 和 privateBytes 均不是任務(wù)管理器用戶(hù)所看到的內(nèi)存。
這里我們通過(guò) patch 定制改造 Electron getMetics API,來(lái)增加不同平臺(tái)任務(wù)管理器的內(nèi)存類(lèi)型的指標(biāo),并且采集包含了主進(jìn)程、渲染進(jìn)程、GPU 進(jìn)程和工具進(jìn)程等所有內(nèi)存指標(biāo)。
為了避免頻繁采集上報(bào)內(nèi)存指標(biāo)所帶來(lái)的的性能消耗,我們?cè)O(shè)定了一定時(shí)間的采集間隔,同時(shí)針對(duì)使用場(chǎng)景的采集做了抽樣。并將渲染進(jìn)程 pid 映射尋找窗口名,只在若干次采集后再做聚合計(jì)算,通過(guò) SDK 上報(bào)到 prometheus + grafana 的指標(biāo)觀測(cè)平臺(tái)。
▲ QQ 內(nèi)存監(jiān)控整體方案
經(jīng)過(guò)若干次內(nèi)存性能優(yōu)化的迭代,目前從線(xiàn)上數(shù)據(jù)指標(biāo)來(lái)看,新版 Windows QQ 運(yùn)行的內(nèi)存在主場(chǎng)景下基本控制在 300M,這個(gè)值已經(jīng)基本達(dá)到我們?cè)O(shè)定的目標(biāo)。
從登錄后使用過(guò)程中的內(nèi)存指標(biāo)如下:
- 1)整體應(yīng)用的內(nèi)存平均占用約為 228M;
- 2)中位數(shù)占用約為 211M;
- 3)90% 分位用戶(hù)內(nèi)存占用約為 350M。
當(dāng)然,這個(gè)目標(biāo)只是階段性的,我們還會(huì)持續(xù)針對(duì)更多使用場(chǎng)景進(jìn)行內(nèi)存優(yōu)化。
13、防劣化與自動(dòng)化測(cè)試
為了持續(xù)關(guān)注和保證新版 QQ 項(xiàng)目的性能達(dá)標(biāo)且不劣化,除了比較常規(guī)的單元測(cè)試、代碼檢查、代碼評(píng)審機(jī)制、框架內(nèi)置一些開(kāi)發(fā)規(guī)范等手段外,我們還在建設(shè)一個(gè)防劣化平臺(tái),主要通過(guò)自動(dòng)化的端對(duì)端 (e2e) 測(cè)試來(lái)持續(xù)監(jiān)控項(xiàng)目集成后的性能變化。
主要是:
- 1)定時(shí)對(duì)主干上集成構(gòu)建的程序進(jìn)自動(dòng)化 e2e 測(cè)試;
- 2)除了對(duì)功能的冒煙測(cè)試外,針對(duì)重點(diǎn)關(guān)注的性能指標(biāo),構(gòu)造了對(duì)應(yīng)的帳號(hào)和環(huán)境,編輯特定的用例,用于采集性能指標(biāo);
- 3)通過(guò)將采集和采樣的指標(biāo)上報(bào)到防劣化的監(jiān)控平臺(tái),來(lái)監(jiān)控項(xiàng)目集成后的性能變化,如會(huì)話(huà)切換響應(yīng)時(shí)間、內(nèi)存占用、CPU 使用率等;
- 4)監(jiān)控平臺(tái)提供按版本和時(shí)間的指標(biāo)曲線(xiàn)、對(duì)比,方便查看和分析性能變化情況。同時(shí)打通企業(yè)微信機(jī)器人,對(duì)性能指標(biāo)情況進(jìn)行實(shí)時(shí)推送告警。
根據(jù)告警信息對(duì)應(yīng)的版本信息和代碼記錄,排查情況,閉環(huán)問(wèn)題。
▲ 防劣化機(jī)制示意圖
這一套機(jī)制之前在 內(nèi)測(cè)中的 QQ 頻道桌面端的項(xiàng)目中嘗試應(yīng)用,運(yùn)行發(fā)現(xiàn)了一些比較典型的代碼異常、crash、oom 問(wèn)題,證明確實(shí)有效。新版 QQ 業(yè)務(wù)和設(shè)計(jì)都更復(fù)雜,建設(shè)好防劣化機(jī)制無(wú)論是對(duì)發(fā)現(xiàn)問(wèn)題的效率,還是對(duì)整體的性能和質(zhì)量都是意義重大的,也是我們團(tuán)隊(duì)當(dāng)前重點(diǎn)建設(shè)、未來(lái)持續(xù)迭代的重要任務(wù)。
▲ 防劣化推送與告警實(shí)際應(yīng)用圖例
14、本文小結(jié)
可能大家比較關(guān)心,為什么一定要選擇 Electron?
其實(shí)我們是經(jīng)過(guò)深思熟慮的:
首先:全新 QQ 意味著我們應(yīng)該專(zhuān)注在功能快速迭代上,否則,以 QQ 的體量戰(zhàn)線(xiàn)會(huì)拉得非常長(zhǎng)。
我們希望最后選擇的跨平臺(tái)方案應(yīng)該是足夠成熟、低開(kāi)發(fā)和使用成本,不需要為了使用框架本身,還需要投入額外巨大的人力成本。這個(gè)其實(shí)在 React Native、Flutter、Tauri 等跨平臺(tái)框架的使用過(guò)程中,我們都遇到過(guò)類(lèi)似的問(wèn)題,除了功能開(kāi)發(fā),為了把框架生態(tài)、周邊、工具鏈建設(shè)好,還需要投入巨大的額外成本,Qt 也有類(lèi)似的問(wèn)題。
而使用 Electron,對(duì)于 Web 前端開(kāi)發(fā)同學(xué),基本上是 0 成本,現(xiàn)有的 Web 前端的大部分基建都可以直接復(fù)用,而且使用 Web 開(kāi)發(fā) UI 的效率,在主流技術(shù)棧里算是很高的了。
并且這幾年主流的桌面端應(yīng)用基本都選擇了 Electron,如 VScode、Discord、Slack、Skype、Whatsapp、Figma 等等,新的桌面應(yīng)用基本上也是首選 Electron。
另外,Electron 版本的迭代速度和社區(qū)氛圍都很在線(xiàn)。
其次:從結(jié)果或者解決問(wèn)題的角度來(lái)看,經(jīng)過(guò)一系列優(yōu)化之后基本可以將 QQ 核心聊天場(chǎng)景的內(nèi)存控制在 300M 以?xún)?nèi),150M 的安裝包大小,與舊版純 Native QQ 差別較小。不單單內(nèi)存占用,其他核心體驗(yàn),比如切 AIO 的流暢度上要優(yōu)于舊版 QQ。即便是在今天,QQ 也堅(jiān)定一年半之前選擇了 Electron。
最后:讓我們?cè)俅尉劢乖趦?nèi)存優(yōu)化的工作上,下圖是我們?cè)谧烂?QQ 中針對(duì) Electron 內(nèi)存優(yōu)化工作的一個(gè)概覽。
▲ 桌面 QQ 內(nèi)存優(yōu)化工作概覽
內(nèi)存優(yōu)化沒(méi)有銀彈,有的只是一步一個(gè)腳印深入做下去,芝麻西瓜都要撿,從量變到質(zhì)變。
未來(lái)我們完全有信心,憑著已有的經(jīng)驗(yàn)和對(duì)其技術(shù)的理解,守住現(xiàn)在這些成果的同時(shí),進(jìn)一步優(yōu)化 QQ 生態(tài)下的各個(gè)子業(yè)務(wù)、子模塊的內(nèi)存占用問(wèn)題。
因此,也希望通過(guò)我們實(shí)踐經(jīng)驗(yàn)分享, 讓大家從更多辯證的視角來(lái)重新看待 Electron 或類(lèi) CEF 的技術(shù)方案。
15、相關(guān)資料
[1] Electron官方開(kāi)發(fā)者手冊(cè)
[2] 快速了解新一代跨平臺(tái)桌面技術(shù)——Electron
[3] Electron初體驗(yàn)(快速開(kāi)始、跨進(jìn)程通信、打包、踩坑等)
[4] Electron 基礎(chǔ)入門(mén) 簡(jiǎn)單明了,看完啥都懂了
[5] vivo的Electron技術(shù)棧選型、全方位實(shí)踐總結(jié)
[6] 融云基于Electron的IM跨平臺(tái)SDK改造實(shí)踐總結(jié)
[7] 閑魚(yú)IM基于Flutter的移動(dòng)端跨端改造實(shí)踐
[8] 網(wǎng)易云信基于Electron的IM消息全文檢索技術(shù)實(shí)踐
[9] 閑話(huà)即時(shí)通訊:騰訊的成長(zhǎng)史本質(zhì)就是一部QQ成長(zhǎng)史
[10] 技術(shù)往事:創(chuàng)業(yè)初期的騰訊——16年前的冬天,誰(shuí)動(dòng)了馬化騰的代碼
[11] 技術(shù)往事:史上最全QQ圖標(biāo)變遷過(guò)程,追尋IM巨人的演進(jìn)歷史
[12] QQ的成功,遠(yuǎn)沒(méi)有你想象的那么順利和輕松
[13] 還原真實(shí)的騰訊:從最不被看好,到即時(shí)通訊巨頭的草根創(chuàng)業(yè)史
附錄:更多QQ團(tuán)隊(duì)分享的技術(shù)文章
《騰訊技術(shù)分享:騰訊是如何大幅降低帶寬和網(wǎng)絡(luò)流量的(圖片壓縮篇)》
《騰訊技術(shù)分享:騰訊是如何大幅降低帶寬和網(wǎng)絡(luò)流量的(音視頻技術(shù)篇)》
《騰訊技術(shù)分享:Android版手機(jī)QQ的緩存監(jiān)控與優(yōu)化實(shí)踐》
《騰訊技術(shù)分享:Android手Q的線(xiàn)程死鎖監(jiān)控系統(tǒng)技術(shù)實(shí)踐》
《讓互聯(lián)網(wǎng)更快:新一代QUIC協(xié)議在騰訊的技術(shù)實(shí)踐分享》
《騰訊技術(shù)分享:社交網(wǎng)絡(luò)圖片的帶寬壓縮技術(shù)演進(jìn)之路》
《QQ音樂(lè)團(tuán)隊(duì)分享:Android中的圖片壓縮技術(shù)詳解(上篇)》
《QQ音樂(lè)團(tuán)隊(duì)分享:Android中的圖片壓縮技術(shù)詳解(下篇)》
《騰訊團(tuán)隊(duì)分享:手機(jī)QQ中的人臉識(shí)別酷炫動(dòng)畫(huà)效果實(shí)現(xiàn)詳解》
《騰訊團(tuán)隊(duì)分享 :一次手Q聊天界面中圖片顯示bug的追蹤過(guò)程分享》
《QQ 18年:解密8億月活的QQ后臺(tái)服務(wù)接口隔離技術(shù)》
《以手機(jī)QQ為例探討移動(dòng)端IM中的“輕應(yīng)用”》
《騰訊原創(chuàng)分享(一):如何大幅提升移動(dòng)網(wǎng)絡(luò)下手機(jī)QQ的圖片傳輸速度和成功率》
《騰訊原創(chuàng)分享(二):如何大幅壓縮移動(dòng)網(wǎng)絡(luò)下APP的流量消耗(下篇)》
《騰訊原創(chuàng)分享(三):如何大幅壓縮移動(dòng)網(wǎng)絡(luò)下APP的流量消耗(上篇)》
《信鴿團(tuán)隊(duì)原創(chuàng):一起走過(guò) iOS10 上消息推送(APNS)的坑》
《騰訊信鴿技術(shù)分享:百億級(jí)實(shí)時(shí)消息推送的實(shí)戰(zhàn)經(jīng)驗(yàn)》
《IPv6技術(shù)詳解:基本概念、應(yīng)用現(xiàn)狀、技術(shù)實(shí)踐(上篇)》
《IPv6技術(shù)詳解:基本概念、應(yīng)用現(xiàn)狀、技術(shù)實(shí)踐(下篇)》
《騰訊TEG團(tuán)隊(duì)原創(chuàng):基于MySQL的分布式數(shù)據(jù)庫(kù)TDSQL十年鍛造經(jīng)驗(yàn)分享》
《了解iOS消息推送一文就夠:史上最全iOS Push技術(shù)詳解》
《騰訊資深架構(gòu)師干貨總結(jié):一文讀懂大型分布式系統(tǒng)設(shè)計(jì)的方方面面》
《騰訊音視頻實(shí)驗(yàn)室:使用AI黑科技實(shí)現(xiàn)超低碼率的高清實(shí)時(shí)視頻聊天》
《騰訊技術(shù)分享:微信小程序音視頻與WebRTC互通的技術(shù)思路和實(shí)踐》
《騰訊技術(shù)分享:GIF動(dòng)圖技術(shù)詳解及手機(jī)QQ動(dòng)態(tài)表情壓縮技術(shù)實(shí)踐》
《社交軟件紅包技術(shù)解密(一):全面解密QQ紅包技術(shù)方案——架構(gòu)、技術(shù)實(shí)現(xiàn)等》
《社交軟件紅包技術(shù)解密(九):談?wù)勈諵紅包的功能邏輯、容災(zāi)、運(yùn)維、架構(gòu)等》
《社交軟件紅包技術(shù)解密(十):手Q客戶(hù)端針對(duì)2020年春節(jié)紅包的技術(shù)實(shí)踐》
《QQ設(shè)計(jì)團(tuán)隊(duì)分享:新版 QQ 8.0 語(yǔ)音消息改版背后的功能設(shè)計(jì)思路》
《微信技術(shù)分享:揭秘微信后臺(tái)安全特征數(shù)據(jù)倉(cāng)庫(kù)的架構(gòu)設(shè)計(jì)》
《IM跨平臺(tái)技術(shù)學(xué)習(xí)(九):全面解密新QQ桌面版的Electron內(nèi)存占用優(yōu)化》
(本文已同步發(fā)布于:http://www.52im.net/thread-4429-1-1.html)