1. 程式人生 > >前端----閉包導致的記憶體洩露問題分析

前端----閉包導致的記憶體洩露問題分析

在工作中遇到了記憶體洩露問題,在這記錄一下:

首先簡單瞭解一下記憶體洩露的概念:連結
然後看一下比較通俗易懂的例子,也是平時很少會注意到的一些地方,瞭解一下:連結

看完後你會大致瞭解瀏覽器會有自己的一套回收機制,當分配出去的記憶體不使用的時候便會回收;記憶體洩露的根本原因就是你的程式碼中分配了一些‘頑固的’記憶體,瀏覽器無法進行回收,如果這些’頑固的’記憶體還在一直不停地分配就會導致後面所用記憶體不足,造成洩露;那為什麼會無法回收呢?我這邊遇到的問題是因為自己沒有處理好閉包的特性,導致造成了記憶體的洩露,文章的最後會分析這個問題;

專案需求

先簡單介紹一下我這邊的需要處理的工作:主要是將攝像機抓拍的人臉圖片與底層人臉庫匹配的資訊在頁面上進行實時展示。
這裡寫圖片描述

具體步驟:

1.每隔300ms從底層獲取所有的資訊,包括:抓拍圖片的url,對比圖片的url,對比的資訊等。
2.動態建立圖片顯示的dom,並將圖片的url賦值給img標籤。
3.在img標籤的load事件中將建立的dom進行渲染。

出現記憶體洩露的原因:

1.閉包造成的記憶體洩露。
注意:這裡我有一個很大的知識盲區:在刪除圖片dom的同時是否需要將其src置為空才能釋放掉圖片所佔的記憶體;

這邊舉個例子看一下:

<!DOCTYPE html>
<head> 
<style>
</style>
<title>Test</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
</head>
<body>
<img style="width:200px;height:100px;" id="img1" src="aa.bmp">
<button id="btn1">Remove1</button>
<img style="width:200px;height:100px;" id="img2" src="aa.bmp">
<button id="btn2">Remove2</button>
<script src="jquery.js"></script>
<script>
   function event1(){
        var img1=document.getElementById("img1")
        var btn1=document.getElementById("btn1")
        
        $(btn1).click(function(){
          $(img1).remove();//img1.parentNode.removeChild(img1);
        })
    }
    
    function event2(){
        var btn2=document.getElementById("btn2")
        
        $(btn2).click(function(){
          var img2=document.getElementById("img2")
          $(img2).remove();//img1.parentNode.removeChild(img1);
        })
    }
    
    event1();
    event2();
</script>
</body>
</html>

程式執行圖片(IE11下):
這裡寫圖片描述

通過IE11的記憶體分析器,第一個方法刪除了圖片並未釋放圖片記憶體,第一個和第二個方法的區別就在於這個img的dom宣告物件是在click函式的裡面還是外面,這個很關鍵,決定了其作用域,也就是是否存在閉包;

//正確修改如下:
function event1(){
        var img1=document.getElementById("img1")
        var btn1=document.getElementById("btn1")
        
        $(btn1).click(function(){
          $(img1).remove();
          img1.src="";
          img1=null;
        })
    }

後面會再重點講解一下閉包處理不當會引發的記憶體未釋放問題;

2.由於伺服器傳來的圖片大小不一樣,而顯示在頁面上的圖片大小要固定,在IE8下對於img標籤設定大小會導致記憶體洩露,無法釋放。(因為這邊每隔300ms就請求一次圖片,資料量太大,所以這個問題就會被放大得很明顯)

解決辦法:

1.當閉包中的變數確定不再使用的時候,將其手動釋放掉;
2.使用canvas進行圖片繪製
這是由於IE8下對img圖片設定大小會導致記憶體洩露,可以借用了一下別人的解決方案——使用canvas繪圖;這裡還存在一個相容canvas在IE8下的相容問題,使用了外掛excanvas.jshtml5.js來解決這個相容性問題。
需要注意的是:
IE8下需要先呼叫G_vmlCanvasManager.initElement函式後才能去呼叫canvas的API,否則會報錯;

if (document.all) { //IE8
   G_vmlCanvasManager.initElement(canvasDom1);
   G_vmlCanvasManager.initElement(canvasDom2);
}
var ctx1 = canvasDom1.getContext("2d");
.....

不過在IE8上會造成影象渲染有延遲的現象:圖片在完全渲染出來前會有一定機率出現一段空白;

閉包

下面重點分析一下閉包所造成的記憶體洩露的原因:

首先舉個閉包的例子:

  function event1(){
        var btn1=document.getElementById("btn1");
        var myName="liuzj";
        
        $(btn1).click(function(){
            console.log(myName);
        })
    }
    
  event1();

程式執行圖片:可以發現myName這個變數還是一直存在於記憶體中
這裡寫圖片描述

 function event1(){
        var btn1=document.getElementById("btn1");
        var myName="liuzj";
        
        $(btn1).click(function(){
            
        })
    }
    
    event1();

