关于Ext内存泄漏的部分心得

来源:互联网 发布:time machine网络备份 编辑:程序博客网 时间:2024/05/22 11:43
首先说明下,本帖所描述的泄漏是指JS运行中的释放问题,它们大多数在页面关闭时都能释放掉。 
---- 
内存释放在使用Ext开发OPOA系统时显得尤为重要 
去年开始接触Ext开发,中间花了很多时间来解决内存泄漏。 
最开始Ext还是3.0版,泄漏得一塌胡涂,也没什么处理头绪,只知道在onDestroy里加delete语句,用sIEve看效果。 
而Ext3.1有了一个很大的进步,基本上绝大多数组件都没有泄漏了。但并不意味着Ext没泄漏,做出的页面也没泄漏。JS是自动回收的,而只要有一个引用没有释放就可能导致一大片JS对象及Dom节点无法释放,所以迟早还是会面对这个问题。 

本人不擅长表达,就直接列心得吧。 

一、泄漏的分类 
1. 组件创建后未销毁(Ext层面)
 
这里说的组件一般是指继承自Ext.Component的,它在创建时会注册到Ext.ComponentMgr中,不调用destroy方法是不会从中移除的,所以它永远不会被释放。 
大部分组件是放置于Container中,Container销毁会将子组件一起销毁,是不存在这个问题。但当它是Ext.Window(没有父容器)或手工render的组件(例如用模板画html,再render到指定Dom节点)时,如果不主动销毁问题就发生了。 

