引言:什么是內存泄漏?
想象一下你的手機是一個酒店,每個應用程序都是酒店的客人。當客人(應用程序)使用房間(內存)時,酒店經理(系統)會分配房間給他們使用。正常情況下,客人退房(應用關閉)后,房間應該被清理并重新可用。
內存泄漏就像是客人離開了酒店卻忘了退房,房間一直被占用無法重新分配。隨著時間推移,被占用的房間越來越多,最終酒店沒有空房可供新客人使用——這就是應用程序變慢甚至崩潰的原因。
先復習一下前端開發中的內存泄漏
常見的前端內存泄漏場景
1. 意外的全局變量
// 錯誤示例:意外創建全局變量
function createLeak() {leakedData = '這是一個全局變量'; // 沒有使用var/let/const
}// 正確做法
function noLeak() {const safeData = '局部變量'; // 使用const或let
}
2. 未清理的定時器和回調函數
// 可能引發內存泄漏
setInterval(() => {// 某些操作
}, 1000);// 正確做法:保存引用并適時清理
const intervalId = setInterval(() => {// 某些操作
}, 1000);// 在組件卸載時清理
clearInterval(intervalId);
3. DOM引用未釋放
// 保存DOM引用
const elements = {button: document.getElementById('myButton'),container: document.getElementById('myContainer')
};// 即使從DOM移除,JavaScript仍保留引用
document.body.removeChild(document.getElementById('myContainer'));// 需要手動釋放引用
elements.container = null;
4. 閉包使用不當
// 可能引起內存泄漏的閉包
function createClosure() {const largeData = new Array(1000000).fill('*');return function() {// 閉包持有largeData引用,即使外部函數已執行完畢console.log('閉包被調用');};
}// 正確使用:避免不必要的閉包引用
function safeClosure() {return function() {// 不引用外部變量console.log('安全的閉包');};
}
前端內存泄漏檢測工具
-
Chrome DevTools
-
Memory面板:生成堆快照(Heap Snapshot)
-
Performance面板:記錄內存分配情況
-
Allocation instrumentation on timeline:跟蹤內存分配
-
使用示例
// 手動觸發垃圾回收(僅在DevTools中有效)
if (window.gc) {window.gc();
}
鴻蒙應用中的內存泄漏
鴻蒙內存管理特點
鴻蒙系統使用方舟編譯器和高性能的運行時環境,但仍然需要開發者注意內存管理。
常見的鴻蒙內存泄漏場景
1. 未取消注冊的回調
javascript
// 注冊回調 appContext.registerReceiver(receiver, intentFilter);// 必須適時取消注冊 appContext.unregisterReceiver(receiver);
2. 資源未正確釋放
javascript
// 使用資源 const pixelMap = await image.createPixelMap(byteBuffer);// 使用完畢后必須釋放 pixelMap.release();
3. 組件引用未清理
javascript
// 自定義組件中 @Component struct MyComponent {private controller: VideoController = new VideoController();// 必須實現aboutToAppear和aboutToDisappearaboutToDisappear() {// 清理資源this.controller.release();} }
4. 異步任務未取消
javascript
// 啟動異步任務 const task = new MyAsyncTask(); task.execute();// 在組件銷毀時取消任務 aboutToDisappear() {if (task) {task.cancel();} }
鴻蒙應用中使用工具來檢測
初步識別內存問題
- 使用實時監控功能(詳細使用方法請參考性能問題定界:實時監控)對應用的內存資源進行監控。正常操作應用,觀察運行過程中的應用內存變化情況。當在一段時間內應用內存沒有明顯增加或者在內存上漲后又逐漸回落至正常水平,則基本可以排除應用存在內存問題;反之,在一段時間內不斷上漲且無回落或者內存占用明顯增長超出預期,那么則可初步判斷應用可能存在內存問題。
- 當從實時監控頁面初步判斷應用可能存在內存問題后,可以使用Memory泳道來抓取應用內存在問題場景下的詳細數據以及變化趨勢,初步定界問題出現的位置。Memory泳道存在Allocation或Snapshot模板中,使用Allocation或Snapshot模板錄制均可。
- 創建模板后,將模板中的其余泳道去除勾選,僅錄制Memory泳道的數據。
說明
其余泳道會開啟對內存分配、內存對象等數據的抓取,這些功能會帶來額外的開銷,可能會對我們初步定界問題產生噪音,影響分析,故先排除錄制。
- 點擊三角按鈕
即開始錄制。
- 錄制過程中,不斷操作應用在問題場景的功能,將問題放大,便于快速定界問題點。
- 點擊下圖中方塊按鈕或者左側停止按鈕結束錄制。
- 錄制完成后,展開Memory泳道,其中ArkTS Heap表示方舟虛擬機內存,這部分內存受到方舟虛擬機的管控。Native Heap表示Native內存,主要是應用使用到的一些涉及Native的API所申請的內存以及開發者自己的Native代碼所申請使用的堆內存(通常是C/C++),這部分內存需要開發者自行管理申請和釋放。
當ArkTS Heap有明顯的上漲,說明在方舟虛擬機內的堆內存上可能存在內存泄漏,可以使用Snapshot模板進行下一步分析;當Native Heap有明顯的上漲,說明Native內存上可能存在內存泄漏,可以使用Allocation模板進行下一步分析。
使用Snapshot模板分析ArkTS內存問題
分析步驟
分析內存泄漏問題步驟如下:
- 使用Snapshot模板錄制數據;
- 在問題場景前拍攝快照;
- 觸發問題場景后,再次拍攝快照;
- 對比兩次快照的數據,可快速找到泄漏對象并做進一步分析;
- 當有多個對象在比較視圖都存在時,可以重復多次觸發問題場景后拍攝快照,分別和問題場景前拍攝的快照進行對比,觀察是否有對象出現明顯的線性變化趨勢,進一步縮小泄漏對象的范圍。
錄制Snapshot模板數據
- 連接設備后啟動應用,點擊應用選擇框選擇需要錄制的應用,選擇Snapshot模板,點擊Create Session或雙擊Snapshot圖標即可創建一個Snapshot的錄制模板。
- 創建模板后,點擊三角按鈕即開始錄制。
- 待右側泳道全部顯示recording后則表明正在錄制中。
- 拍攝第一次堆快照作為基準(點擊圖中①處拍攝按鈕,待②處顯示出紫色條塊表示快照拍攝完成)。
說明
方舟虛擬機提供了在獲取快照前自動GC(Garbage Collection,對堆內存進行垃圾回收)的能力,因此拍攝快照之前不用主動觸發GC。
- 多次觸發內存泄漏操作。可以操作5,7,11等這種特殊的次數。比如操作了5次對比兩個快照發現有很多創建了5次沒釋放的場景,則可能存在內存泄漏,再操作7次,如果創建了7次那就可以確認發生了泄漏。
- 拍攝第二次堆快照。
- 點擊下圖中方塊按鈕或者左側停止按鈕結束錄制。
分析ArkTS Heap
- 在每次拍攝堆快照之前,虛擬機都會觸發GC,所以理論上堆快照內存在的對象都是當前虛擬機已經無法GC掉的對象。我們可以將兩個堆快照進行比較,來查看哪些對象是在觸發問題場景時新增了且不能釋放的。切換到窗口下方詳情區域的“Comparison”頁簽,將兩次快照進行對比。圖中數據的含義是以Snapshot2作為基準,Snapshot2對比Snapshot1的數據變化量。
- 優先尋找與觸發內存泄漏操作次數強相關、與業務代碼強相關的Constructor,首先來分析這些對象是否正常。主要是按照Distance逐漸減小的方式找引用鏈,可以從references里面一層層去尋找,排查引用鏈上的可疑對象(一般指與業務代碼關聯的對象)。
說明
選擇一個實例結點,底部搜索欄的Path to GC Root按鈕成可點擊狀態。點擊該按鈕,系統會計算從GC Roots垃圾收集器根到選定實例對象的最短路徑(最短路徑是指Distance逐漸-1的路徑,最終抵達Distance = 1的結點),并在右側區域展示。
分析Snapshot數據
常見對象介紹
JSArray
目前所有JSArray展開后為數組里的各個元素:
其中__proto__:原型對象,所有數組的__proto__應該是一致的;length:內置屬性訪問器,可以訪問數組長度。
TaggedDict
位于(array)標簽中,一般為虛擬機內部創建的字典,ArkTS代碼層面不可見。
TaggedArray
位于(array)標簽中,一般為虛擬機內部創建的數組,ArkTS代碼層面不可見。
COWArray
位于(array)標簽中,一般為虛擬機內部創建的數組,ArkTS代碼層面不可見。
JSObject
JSObject展開后為內部的各個屬性如下:
以下通過具體代碼來介紹下實例化對象、聲明對象、構造函數間的關系:
// HelloWorldPage.ets
class People {
old: number
name: string
constructor(old: number, name: string) {
this.old = old;
this.name = name;
}
printOld() {
console.log("old = ", this.old);
}
printName() {
console.log("name = ", this.name);
}
}@Entry
@Component
struct HelloWorldPage {
@State message: string = 'Hello World';
private people: People = new People(20, "Tom");build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.height('100%')
}
}
采集到的snapshot數據如下:
202169對象對應的是People,其主要聲明了對象的屬性和方法。
實例化對象的__proto__屬性指向聲明時的對象,聲明對象里則會有constructor構造函數。當實例化多個對象時,實例化對象會有多個,但是聲明對象和構造函數只有一個。
JSFunction
目前所有JSFunction都在(closure)標簽中,展開即可看到所有JSFunction:
每個函數展開后為函數內的各個屬性:
其中HomeObject表示父類對象,即該方法屬于哪個對象;_proto_表示原型對象;LexicalEnv表示該函數的閉包上下文;name是內置屬性訪問器,可獲取函數名;FunctionExtraInfo表示額外信息,比如一些napi接口會在這里記錄函數地址;ProtoOrHClass表示原型或者隱藏類。
如果函數顯示為anonymous(),則表示為匿名函數;如果函數顯示為JSFunction(),則表示該函數可能為框架層函數,創建函數的時候未設置函數名。對于這兩種函數名不可見的情況,可以通過查看其引用來間接確認其名稱:
ArkInternalConstantPool
虛擬機創建的常量池,ArkTS代碼層面不可見,涉及到的字符串常量會在(array)標簽中展示:
LexicalEnv
閉包變量上下文;閉包是一個鏈狀結構,如下所示:
733這個節點本身是一個閉包數組,其中0號元素是調用者(或者再往上的調用者,以此類推)的閉包;1號元素存儲的是調試信息;2號及以后的元素存儲的就是閉包傳遞的變量,上例傳遞了一個變量。
InternalAccessor
內置屬性訪問器,會有getter和setter方法,通過getter、setter可以獲取、設置該屬性。
分析方法
查看對象名稱
對于聲明對象,可以通過constructor屬性來確定對象名稱。
對于實例化對象,一般沒有constructor,則需要展開__proto__屬性后查找constructor;
若對象里有一些標志性屬性,可以通過在代碼里搜索屬性名稱來找到具體是哪個對象。
如果對象間有繼承關系,則可以繼續展開__proto__:
總結
內存泄漏就像是軟件中的"慢性病",初期不易察覺,但長期積累會導致嚴重性能問題。無論是前端開發還是鴻蒙應用開發,都應該:
-
提高意識:認識到內存管理的重要性
-
遵循最佳實踐:使用正確的編程模式和API
-
定期檢查:使用工具檢測潛在的內存問題
-
及時修復:發現內存泄漏立即解決
通過良好的內存管理習慣,可以顯著提升應用性能,提供更流暢的用戶體驗。記住,預防總是比治療更有效,在編寫代碼時就考慮到內存管理,可以避免后期大量的調試和優化工作
華為開發者學堂