內存洩露是指new了一塊內存,但無法被釋放或者被垃圾回收。
new了一個對象之後,它申請佔用了一塊堆內存,當把這個對象指針置為null時或者離開作用域導致被銷燬,那麼這塊內存沒有人引用它了在JS裡面就會被自動垃圾回收。
但是如果這個對象指針沒有被置為null,且代碼裡面沒辦法再獲取到這個對象指針了,就會導致無法釋放掉它指向的內存,容易造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。
一、module閉包
1
2
3
4
5
6
7
8
9
10
11
// module date.js
let date = null;
export default {
init () {
date = new Date();
}
}
// main.js
import date from 'date.js';
date.init();
在 mainjs文件 初始化了date之後,date這個變量就一會直存在了,直到你把頁面關了 因為date的引用是在另一個module裡面,可以理解為模塊就是一個閉包對外是不可見的。
二、事件綁定 -> 閉包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 一個圖片懶惰加載引擎示例
class ImageLazyLoader {
constructor ($photoList) {
$(window).on('scroll', () => {
this.showImage($photoList);
});
}
showImage ($photoList) {
$photoList.each(img => {
// 通過位置判斷圖片滑出來了就加載
img.src = $(img).attr('data-src');
});
}
}
// 點擊分頁的時候就初始化一個圖片懶惰加載的
$('.page').on('click', function () {
new ImageLazyLoader($('img.photo'));
});
這裡就發生了內存洩露,主要是以下3行代碼導致的:
1
2
3
$(window).on('scroll', () => {
this.showImage($photoList);
});
因為這裡的事件綁定形成了一個閉包,this/$photoList這兩個變量一直沒有被釋放, this是指向ImageLazyLoader的實例,而$photoList是指向DOM結點。
解決方法: 銷燬實例的時候把綁定的事件off掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ImageLazyLoader {
constructor ($photoList) {
this.scrollShow = () => {
this.showImage($photoList);
};
$(window).on('scroll', this.scrollShow);
}
// 新增一個事件解綁
clear () {
$(window).off('scroll', this.scrollShow);
}
showImage ($photoList) {
$photoList.each(img => {
// 通過位置判斷圖片滑出來了就加載
img.src = $(img).attr('data-src');
});
// 判斷如果圖片已全部顯示,就把事件解綁了
if (this.allShown) {
this.clear();
}
}
}
// 點擊分頁的時候就初始化一個圖片懶惰加載的
let lazyLoader = null;
$('.page').on('click', function () {
lazyLoader && (lazyLoader.clear());
lazyLoader = new ImageLazyLoader($('img.photo'));
});
在每次實例化一個ImageLazyLoader之前把先把上一個實例clear掉,clear裡面進行解綁。解綁後JS引擎檢測到那個閉包沒用了,就把那個閉包銷燬了,那麼閉包引用的外部變量也自然會被置空。
三、綁定在window上的函數
1
2
3
4
5
6
7
8
9
mounted () {
window.addEventListener('resize', this.getScale);
}
beforeDestroy () {
window.removeEventListener('resize', this.getScale);
},
四、$on綁定
1
2
3
4
5
6
7
8
9
mounted () {
EventBus.$on('goToNextHomeworkTask', this.go2NextQuestion);
}
destroyed () {
EventBus.$off('goToNextHomeworkTask', this.go2NextQuestion);
}
五、$store的watch監聽
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mounted () {
this.$store.watch(state => state.currentIndex, (newIndex, oldIndex) => {
if (this.$refs.animation && newIndex === this.task.index - 1) {
this.$refs.animation.beginElement();
}
});
}
mounted () {
this.unwatchStore = this.$store.watch(state => state.currentIndex, (newIndex, oldIndex) => {
// 代碼略
});
},
destroyed () {
this.unwatchStore();
}
六、動畫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
let loadingAnimate = null;
let bodymovinAnimate = {
// 顯示loading動畫
showLoading () {
loadingAnimate = bodymovinAnimate._showAnimate();
return loadingAnimate;
},
// 停止loading動畫
stopLoading () {
loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
},
// 開始lottie動畫
_showAnimate () {
const animate = lottie.loadAnimation({
// 參數省略
});
return animate;
}
// 結束lottie動畫
_stopAnimate (animate) {
animate.stop();
let $container = $(animate.wrapper).closest('.bodymovin-container');
$container.remove();
},
};
export default bodymovinAnimate;
// 結束lottie動畫
_stopAnimate (animate) {
animate.destroy();
let $container = $(animate.wrapper).closest('.bodymovin-container');
$container.remove();
},
// 停止loading動畫
stopLoading () {
loadingAnimate && bodymovinAnimate._stopAnimate(loadingAnimate);
loadingAnimate = null;
},
所以綜合上面的分析,造成內存洩露的可能會有以下幾種情況:
1.監聽在window/body等事件沒有解綁
2.綁在EventBus的事件沒有解綁
3.Vuex的$store watch了之後沒有unwatch
4.模塊形成的閉包內部變量使用完後沒有置成null
5.使用第三方庫創建,沒有調用正確的銷燬函數
[ShareSDK] 輕鬆實現社會化功能 強大的社交分享
[SMSSDK] 快速集成短信驗證 聯結通訊錄社交圈
[MobLink] 打破App孤島 實現Web與App無縫鏈接
[MobPush] 快速集成推送服務 應對多樣化推送場景
[AnalySDK] 精準化行為分析 + 多維數據模型 + 匹配全網標籤 + 垂直行業分析顧問
BBSSDK | ShareREC | MobAPI | MobPay | ShopSDK | MobIM | App工廠
截止2018 年4 月,Mob 開發者服務平臺全球設備覆蓋超過84 億,SDK下載量超過3,300,000+次,服務超過380,000+款移動應用