2. 组件自身有泄漏或使用不当(JS&Dom层面) 
目前Ext原生组件的泄漏已经很少了,但自己扩展的组件不注意的话会造成浏览器无法释放的泄漏。 
(参考:http://www.ibm.com/developerworks/cn/web/wa-memleak/index.html) 
而使用不当是指破坏了组件内部结构,导致无法完全释放。 

3. JS对象泄漏(JS层面) 
JS是有自动回收机制,但它也只会回收你用不到的对象(或者说你已经不可能访问到的对象) 
所以,如果在一个长时间运行的JS代码中,你创建了很多属性或对象,并且一直保存着它们的引用,也会使内存增长。 
正确的做法是,不用的属性就要delete掉,不用的对象也要解除引用。(delete引用的属性或置变量为null) 

看似很少,积少成多也不得了,在1,2类泄漏解决完后,就要面对这样的问题了。 

4. 浏览器自身比较恶心的bug 
虽然JS&Dom循环引用无法释放也算是浏览器的bug,但好像不只是IE独有,所以没有归到此类。 
目前我已经知道的IE的两个很恶心的bug: 
1. IE Object leaks 
http://www.sencha.com/forum/showthread.php?89317-quot-IE-object-leaks-quot-What-s-going-on 
简单点描述,就是IE下,将JS对象的属性delete掉后,属性引用的对象是会解除引用,但属性本身占用的内存不会释放。虽然增长很少,但在极端情况也绝不能无视。 

2. IE 8 bug 
http://www.iteye.com/topic/617997  
(一点题外话,想不通为什么这帖被评为隐藏帖,大家都这么高手了?国内还真没看到什么人提起过这问题) 
在IE8下,form, button, input, select, textarea, a, img, object这些Dom节点,只要创建了就不会被释放掉(其实在IE6/7下,form节点也会有问题)。对这个真的很无语。。。 

二、泄漏检查 
1. 组件泄漏
 
这个比较简单,可以写个函数记录Ext.ComponentMgr.all(Ext.util.MixedCollection)中的组件列表,从而判断哪些组件还没有被销毁。 

2. 组件内部泄漏 
这个就要用sIEve查看了。创建、销毁组件,看Dom列表有无没释放的。 

3. JS对象泄漏 
当有个内存无法释放的问题,使用sIEve又检查不出Dom节点泄漏,一般就是JS对象的问题了 
只能用笨办法:逐步排除 
一种是使用Firefox + firebug,直接展开JS对象,查找属性有没有无限制增长。 
再一种就是使用IE6/7,调快逻辑执行,长时间运行,记录任务管理器中显示的虚拟内存(不能用其它浏览器,经测试FF和Chrome缓存很厉害,很难测出增长。也不能用sIEve,它监控内存及dom数量时也会造成内存增长,IE8有些bug会影响判断),看有无无限制增长。 

4. 浏览器自身bug 
对于IE Object leaks,同JS对象泄漏检测。 
IE8的,据我测试,sIEve用的是IE7内核,所以无能为力,也是同3的方法。 

三、定位并解决 
1. 组件泄漏
 
找到没销毁的,扩展onDestroy,在其中销毁掉。 

2. 组件内部泄漏 
定位泄漏的Dom节点关联的代码,查检有没有调用removeNode移除,有没有循环引用 
具体的不好讲,原因非常多,可以边改边用sIEve看效果。 
注意: 请开启Ext.enableListenerCollection = true;这个配置,以便Ext自动回收孤立Dom节点上的事件。默认只会将孤立节点从Ext.elCache中移除,而不会清理事件,可能会导致泄漏。另外需注意Ext每30秒才清理一次,注意分辨。 

3. JS对象泄漏 
这个没啥办法。。。只能调快操作长时间运行查看平均增长,然后一步步改代码排除了…… 

4. 浏览器自身bug 
IE Object leaks: 
参考Ext的解决方案,将对象for in循环复制一份,替换旧的。 
IE 8 bug: 
尽量避免重复创建form, button, input, select, textarea, a, img, object这些Dom节点,能替换就替换,能复用就复用。 

关于内存泄漏的解决方法,以上几乎没写什么有用的东西,说实话,我也不知道该写些什么。 
因为我处理的内存泄漏大部分都是排除法定位并解决的,现在除了sIEve能查看Dom节点泄漏,没啥好用的工具能检查与之相关的JS对象的情况。从而导致只能靠蒙来找到造成泄漏的代码。 

四、一些泄漏实例 
1. 组件泄漏 
举些常见的例子 
a) 弹出窗口未销毁 
Js代码  收藏代码
  1. Ext.ns("Ext.ux");  
  2. Ext.ux.MyWindow = Ext.extend(Ext.Window, {  
  3.     closeAction : "hide",  
  4.     modal : true,  
  5.     initComponent : function(){  
  6.         this.buttons = [{  
  7.             text : "确定",  
  8.             handler : function(){  
  9.                 this.fireEvent("confirm");  
  10.                 this.hide();  
  11.             },  
  12.             scope : this  
  13.         }];  
  14.         Ext.ux.MyWindow.superclass.initComponent.apply(this, arguments);  
  15.     }  
  16. });  
  17. Ext.ux.MyComponent = Ext.extend(Ext.Panel, {  
  18.     initComponent : function(){  
  19.         // 它创建出来,没有父容管理,也没有主动销毁  
  20.         this.win = new Ext.ux.MyWindow({  
  21.             width : 300,  
  22.             height : 400,  
  23.             title : "配置xxx",  
  24.             html : "内容xxx"  
  25.         });  
  26.         this.win.on("confirm"this.confirmCfg, this);  
  27.         this.tbar = [{  
  28.             text : "设置xxx",  
  29.             handler : function(){  
  30.                 this.win.show();  
  31.             },  
  32.             scope : this  
  33.         }];  
  34.         Ext.ux.MyComponent.superclass.initComponent.apply(this, arguments);  
  35.     },  
  36.     confirmCfg : function(){  
  37.         // do something  
  38.     }  
  39. });  
  40.   
  41. var test = new Ext.ux.MyComponent({  
  42.     title : "测试面板",  
  43.     html : "test",  
  44.     renderTo : Ext.getBody()  
  45. });  
  46.   
  47. // 创建&销毁  
  48. test.destroy();  
  49. test = null;  


