`
xpdr14xpdr
  • 浏览: 12083 次
最近访客 更多访客>>
社区版块
存档分类
最新评论

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

 
阅读更多

关于Ext内存泄漏的部分心得
2011年04月10日
  首先说明下,本帖所描述的泄漏是指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-memlea k/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-q uot-IE-object-leaks-quot-What-s-going-on 
  简单点描述,就是IE下,将JS对象的属性delete掉后,属性引用的对象是会解除引用,但属性本身占用的内存不会释放。虽然增长很少,但在极端情况也绝不能无视。 
  2. IE 8 bug 
  http://www.javaeye.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) 弹出窗口未销毁 
  
  
  Ext.ns("Ext.ux");   
  Ext.ux.MyWindow = Ext.extend(Ext.Window, {   
  closeAction : "hide",   
  modal : true,   
  initComponent : function(){   
  this.buttons = [{   
  text : "确定",   
  handler : function(){   
  this.fireEvent("confirm");   
  this.hide();   
  },   
  scope : this  
  }];   
  Ext.ux.MyWindow.superclass.initComponent.apply(this, arguments);   
  }    });   
  Ext.ux.MyComponent = Ext.extend(Ext.Panel, {   
  initComponent : function(){   
  // 它创建出来,没有父容管理,也没有主动销毁   
  this.win = new Ext.ux.MyWindow({   
  width : 300,   
  height : 400,   
  title : "配置xxx",   
  html : "内容xxx"  
  });   
  this.win.on("confirm", this.confirmCfg, this);   
  this.tbar = [{   
  text : "设置xxx",   
  handler : function(){   
  this.win.show();   
  },   
  scope : this  
  }];   
  Ext.ux.MyComponent.superclass.initComponent.apply(this, arguments);   
  },   
  confirmCfg : function(){   
  // do something        }    });   
  var test = new Ext.ux.MyComponent({   
  title : "测试面板",   
  html : "test",   
  renderTo : Ext.getBody()   
  });      
  // 创建&销毁   
  test.destroy();   
  test = null;  
  Ext.ns("Ext.ux"); Ext.ux.MyWindow = Ext.extend(Ext.Window, { closeAction : "hide", modal : true, initComponent : function(){ this.buttons = [{ text : "确定", handler : function(){ this.fireEvent("confirm"); this.hide(); }, scope : this }]; Ext.ux.MyWindow.superclass.initComponent.apply(thi s, arguments); } }); Ext.ux.MyComponent = Ext.extend(Ext.Panel, { initComponent : function(){ // 它创建出来,没有父容管理,也没有主动销毁 this.win = new Ext.ux.MyWindow({ width : 300, height : 400, title : "配置xxx", html : "内容xxx" }); this.win.on("confirm", this.confirmCfg, this); this.tbar = [{ text : "设置xxx", handler : function(){ this.win.show(); }, scope : this }]; Ext.ux.MyComponent.superclass.initComponent.apply( this, arguments); }, confirmCfg : function(){ // do something } }); var test = new Ext.ux.MyComponent({ title : "测试面板", html : "test", renderTo : Ext.getBody() }); // 创建&销毁 test.destroy(); test = null; 
  b) 创建出来未使用,也未销毁 
  
  
  Ext.ns("Ext.ux");   
  Ext.ux.MyComponent = Ext.extend(Ext.Panel, {   
  initComponent : function(){   
  this.btnA = new Ext.Button({   
  text : "模式A",   
  handler : function(){}   
  });   
  this.btnB = new Ext.Button({   
  text : "模式B",   
  handler : function(){}   
  });   
  // btnA与btnB必有一个未纳入Toolbar管理,也未主动销毁   
  this.tbar = [this.mode==="a" ? this.btnA : this.btnB];   
  Ext.ux.MyComponent.superclass.initComponent.apply(this, arguments);   
  }    });      
  // 创建&销毁   
  var test = new Ext.ux.MyComponent({   
  title : "测试面板",   
  html : "test",   
  renderTo : Ext.getBody()   
  });      
  test.destroy();   
  test = null;  
  Ext.ns("Ext.ux"); Ext.ux.MyComponent = Ext.extend(Ext.Panel, { initComponent : function(){ this.btnA = new Ext.Button({ text : "模式A", handler : function(){} }); this.btnB = new Ext.Button({ text : "模式B", handler : function(){} }); // btnA与btnB必有一个未纳入Toolbar管理,也未主动销毁 this.tbar = [this.mode==="a" ? this.btnA : this.btnB]; Ext.ux.MyComponent.superclass.initComponent.apply( this, arguments); } }); // 创建&销毁 var test = new Ext.ux.MyComponent({ title : "测试面板", html : "test", renderTo : Ext.getBody() }); test.destroy(); test = null; 
  c) 直接render到dom子节点中,未显式销毁 
  
  
  Ext.ns("Ext.ux");   
  Ext.ux.MyComponent = Ext.extend(Ext.BoxComponent, {   
  tpl : "{text}:",   
  afterRender : function(){   
  Ext.ux.MyComponent.superclass.afterRender.apply(this, arguments);   
  this.setCombo();   
  },   
  update : function(){   
  Ext.ux.MyComponent.superclass.update.apply(this, arguments);   
  this.setCombo();   
  },   
  setCombo : function(){   
  // this.combo每次更新时都创建了新实例,没有销毁旧的,组件销毁时也未销毁它。   
  this.combo = new Ext.form.ComboBox({   
  store : new Ext.data.ArrayStore({   
  fields: ['id', 'mode'],   
  data :  [   
  ['1', 'mode1'],   
  ['2', 'mode2']   
  ]   
  }),   
  valueField : "id",   
  displayField:'mode',   
  mode: 'local',   
  triggerAction: 'all',   
  emptyText:'请选择模式',   
  selectOnFocus:true  
  });   
  this.combo.render(this.el.child("div.combo"));   
  }    });      
  // 创建&销毁   
  var test = new Ext.ux.MyComponent({   
  data : {   
  text : "请选择模式"  
  },   
  renderTo : Ext.getBody()   
  });      
  test.destroy();   
  test = null;  
  Ext.ns("Ext.ux"); Ext.ux.MyComponent = Ext.extend(Ext.BoxComponent, { tpl : "{text}:", afterRender : function(){ Ext.ux.MyComponent.superclass.afterRender.apply(th is, arguments); this.setCombo(); }, update : function(){ Ext.ux.MyComponent.superclass.update.apply(this, arguments); this.setCombo(); }, setCombo : function(){ // this.combo每次更新时都创建了新实例,没有销毁旧的,组件销毁时也未销毁它。 this.combo = new Ext.form.ComboBox({ store : new Ext.data.ArrayStore({ fields: ['id', 'mode'], data : [ ['1', 'mode1'], ['2', 'mode2'] ] }), valueField : "id", displayField:'mode', mode: 'local', triggerAction: 'all', emptyText:'请选择模式', selectOnFocus:true }); this.combo.render(this.el.child("div.combo")); } }); // 创建&销毁 var test = new Ext.ux.MyComponent({ data : { text : "请选择模式" }, renderTo : Ext.getBody() }); test.destroy(); test = null; 
  2. 组件内部泄漏 
  这一类例子不太好举,Ext自身几乎不存在这种泄漏了,就以excanvas为例吧: 
  
  
  /**  
  * Public initializes a canvas element so that it can be used as canvas  
  * element from now on. This is called automatically before the page is  
  * loaded but if you are creating elements using createElement you need to  
  * make sure this is called on the element.  
  * @param {HTMLElement} el The canvas element to initialize.  
  * @return {HTMLElement} the element that was created.  
  */  
  initElement: function(el) {   
  if (!el.getContext) {   
  el.getContext = getContext; // 给Dom节点添加了方法属性,造成dom -> js引用   
  // Remove fallback content. There is no way to hide text nodes so we   
  // just remove all childNodes. We could hide all elements and remove   
  // text nodes but who really cares about the fallback content.   
  el.innerHTML = '';      
  // do not use inline function because that will leak memory   
  el.attachEvent('onpropertychange', onPropertyChange); // 添加了事件,造成dom -> js引用   
  el.attachEvent('onresize', onResize);   
  var attrs = el.attributes;   
  if (attrs.width && attrs.width.specified) {   
  // TODO: use runtimeStyle and coordsize   
  // el.getContext().setWidth_(attrs.width.nodeValue);   
  el.style.width = attrs.width.nodeValue + 'px';   
  } else {   
  el.width = el.clientWidth;   
  }   
  if (attrs.height && attrs.height.specified) {   
  // TODO: use runtimeStyle and coordsize   
  // el.getContext().setHeight_(attrs.height.nodeValue) ;   
  el.style.height = attrs.height.nodeValue + 'px';   
  } else {   
  el.height = el.clientHeight;   
  }   
  //el.getContext().setCoordsize_()   
  }   
  return el;   
  },   
  // ......      /**  
  * This funtion is assigned to the  elements as element.getContext().  
  * @this {HTMLElement}  
  * @return {CanvasRenderingContext2D_}  
  */  
  function getContext() {   
  // 调用getContext,在dom上添加了js对象引用,造成dom -> js引用   
  return this.context_ ||   
  (this.context_ = new CanvasRenderingContext2D_(this));   
  }   
  //.....      /**  
  * This class implements CanvasRenderingContext2D interface as described by  
  * the WHATWG.  
  * @param {HTMLElement} surfaceElement The element that the 2D context should  
  * be associated with      */  
  function CanvasRenderingContext2D_(surfaceElement) {   
  this.m_ = createMatrixIdentity();   
  this.mStack_ = [];   
  this.aStack_ = [];   
  this.currentPath_ = [];   
  // Canvas context properties   
  this.strokeStyle = '#000';   
  this.fillStyle = '#000';      
  this.lineWidth = 1;   
  this.lineJoin = 'miter';   
  this.lineCap = 'butt';   
  this.miterLimit = Z * 1;   
  this.globalAlpha = 1;   
  this.canvas = surfaceElement; // 在JS对象上引用了dom节点,这里算是一个循环引用。   
  var el = surfaceElement.ownerDocument.createElement('div');   
  el.style.width =  surfaceElement.clientWidth + 'px';   
  el.style.height = surfaceElement.clientHeight + 'px';   
  el.style.overflow = 'hidden';   
  el.style.position = 'absolute';   
  surfaceElement.appendChild(el);   
  this.element_ = el; // 同样,JS对象引用dom节点   
  this.arcScaleX_ = 1;   
  this.arcScaleY_ = 1;   
  this.lineScale_ = 1;   
  }  
  /** * Public initializes a canvas element so that it can be used as canvas * element from now on. This is called automatically before the page is * loaded but if you are creating elements using createElement you need to * make sure this is called on the element. * @param {HTMLElement} el The canvas element to initialize. * @return {HTMLElement} the element that was created. */ initElement: function(el) { if (!el.getContext) { el.getContext = getContext; // 给Dom节点添加了方法属性,造成dom -> js引用 // Remove fallback content. There is no way to hide text nodes so we // just remove all childNodes. We could hide all elements and remove // text nodes but who really cares about the fallback content. el.innerHTML = ''; // do not use inline function because that will leak memory el.attachEvent('onpropertychange', onPropertyChange); // 添加了事件,造成dom -> js引用 el.attachEvent('onresize', onResize); var attrs = el.attributes; if (attrs.width && attrs.width.specified) { // TODO: use runtimeStyle and coordsize // el.getContext().setWidth_(attrs.width.nodeValue); el.style.width = attrs.width.nodeValue + 'px'; } else { el.width = el.clientWidth; } if (attrs.height && attrs.height.specified) { // TODO: use runtimeStyle and coordsize // el.getContext().setHeight_(attrs.height.nodeValue) ; el.style.height = attrs.height.nodeValue + 'px'; } else { el.height = el.clientHeight; } //el.getContext().setCoordsize_() } return el; }, // ...... /** * This funtion is assigned to the  elements as element.getContext(). * @this {HTMLElement} * @return {CanvasRenderingContext2D_} */ function getContext() { // 调用getContext,在dom上添加了js对象引用,造成dom -> js引用 return this.context_ || (this.context_ = new CanvasRenderingContext2D_(this)); } //..... /** * This class implements CanvasRenderingContext2D interface as described by * the WHATWG. * @param {HTMLElement} surfaceElement The element that the 2D context should * be associated with */ function CanvasRenderingContext2D_(surfaceElement) { this.m_ = createMatrixIdentity(); this.mStack_ = []; this.aStack_ = []; this.currentPath_ = []; // Canvas context properties this.strokeStyle = '#000'; this.fillStyle = '#000'; this.lineWidth = 1; this.lineJoin = 'miter'; this.lineCap = 'butt'; this.miterLimit = Z * 1; this.globalAlpha = 1; this.canvas = surfaceElement; // 在JS对象上引用了dom节点,这里算是一个循环引用。 var el = surfaceElement.ownerDocument.createElement('div'); el.style.width = surfaceElement.clientWidth + 'px'; el.style.height = surfaceElement.clientHeight + 'px'; el.style.overflow = 'hidden'; el.style.position = 'absolute'; surfaceElement.appendChild(el); this.element_ = el; // 同样,JS对象引用dom节点 this.arcScaleX_ = 1; this.arcScaleY_ = 1; this.lineScale_ = 1; } 
  3. JS对象泄漏 
  在前面已经介绍过Ext.lib.Ajax存在的属性未移除情况,如果想要更详细,可以看这里: 
  http://www.sencha.com/forum/showthread.php?103148- OPEN-1099-Tiny-memory-increase-in-Ext.lib.Ajax-so-t iny... 
  4. 浏览器bug 
  IE Object leaks,参考Ext作法: 
  
  
  IE 8 bug,没啥说的,bug描述得很清楚,怎么避免各显神通吧 
  PS:有个firefox相关的工具可以查看JS运行状态,非常详细非常专业,但我研究了一天没弄懂 
  有会用的人发发心得吧。。。 
  http://www.softwareverify.com/javascript/memory/in dex.html 
  点页面右边的eval获取评估版注册码。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics