閉包內存泄露分析


看了大家的討論,我也很感興趣,所以特意去了MSDN瞭解了下IE解析器小組對閉包的解釋,自己做了下研究。覺得挺有意思的,發出來給大家分享下。

該分析不僅僅適用js,凡是可以實現閉包的語言都存在相同問題。

何謂閉包:方法內的局部變量,可以在該方法執行完後(即該方法的作用域外部)被訪問。
或者說:方法內局部變量的生命週期超過了方法本身的生命週期。

看js示例1:
function AdderFactory(y) {
  return function(x){return x + y;}
}
var MyFunc;
if (whatever)
  MyFunc = AdderFactory(5);
else
  MyFunc = AdderFactory(10);
print(MyFunc(123)); // Either 133 or 128.
第一點要牢記,js中一切都是對象,方法也是,屬性也是,變量也是。

在上例中,方法(對象)AdderFactory內部創建了一個匿名方法(對象)function(x){return x + y;}。並且,我們利用return指令將該匿名方法的一個引用扔給AdderFactory方法外部使用,比如可以用於賦值。
如果變量whatever爲true,則語句MyFunc = AdderFactory(5);執行完後,變量MyFunc就被賦予了AdderFactory對象中用return扔出的匿名對象的引用。
一般情況下,AdderFactory方法(對象)內部所有變量的生命週期都應該小於該方法的執行時間。但在示例中,由於MyFunc變量保持了對匿名對象的引用,所以該匿名對象在AdderFactory方法執行後不會被GC回收,又由於匿名對象是AdderFactory對象的一個屬性,所以同時GC也不會回收AdderFactory對象。
以上示例代碼執行完成後的對象引用圖如下:

[attach]7395[/attach]

圖中矩形和橢圓代表對象,箭頭代表引用。

閉包的功能非常強大,上面的示例中也不會造成不好的影響。

但是,濫用閉包將十分可怕。

看js示例2:
<html>
<script language="JavaScript">
<!--
Function closureTest (){
        var maskDiv = document.createElement("div");
        maskDiv.id = "myDiv";
        maskDiv.style.width = "100%";
        maskDiv.style.height = "100%";
        maskDiv.style.position = "absolute";
        maskDiv.style.filter = "alpha(opacity=50)";
        maskDiv.style.backgroundColor = "red";
        maskDiv.oncontextmenu = function(){ return false; };
        document.body.appendChild(maskDiv);
}
//-->
</script>
<body onload=”closureTest();”>
</body>
</html>

爲了突出主題,我省略了不影響示例的多餘標籤。
用IE執行一個如上的頁面,打開windows任務管理器,查看iexplore.exe進程的“內存使用”。
按幾下F5看看內存是如何增加的。^_^
這就是閉包導致內存泄露問題。

看分析:
這是一個閉包應用。爲什麼?
看這裏,看這裏,看這裏
maskDiv.oncontextmenu = function(){ return false; };

爲了說明問題,看下圖:

[attach]7396[/attach]

顯然,這裏出現了循環引用(實箭頭是直接引用,虛箭頭是間接引用)。爲什麼?
我們看看程序執行過程:
①.        頁面載入完成後IE調用body的onload事件的綁定方法closureTest(),並以該方法爲構造器創建closureTest對象。

然後執行var maskDiv = document.createElement("div");語句。該語句有下面3步,
②.        通過document.createElement("div")方法生成一個div對象,對象id在之後賦予。
③.        通過var maskDiv語句生成一個closureTest的內部變量(對象)maskDiv。
④.        通過“等號”= 賦值,將一個對新建div的引用存入maskDiv變量。

一直到maskDiv.style.backgroundColor = "red";語句都是對div對象的設置,與本例無關。
然後執行maskDiv.oncontextmenu = function(){ return false; };語句。該語句也有下面3步,
⑤.        在closureTest對象內創建一個匿名對象Anonymity,其構造器是function(){ return false; }方法。
⑥.        在IE中,也許oncontextmenu屬性是div原有的屬性(對象),也許不是,但不管如何,maskDiv.oncontextmenu語句保證myDiv對象必定有該屬性(有則用之,無則創建)。
⑦.        通過“等號”= 賦值,將匿名對象的引用存入oncontextmenu對象中。