程式執行圖片:可以發現myName這個變數已經不存在於記憶體中
這裡寫圖片描述

上面這個例子就是一個閉包所帶來的隱藏問題。
由於js中只有函式才具有獨立的作用域,所以當click事件繫結的匿名函式去訪問myName的時候,發現myName並不在其自身作用域,而需要去其父作用域中去呼叫,形成了閉包,使得myName會一直存在。

如上圖所示,我們通過瀏覽器自帶的記憶體分析器,可以更加清楚的看到記憶體的變化,在第一個程式中myName在click事件執行完後還是一直存在於我們的記憶體中,而第二個程式中發現myName在click事件執行完後並沒有存在於記憶體,被回收了,這就是閉包特性造成的‘頑固的’記憶體,無法被瀏覽器所回收,需要手動去清除:myName=null;

為啥不被瀏覽器回收?
下面的分析純屬於自己的猜測:
如果看了前面推薦的文章再加上網上一些其他的文章,你會了解到瀏覽器有兩個回收機制叫做–引用計數和標記清除
而js中物件的賦值是傳的地址,也就是引用賦值,其他數字、字串、布林型別的是傳值賦值,由於閉包的原因,導致這個變數會一直儲存在記憶體中,如果是物件變數(包括陣列和函式)則使得引用次數不為0,如果是其餘變數則使得此變數標記為正在被使用,所以瀏覽器就利用自身的回收機制將其釋放。
不管瀏覽器到底是利用哪種回收機制去回收記憶體,這裡可以確定的是因為閉包的特性使得瀏覽器無法進行正常回收,需要我們自己手動清理

通過上面閉包例項的‘拋磚引玉’,對於專案中圖片渲染的閉包問題也就很好避免了,主動去清除閉包中所使用到的變數,這邊的抓拍圖片顯示的一部分程式(修改後的):
這裡寫圖片描述

   /*由於要隨時保持8張圖片的輪播,所以需要儲存一下當前頁面8張圖片的dom物件;*/
   /*當來第九張的時候再把最開始的第一張圖片資源釋放,所以需要一個數組來管理這8張圖片*/
   var bottomImgDomArray = []; //儲存下方渲染圖片dom物件,全域性變數
   
   //渲染下方抓拍圖片,bottomDom為渲染在頁面上的抓拍圖片dom物件
   function renderBottom(bottomDom) {
        var imgDom = bottomDom.getElementsByTagName('img')[0];
        //圖片載入完成再渲染
        $(imgDom).load(function () { //這裡需要等圖片載入成功再進行渲染,由於需要使用到父作用域的變數,所以形成了一個閉包。
            $("#live_cap_picture").prepend(bottomDom);
            bottomImgDomArray.push(imgDom);
            bottomDom = null;
            imgDom = null;
            if ($(".imgDivBottom").length - 8 > 0) { //保證記憶體中只儲存8張圖片的記憶體,多餘則清除
                bottomImgDomArray[0].src = ""; //important!很重要的一步
                bottomImgDomArray[0] = null;
                bottomImgDomArray.splice(0, 1);
                $(".imgDivBottom").eq(8).remove();
            }
        })
    }

當分析到這裡的時候,發現上面的程式碼是可以進行優化的,沒有必要去用全域性變數去儲存那八張圖片的dom物件,只要我在閉包中清除其dom物件引用,再等到第9張圖片來了後,直接使用remove刪除即可釋放其記憶體,這個最開始的例子中就已經驗證,而且還可以消除全域性變數所帶來的記憶體消耗。(之所以前面大費周章去使用全域性變數來儲存,還是前面說的,我存在一個盲區:刪除圖片的dom,是否還需要將其src賦值為空才能釋放圖片的記憶體)

	  $(imgDom).load(function () { 
            $("#live_cap_picture").prepend(bottomDom);
            bottomDom = null;
            if ($(".imgDivBottom").length - 8 > 0) { 
                $(".imgDivBottom").eq(8).remove();
            }
        })

這樣就簡單多了!!!!

上面遇到的是釋放記憶體中的閉包問題,其實我在做的過程中還遇到了一個典型的迴圈閉包問題;

為啥會遇到這個問題?
因為每次是先通過ajax去請求有多少條抓拍圖片資訊、對比資訊和它們對應的url地址,然後在ajax回撥函式中去渲染圖片dom,這樣就會存在一個回撥函式中渲染多張圖片的問題(下方是抓拍圖片渲染,右方是對比資訊渲染,兩者分開,但是都會存在渲染多張的問題),在程式碼中就需要為多個圖片dom繫結相應的load事件;
例項程式碼:

