背景
近期收到相關的反饋表示App偶現渲染異常、閃退等問題,嚴重影響用戶體驗。為此,我們對App端webview頁面進行了梳理、重構和部分邏輯的優化,在排除了webview模塊本身的影響后,經排查,發現多個H5項目存在內存泄漏的問題。由架構直接維護的mobile-system模塊已定位並修復了引起內存泄漏的主要問題,鑒於上述問題的可能在多個業務組普遍存在,本文總結了我們排查和解決這一問題的思路和方法,希望通過此文檔,幫助業務定位和解決項目的內存泄漏問題提供借鑒和參考。
基礎概念
1、什么是內存泄露?
內存泄漏(Memory Leak)是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。 談到內存泄露就不得不提到另一個重要的概念:"垃圾回收" 機制。Javascript是一種高級語言,它不像C語言那樣要手動申請內存,然后手動釋放,js在聲明變量的時候自動會分配內存,普通的類型比如Number,一般放在棧內存里,對象放在堆內存里,聲明一個變量,就分配一些內存,然后定時進行垃圾回收。
2、什么是垃圾回收(GC:Garbage Collecation) ?
JavaScript是在創建變量(對象,字符串等)時自動進行了分配內存,並且在不使用它們時“自動”釋放。 釋放的過程稱為垃圾回收(GC:Garbage Collecation) 。 JavaScript 引擎中有一個后台進程稱為垃圾回收器,它監視所有對象,並刪除那些不可訪問的對象。
其原理是垃圾收集器會定期(周期性)找出那些不在繼續使用的變量,然后釋放其內存。但是這個過程不是實時的,因為其開銷比較大並且 GC 時停止響應其他操作,所以垃圾回收器會按照固定的時間間隔周期性的執行。 到底哪個變量是沒有用的?所以垃圾收集器必須跟蹤到底哪個變量沒用,對於不再有用的變量打上標記,以備將來收回其內存。用於標記的無用變量的策略可能因實現而有所區別,通常情況下有兩種實現方式:標記清除和引用計數,這里不作過多的闡述,文章下方附上更詳細的資料了解,感興趣的同學可自行了解。
小結
有了上述概念,我們很容易理解,js的內存泄露就是指new了一塊內存,但無法被釋放或者被垃圾回收。new了一個對象之后,它申請占用了一塊堆內存,當把這個對象指針置為null時或者離開作用域導致被銷毀,那么這塊內存沒有人引用它了在JS里面就會被自動垃圾回收。言歸正傳,既然js會分配內存並定時進行垃圾回收,那為什么還會產生內存泄漏的情況呢?
內存泄漏的產生原因?
如官方解釋的那樣:由於JavaScript是在創建變量(對象,字符串等)時自動進行了分配內存,並且在不使用它們時“自動”釋放“, 這個“自動”是混亂的根源,並讓JavaScript(和其他類似機制的高級語言)開發者錯誤的感覺他們可以不關心內存管理。因此,代碼中一些不規范的寫法如變量的不合理聲明和引用、濫用閉包、未清除 dom 元素的引用 dom 元素移除但對 dom 元素的引用沒有解除等情況均可能引發內存泄漏的情況。概括的說,如果一個不再使用的對象指針沒有被置為null,且代碼里面沒辦法再獲取到這個對象指針了,就會導致無法釋放掉它指向的內存,也就是說發生了內存泄露。舉個vue中的例子:
上述代碼在實例掛載后,監聽了document的click事件,且回調函數中存在對$refs節點的引用,會導致被引用的真實DOM節點在銷毀時不仍存在引用,從而導致分配的內存未被回收,就發生了內存泄露 。
首先要明白一點,如濫用閉包是導致內存泄漏的罪魁禍首,但並不代表所有的閉包都是有害的,比如特定情況下的閉包,就是利用了內存引用不會被釋放的形式,完成一些特殊功能,因此,我們應着重關注導致內存持續激增且失效后仍不能被回收的點。 下面結合實際的操作,介紹排查內存泄漏問題的方法。
如何排查內存泄漏問題?
對於js的內存泄漏問題,Chrome提供了Memory 內快照的記錄功能為我們分析內存問題提供參考,內存分析主要使用Chrome的debug工具Profiles面板,Profiles可以追蹤網頁程序的內存問題。打開控制台,選擇Memory選項, 如圖,左側是Profiles的start等操作選項,右側是Profiles提供的功能選項
Profiles提供了3個功能項
Heap snapshot
通過創建堆快照可以查看創建快照時網頁上的JS對象和DOM節點的內存分布情況,使用該工具可以創建JS的堆快照、內存分析圖、對比堆快照幫助定位內存泄漏問題。
Allocation instrumentation on timeline
從整個Heap角度記錄內存分配的時間軸信息。
點擊Start按鈕之后,執行可能會引起內存泄漏的操作,操作之后點擊左上角的Stop按鈕即可。在藍色豎線上通過縮放過濾構造器窗格來顯示在指定的時間幀內被分配的對象信息。在錄制過程中,在時間線上會出現一些藍色豎條,這些藍色豎條代表一個新的內存分配。
Allocation sampling
用於分析網頁上的JS函數在執行過程中的CPU消耗信息。
點擊Start按鈕,執行你想要去深入分析的頁面操作,當你完成你的操作后點擊Stop按鈕。然后會顯示一個按JS函數進行內存分配的分解圖,默認的視圖是Heavy,該視圖會把最消耗內存的函數顯示在最頂端。
內存快照只是為了幫助我們能更具體定位發生泄漏問題的點 ,但是這里並不建議直接拍攝內存快照進行排查。 根據排查內存問題的經驗,建議大家打開Chrome devtools,並開啟’性能監控器 ‘定位到導致內存持續激增且未被及時釋放的操作,這能為你的排查工作節省大量的不必要操作,如下圖所示:
根據性能監控器的反饋,我們可以很快的定位到內存激增時的操作,如點擊了某個按鈕或加載了某個組件的操作,這樣,我們就能盡快的將問題復現出來,並方便使用堆快照進行分析。
利用谷歌瀏覽器調試工具的內存快照分析問題引發的點,具體操作如下:
1、拍攝初始狀態的堆快照