b) 创建出来未使用,也未销毁 
Js代码  收藏代码
  1. Ext.ns("Ext.ux");  
  2. Ext.ux.MyComponent = Ext.extend(Ext.Panel, {  
  3.     initComponent : function(){  
  4.         this.btnA = new Ext.Button({  
  5.             text : "模式A",  
  6.             handler : function(){}  
  7.         });  
  8.         this.btnB = new Ext.Button({  
  9.             text : "模式B",  
  10.             handler : function(){}  
  11.         });  
  12.         // btnA与btnB必有一个未纳入Toolbar管理,也未主动销毁  
  13.         this.tbar = [this.mode==="a" ? this.btnA : this.btnB];  
  14.         Ext.ux.MyComponent.superclass.initComponent.apply(this, arguments);  
  15.     }  
  16. });  
  17.   
  18. // 创建&销毁  
  19. var test = new Ext.ux.MyComponent({  
  20.     title : "测试面板",  
  21.     html : "test",  
  22.     renderTo : Ext.getBody()  
  23. });  
  24.   
  25. test.destroy();  
  26. test = null;  


c) 直接render到dom子节点中,未显式销毁 
Js代码  收藏代码
  1. Ext.ns("Ext.ux");  
  2. Ext.ux.MyComponent = Ext.extend(Ext.BoxComponent, {  
  3.     tpl : "{text}:<div class='combo'></div>",  
  4.     afterRender : function(){  
  5.         Ext.ux.MyComponent.superclass.afterRender.apply(this, arguments);  
  6.         this.setCombo();  
  7.     },  
  8.     update : function(){  
  9.         Ext.ux.MyComponent.superclass.update.apply(this, arguments);  
  10.         this.setCombo();  
  11.     },  
  12.     setCombo : function(){  
  13.         // this.combo每次更新时都创建了新实例,没有销毁旧的,组件销毁时也未销毁它。  
  14.         this.combo = new Ext.form.ComboBox({  
  15.             store : new Ext.data.ArrayStore({  
  16.                 fields: ['id''mode'],  
  17.                 data :  [  
  18.                     ['1''mode1'],  
  19.                     ['2''mode2']  
  20.                  ]  
  21.             }),  
  22.             valueField : "id",  
  23.             displayField:'mode',  
  24.             mode: 'local',  
  25.             triggerAction: 'all',  
  26.             emptyText:'请选择模式',  
  27.             selectOnFocus:true  
  28.         });  
  29.         this.combo.render(this.el.child("div.combo"));  
  30.     }  
  31. });  
  32.   
  33. // 创建&销毁  
  34. var test = new Ext.ux.MyComponent({  
  35.     data : {  
  36.         text : "请选择模式"  
  37.     },  
  38.     renderTo : Ext.getBody()  
  39. });  
  40.   
  41. test.destroy();  
  42. test = null;  