for (var i = 0; i < len; i++) {
     var capSrc = para.Groups[i].faceDetect_url;
     var libSrc = para.Groups[i].faceLibrary_url;
     var timeStamp = capSrc.match(/timestamp=\d+/g)[0].split('=')[1];
     var date = new Date(parseInt(timeStamp));
     var dataObj = {
         src : capSrc,
         time : [date.getHours() > 9 ? date.getHours() : '0' + date.getHours(), date.getMinutes() > 9 ? date.getMinutes() : '0' + date.getMinutes(), date.getSeconds() > 9 ? date.getSeconds() : '0' + date.getSeconds()].join(':')
     }

     var string1 = [
         '<div class="imgDivBottom">',
         '<img class="imgBottomClass" src="/faceRecognition_get/1/data?<%=src%>"/>',
         '<label><%=time%></label>',
         '</div>',
     ].join("");

     string1 = _.template(string1);
     var domString = string1(dataObj);
     var bottomDom = createDOMFromString(domString);
     var imgDom = bottomDom.getElementsByTagName('img')[0];
   
     /*以前是下面這樣寫的,而且這個函式裡面還用到了上面的imgDom和bottomDom這兩個變數*/
     $(imgDom).load(function () { 
          //doSomthing
      })
     ......     
 }

上面就是一個很典型的迴圈閉包中遇到的錯誤:
假如這個時候有2兩張圖片,則需要把這兩張圖片分別渲染出來,但是你會發現如果按照上面的寫法,渲染出來的兩張圖都是第二張;這是因為當for迴圈結束後imgDom和bottomDom就變為第二張圖片的資訊了,由於閉包的原因,使得這兩個變數一直存在,再當load事件觸發的時候使用的是一直存在於記憶體中的這兩個變數,所以渲染出來的都是第二張圖片的資訊。

方法1:

for(var i=0;i<len;i++){
	....
	$(imgDom).load(function (_imgDom,_bottomDom) { 
			return function(){      
			   //doSomthing
			}
     }(imgDom,bottomDom))
	....
 }

方法2:(也是我目前所使用的方法)

for(var i=0;i<len;i++){
	....
	renderRight(bottomDom);
	....
}

function renderRight(bottomDom){
	//doSomthing
}

當然還會遇到一些其他的小問題:比如右方對比資訊的圖片渲染,是需要等到抓拍圖片和底層人臉庫圖片都載入完成後再進行渲染,這個還是比較簡單的,簡單的寫一個小例子;

var flag=0;
for (var i = 0; i < 2; i++) {
     $(imgArray[i]).load(function () {
          flag++;
          if (flag == 2) {
            //渲染
          }
      })
 }

null

最後還想提一下關於null的問題:
null屬於js中的一種基本資料型別,null不會被js自動賦值,這個值是我們手動去賦予的。其實這個值我個人覺得可以理解為‘空指標’,下面舉些例子說一下null的用處:

var o1;
var o2={a:1};
o1=o2;

o2.a=2;
o1.a//2  


	/*上面可以理解為我為物件{a:1}申請了一塊記憶體空間,然後又申請了兩個變數o1和o2,這兩個相當於兩個指標,o2指向了{a:1}這塊記憶體空間,而o1並指向任何空間;*/
	/*然後o1=o2,代表著o1也同時指向了o2指向的那個空間,也就是{a:1}的地址;*/
	/*o2.a=2代表著去改變{a:1}這段記憶體空間中a的值,然後列印o1.a的值發現也變成了2,因為它們兩個都指向了同一個記憶體空間;
*/

接上面的程式碼

o2=null;
o2.a //報錯
o1.a //2

/*
	將o2賦值為null,代表著o2指向的是空指標,不再指向{a:2}這個物件,所以無法訪問a屬性,但是o1還是指向的原地址,所以可以訪問
*/

這裡為什麼要提這個null呢?
前面清除閉包中的圖片記憶體未釋放問題中用到了img.src=“”; img=null;

  function event1(){
        var img1=document.getElementById("img1")
        var btn1=document.getElementById("btn1")
        $(btn1).click(function(){
          $(img1).remove();
          img1.src=""; 
          img1=null;
        })
    }
    //上面程式碼中的img1.src=""是可以省略的,為什麼呢?
    //因為將img1=null代表著這段img1指向了空指標,而它所指向的那段記憶體也沒有其他指標去引用,當程式執行結束後會通過垃圾回收機制將其進行回收

但是參考如下程式碼:

	var arr=[];
    function event1(){
        var img1=document.getElementById("img1")
        var btn1=document.getElementById("btn1")
        arr.push(img1);
        $(btn1).click(function(){
          $(img1).remove();
          img1.=null; 
        })
    }
   
   //上面程式碼中的img1.src=""這句就不能去掉,不然也無法釋放圖片記憶體
   //因為arr[0]同樣指向了那段圖片的記憶體空間,而它都是全域性變數,程式執行結束瀏覽器不會釋放掉這段記憶體

之所以說上面這段程式碼也是因為我自己在寫的過程中遇到過這個問題,正好也學習一下null這個基本資料型別。