2、找到使你內存增加的業務場景(如打開某個組件內存上升,關閉該組件時內存卻沒有釋放下降),截取兩段快照(如第一段為組件打開前,第二段為組件關閉后),對比內存大小這樣就可以知道這個組件打開一次造成了多少內存泄漏。

3、選擇第二段快照,右邊選擇Comparison,比對。

由上圖可以看到,VueComponent組件
新增了#New 538
釋放了#Deleted 41
泄漏值 538– 41 = 137 泄漏了 497個組件
泄漏內存大小Alloc Size 3286 B 字節
釋放的內存 Freed Size 0 B 字節
同理也可以觀察其他部分的情況,比如Object Array 有多少新增,釋放了多少,泄漏了多少。
重點:我們最終還是要注意VueComponent的泄漏值,因為VueComponent是會掛載對象、數據、事件的,所以那些Object Array產生泄漏值也很大可能是VueComponent造成的。
根據經驗,vue項目中最容易出現內存泄漏的情況如下:
(1)監聽在window/body/document等事件沒有解綁
(2)綁在EventBus的事件沒有解綁
(3)Vuex的$store watch了之后沒有unwatch
(4)模塊形成的閉包內部變量使用完后沒有置成null
(5)使用第三方庫創建,沒有調用正確的銷毀函數
我們在處理泄漏的時候最好盡可能保證VueComponent的泄漏值盡可能減少, 然后再依次排查js閉包、事件監聽 循環定時器方面的影響。
內存泄漏問題如何解決?
定位到了具體的問題,那么修改起來也就相對容易一些了,我們回顧上面的例子:
上述代碼在實例掛載后,定位到事件監聽引發的內存泄露,只需要將函數聲明到實例上,然后在beforeDestroy時機移除事件監聽就可以解決,如下圖所示:
當然,很多時候情況會相對復雜一些,如vue官方就提供了很好的示例:
接下來的示例展示了一個由於在一個 Vue 組件中使用 Choices.js 庫而沒有將其及時清除導致的內存泄漏。等一下我們再交代如何移除這個 Choices.js 的足跡進而避免內存泄漏。
下面的示例中,我們加載了一個帶有非常多選項的選擇框,然后我們用到了一個顯示/隱藏按鈕,通過一個 v-if 指令從虛擬 DOM 中添加或移除它。這個示例的問題在於這個 v-if
指令會從 DOM 中移除父級元素,但是我們並沒有清除由 Choices.js 新添加的 DOM 片段,從而導致了內存泄漏。
解決這個內存泄漏問題
在上述的示例中,我們可以用 hide()
方法在將選擇框從 DOM 中移除之前做一些清理工作,來解決內存泄露問題。為了做到這一點,我們會在 Vue 實例的數據對象中保留一個 property,並會使用 Choices API 中的 destroy()
方法將其清除。
通過這個更新之后的 CodePen 示例可以再重新看看內存的使用情況。
這樣做的價值
內存管理和性能測試在快速交付的時候是很容易被忽視的,然而,保持小內存開銷仍然對整體的用戶體驗非常重要。
考慮一下你的用戶使用的設備類型,以及他們通常情況下的使用方式。他們使用的是內存很有限的上網本或移動設備嗎?你的用戶通常會做很多應用內的導航嗎?如果其中之一是的話,那么良好的內存管理實踐會幫助你避免糟糕的瀏覽器崩潰的場景。即便都不是,因為一個不小心,你的應用在經過持續的使用之后,仍然有潛在的性能惡化的問題。
參考資料: