一般来说,浏览器的内存泄漏对于 web 应用程序来说并不是什么问题。用户在页面之间切换,每个页面切换都会引起浏览器刷新。即使页面上有内存泄漏,在页面切换后泄漏就解除了。由于泄漏的范围比较小,因此常常被忽视。
但在移动端,内存泄漏就成了一个比较严重的问题。在单面应用中,用户不能刷新页面的,整个应用程序构建在一个页面上。在这种情况下泄漏会被累积,导致内存不被回收。
Javascript中的垃圾回收机制类似于Java/C#这类语言中的回收机制:
一个对象不再被引用,即将被自动回收
具体回收时刻是我们无法控制的,我们只需适当地解除对象的引用,剩下的事,让运行时去做吧。
在我们开发过程中,往往稍不留神,内存泄露了我们可能都不会察觉:
例1:
1 function doFn(){ 2 bigString=new Array(1000).join(new Array(2000).join("XXXXX")); 3 }
不论是你不小心少写了个var,还是觉得这样写很cool,执行doFn(),即退出函数作用域后,bigString会被回收掉么?
不会被回收,bigString现在成为了全局对象window的一个属性,在应用的整个生命周期,window都是一直存在的,所以其属性是不会被销毁的。
例2:
1 var doFn=(function(){ 2 var bigString=new Array(1000).join(new Array(2000).join("XXXXX")); 3 return function(){ 4 console.dir(bigString); 5 } ; 6 })();
上面代码运行后,bigString会被回收么?
不会被回收,闭包里的数据是不会被释放的。
例3:
<intput type=”button” value=”submit” id=”submit” />
1 (function(){ 2 var Zombie=function(){}; 3 var zombie=new Zombie; 4 var print=function(){ 5 console.dir(zombie); 6 }; 7 var node=document.getElementById(‘submit’); 8 node.addEventListener(‘click‘,print,false); 9 })()
运行代码后,事件处理函数执行正常,会打印zombie到控制台,而且这里会发生内存泄露,zombie一直不能被回收。
也许有人会说,离开这个页面,zombie就会被释放。在单页应用中,离开当前页面,实质是,移除页面上body内的所有DOM元素,然后再把新的HTML追加至body的DOM树上。
所以,我们来移除button这个节点:
1 node.parentNode.removeChild(node);
执行之后,我们发现页面上按钮被移除了。现在,zombie对象应该被回收了吧?
我们用chrome浏览器的Heap Profiler来追踪下内存,下面是内存快照:
发现即使移除DOM节点,内存泄露一样存在。当我们在移除元素的同时移除其上的事件时,发现这次zombie被回收了:
1 node.parentNode.removeChild(node); 2 node.removeEventListener(‘click’,print,false);
再次追踪内存,已经没有在Zombie类型的对象遗留在内存中了。
所以,我们得出一个结论:移除一个DOM元素的同时,也要移除元素上面的事件,不然很可能会发生内存泄露,伤你于无形。
说到这里,我就想起了zepto里的移除元素的remove方法:
1 remove: function(){ 2 return this.each(function(){ 3 if(this.parentNode != null) 4 this.parentNode.removeChild(this) 5 }) 6 }
说好的要移除元素上面的事件呢?
另外我们对比下zepto和jQuery里的empty方法:
zepto的empty方法:
1 empty: function(){ 2 return this.each(function(){ this.innerHTML = ‘‘ }) 3 }
jQuery的empty方法:
1 empty: function() { 2 var elem,i = 0; 3 for ( ; (elem = this[i]) != null; i++ ) { 4 if ( elem.nodeType === 1 ) { 5 // Prevent memory leaks 6 jQuery.cleanData( getAll( elem, false ) ); 7 // Remove any remaining nodes 8 elem.textContent = ""; 9 } 10 } 11 return this; 12 }
其API文档里还有这么一句话:
To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.
可见,对于移除DOM元素时,jQuery处理要更为严谨和合理。
在模块化编程时,当我们会用RequireJS来组织代码时,有一种情况是需要注意的:
1 define([],function(){ 2 var obj={ 3 bigString:new Array(1000).join(new Array(2000).join("XXXXX")); 4 //… 5 }; 6 return obj; 7 });
当这个模块作为一个数据源时,在某个地方被加载一次后,即时当前视图已不再需要它,它还会一直保留在内存中。也就是说,返回值为一个对象时,它是不会被释放的。
至于为何这样,你可以想想,我们define一个类后,能通过require来调用它,那么它肯定是在什么地方被保存了起来。所以,我们这个obj在RequireJs内部也会被引用,无法释放。
也许你会问,那你干嘛要返回一个对象呢?我想,有时候,你应该也是这么做的。
另外,不知道大家的Controller层是如何写的,我是让它继承Backbone.Router的:
1 jass.Controller = Backbone.Router.extend({ 2 module: "", 3 name: "", 4 _bindRoutes: function () { 5 if (!this.routes) return; 6 this.routes = _.result(this, ‘routes‘); 7 var route, routes = _.keys(this.routes); 8 var prefix = this.module + "/" + this.name + "/"; 9 while ((route = routes.pop()) != null) { 10 this.route(prefix + route, this.routes[route]); 11 } 12 }, 13 close: function () { 14 // destory 15 // remove actions from history.Handlers ??? 16 this.stopListening(); 17 this.off(); 18 this.trigger(‘destroy‘); 19 } 20 });
这样写也会内存泄露,我们跟踪下router方法:
1 this.route(prefix + route, this.routes[route]); // this -->controller
controller被引用了,它是无法释放的。如果在Controller层上面再引用了Model层表示的数据,泄露将会更加严重。
另外,我这里企图作一些清理工作的close方法根本就没有时机去触发。
我们简化Controller逻辑,它只负责向View层传递Model层的数据时,在多数情况下是会降低泄露的发生。
但是,我们经常会面临这样的问题:
1 多个View之间共享数据;
2 多个Controller之间共享数据;
这时数据应该保存在哪,该何时被清理掉?
为了解决上面的问题,我希望从AngularJS中能得到一些启发,发现它的概念还是挺多的。然后找到AngularJS中依赖注入的模拟代码:
1 var angular = function(){}; 2 3 Object.defineProperty(angular,"module",{ 4 value:function(modulename,args){ 5 var module = function(){ 6 this.args = args; 7 this.factoryObject = {}; 8 this.controllerObject = {}; 9 } 10 module.prototype.factory = function(name,service){ 11 //if service is not a function ... 12 //if service() the result is not a object ... and so on 13 this.factoryObject[name] = service(); 14 } 15 module.prototype.controller = function(name,args){ 16 var _self = this; 17 //init 18 var content = { 19 $scope:{}, 20 scope:function(){ 21 return content.$scope; 22 } 23 // $someOther:{...} 24 } 25 26 var ctrl = args.pop(); 27 console.log(typeof ctrl); 28 var factorys = []; 29 while(service = args.shift()){ 30 if(service in content){ 31 factorys.push(content[service]) 32 }else{ 33 factorys.push(_self.factoryObject[service]) 34 } 35 36 } 37 ctrl.apply(null,factorys); 38 39 _self.controllerObject[name] = function(){ 40 return content; 41 }; 42 } 43 var m = new module(); 44 window[modulename] = m; 45 return m; 46 } 47 })
测试:
1 var hello = angular.module(‘Test‘); 2 3 hello.factory("actionService",function(){ 4 var say = function(){ 5 console.log("hello") 6 } 7 return { 8 "say":say 9 } 10 }) 11 12 hello.controller("doCtrl",[‘$scope‘,"actionService",function($scope,actionService){ 13 $scope.do = function(){ 14 actionService.say(); 15 } 16 }]); 17 18 hello.controllerObject.doCtrl().scope().do()
可见,AngularJS中构造的模块,控制器也是不会被释放的。
在单页应用开发中,更要警惕内存泄露问题,不然它会是性能优化的一个巨大绊脚石。
性能优化,是一个永久的话题,以后有所感悟,再来补充,持续更新!
最近在研究Sencha Touch,期待有趣的发现!
更多有关性能优化的讨论,推荐阅读:
Memory leak patterns in JavaScript
Writing Fast,Memory-Efficient JavaScript
Backbone.js And JavaScript Garbage Collection