顯然,例2是比例1略微複雜的一個閉包應用。
本來,光是實箭頭還不至於產生循環引用,但是由於閉包的使用,導致Anonymity與maskDiv對象出現了間接引用。因爲在closureTest()方法執行完後,由於圖中⑦的引用的存在,Anonymity對象將繼續快樂的生活着,所謂“一佛得道,雞犬升天”。closureTest對象也要沾點喜氣啦,而maskDiv變量的長壽也就順理成章羅。如此循環往復,無窮盡也。

那麼,如果順其自然,IE何時纔會結束他們的圈地運動呢?
答案是關閉當前頁面所處的IE窗口時。
但是,可惡的是,如果你是在某些IDE內使用預覽功能看頁面的話,由於IDE替代IE接管了內存管理,所以,圈地運動的結束被迫延遲到了,關閉IDE時,比如EditPlus。
這太恐怖了,我要手工幹掉這些亂炒房地產的開發商。OK,沒問題,我們有幾十種方法,但爲了方便,舉例4種典型方法如下:

方法一:移花接木
這是常用的解決辦法,將
var maskDiv = document.createElement("div");
語句前的var定義去掉,maskDiv對象將不屬於closureTest對象,變成了一個全局變量。
變化後的引用關係圖如下:

[attach]7397[/attach]

很顯然,圈地運動不可能發展起來了。但其還是有點不盡如人意。
1.        maskDiv是全局變量,當頁面複雜時(比如引入了多個js文件),隨意污染window對象的全局作用域,將陷入名稱空間碰撞的泥潭。
2.        自然情況下,這些對象及引用一旦創建,則只能在頁面卸載時被GC回收,有點浪費內存空間。——我們可是有名的鐵公雞。

方法二:戰爭踐踏
在腳本中增加如下代碼:
function myGC(){
        document.body.removeChild(document.getElementById("myDiv");
}
在body標籤上增加onunload事件處理 onunload=” myGC();”
這有些暴力了,在卸載頁面時,我們強行將myDiv給幹掉,循環引用自然被打破,皮之不存,毛將焉附!
但是,它的缺點也是明顯的,
1.        增加了龐大的代碼量
看人家方法一,不但沒有增加代碼,還去掉了個var,咱可到好,一個div就多寫這麼多代碼,那複雜點的頁面還不把人給煩死。
2.        增加了代碼耦合
myDiv這個名字這麼好聽,我closureTest就想自己留着用,憑啥要告訴你個myGC。知道個名字也就算了,還要知道我把她嫁到了body家,鬱悶那。

方法三:溫柔一刀
比方法二溫柔一點,既然不讓幹那麼野蠻的事情,我們把方法二中的
document.body.removeChild(document.getElementById("myDiv");
修改爲
document.getElementById("myDiv").oncontextmenu = null;
在頁面卸載時,將引用圖中的⑦給一刀斬斷。不要把問題擴大化,應該在局部解決。
但是,本質上,方法三並不比方法二好很多,龐大的代碼量和強耦合還是我們的心頭刺。

方法四:Oh,my god! 上帝說,“這很簡單”!
我們只需要在
document.body.appendChild(maskDiv);
語句後增加一句話,一切煩惱都煙消雲散。
maskDiv = null;
真是太完美了,優點如下:
1.        只是簡單的增加了幾個字符,
2.        他不會在window對象下增加危險的全局變量,不會出現無聊的命名衝突,
3.        在closureTest()方法執行後,引用圖中的③、④就乖乖的讓出內存地盤,真應了偶鐵公雞的名號。(一些小氣的GC還是不聞不問)
4.        沒有增加討厭的myGC()方法,不需要在body上再綁定onunload事件。讚美神!少敲好多代碼啊。
5.        myDiv這名字就我closureTest自己用了,減少了可能的耦合點,贊!


神說:“簡單就是美!”

綜上所述,閉包之所以容易引起內存泄露,本質是由於其容易引起循環引用。所以,如何發現並避免出現循環引用是我們要關注的重點。不要被所謂閉包的新名詞遮蔽我們的雙眼。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章