2. 组件内部泄漏 
这一类例子不太好举,Ext自身几乎不存在这种泄漏了,就以excanvas为例吧: 
Js代码  收藏代码
  1.     /** 
  2.      * Public initializes a canvas element so that it can be used as canvas 
  3.      * element from now on. This is called automatically before the page is 
  4.      * loaded but if you are creating elements using createElement you need to 
  5.      * make sure this is called on the element. 
  6.      * @param {HTMLElement} el The canvas element to initialize. 
  7.      * @return {HTMLElement} the element that was created. 
  8.      */  
  9.     initElement: function(el) {  
  10.       if (!el.getContext) {  
  11.   
  12.         el.getContext = getContext; // 给Dom节点添加了方法属性,造成dom -> js引用  
  13.   
  14.         // Remove fallback content. There is no way to hide text nodes so we  
  15.         // just remove all childNodes. We could hide all elements and remove  
  16.         // text nodes but who really cares about the fallback content.  
  17.         el.innerHTML = '';  
  18.   
  19.         // do not use inline function because that will leak memory  
  20.         el.attachEvent('onpropertychange', onPropertyChange); // 添加了事件,造成dom -> js引用  
  21.         el.attachEvent('onresize', onResize);  
  22.   
  23.         var attrs = el.attributes;  
  24.         if (attrs.width && attrs.width.specified) {  
  25.           // TODO: use runtimeStyle and coordsize  
  26.           // el.getContext().setWidth_(attrs.width.nodeValue);  
  27.           el.style.width = attrs.width.nodeValue + 'px';  
  28.         } else {  
  29.           el.width = el.clientWidth;  
  30.         }  
  31.         if (attrs.height && attrs.height.specified) {  
  32.           // TODO: use runtimeStyle and coordsize  
  33.           // el.getContext().setHeight_(attrs.height.nodeValue);  
  34.           el.style.height = attrs.height.nodeValue + 'px';  
  35.         } else {  
  36.           el.height = el.clientHeight;  
  37.         }  
  38.         //el.getContext().setCoordsize_()  
  39.       }  
  40.       return el;  
  41.     },  
  42. // ......  
  43.   /** 
  44.    * This funtion is assigned to the <canvas> elements as element.getContext(). 
  45.    * @this {HTMLElement} 
  46.    * @return {CanvasRenderingContext2D_} 
  47.    */  
  48.   function getContext() {  
  49.     // 调用getContext,在dom上添加了js对象引用,造成dom -> js引用  
  50.     return this.context_ ||  
  51.         (this.context_ = new CanvasRenderingContext2D_(this));  
  52.   }  
  53. //.....  
  54.   /** 
  55.    * This class implements CanvasRenderingContext2D interface as described by 
  56.    * the WHATWG. 
  57.    * @param {HTMLElement} surfaceElement The element that the 2D context should 
  58.    * be associated with 
  59.    */  
  60.   function CanvasRenderingContext2D_(surfaceElement) {  
  61.     this.m_ = createMatrixIdentity();  
  62.   
  63.     this.mStack_ = [];  
  64.     this.aStack_ = [];  
  65.     this.currentPath_ = [];  
  66.   
  67.     // Canvas context properties  
  68.     this.strokeStyle = '#000';  
  69.     this.fillStyle = '#000';  
  70.   
  71.     this.lineWidth = 1;  
  72.     this.lineJoin = 'miter';  
  73.     this.lineCap = 'butt';  
  74.     this.miterLimit = Z * 1;  
  75.     this.globalAlpha = 1;  
  76.     this.canvas = surfaceElement; // 在JS对象上引用了dom节点,这里算是一个循环引用。  
  77.   
  78.     var el = surfaceElement.ownerDocument.createElement('div');  
  79.     el.style.width =  surfaceElement.clientWidth + 'px';  
  80.     el.style.height = surfaceElement.clientHeight + 'px';  
  81.     el.style.overflow = 'hidden';  
  82.     el.style.position = 'absolute';  
  83.     surfaceElement.appendChild(el);  
  84.   
  85.     this.element_ = el; // 同样,JS对象引用dom节点  
  86.     this.arcScaleX_ = 1;  
  87.     this.arcScaleY_ = 1;  
  88.     this.lineScale_ = 1;  
  89.   }  


3. JS对象泄漏 
在前面已经介绍过Ext.lib.Ajax存在的属性未移除情况,如果想要更详细,可以看这里: 
http://www.sencha.com/forum/showthread.php?103148-OPEN-1099-Tiny-memory-increase-in-Ext.lib.Ajax-so-tiny... 

4. 浏览器bug 
IE Object leaks,参考Ext作法: 
Js代码  收藏代码
  1. if (Ext.isIE) {  
  2.     var t = {};  
  3.     for (eid in EC) {  
  4.         t[eid] = EC[eid];  
  5.     }  
  6.     EC = Ext.elCache = t;  
  7. }  

IE 8 bug,没啥说的,bug描述得很清楚,怎么避免各显神通吧 

PS:有个firefox相关的工具可以查看JS运行状态,非常详细非常专业,但我研究了一天没弄懂 
有会用的人发发心得吧。。。 
http://www.softwareverify.com/javascript/memory/index.html 
点页面右边的eval获取评估版注册码。