移动web app:内存溢出与优化(附项目分析)
主要的问题:
heap过大,内存低性能差的机子上引起奔溃,直接退出
关于web app的优化,不仅仅只是js方面,包括HTML布局嵌套,CSS的属性使用,数据的读取,还有浏览器的重排与回流之类的这里就不讨论了,
本章涉及的是脚本代码引发的性能问题,更进一步说就是闭包带来的内存泄露
关于性能:
首先我不得不承认一个事实,移动端的性能跟PC端,那完全不是一回事
比如用innerHTML绘制大段的HTML结构,之后同步获取生成HTML中的ID节点,结果不存在
这种问题在单页面模拟多页面,动态创建DOM的时候,尤为明显
var element = $('<div id = "aaron">...填充大量结构...</div>'); $(root).html(element) $('#aaron') //为空
- 这个是很简单的一段代码,按照常规的认识,JS主线程与GUI的渲染线程是互斥的,所以在执行JS的时候,GUI应该就是挂起的, 同理执行GUI的时候亦然, 因为JS可以动态操作节点,所以如果我们在GUI绘制的时候做操作明显就会打乱了,所以互斥的解释也合理
- 但是实际上这样并不能直接获取到$('#aaron'),PC上基本不会出现,常规的办法都是加setTimeout
- 实际上由于setTimeout的机制,所以也是不准确的,当然我已经有一个比较完美的方式解决
关于JavaScript内存管理:
原文:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management
JavaScript会给开发者一个错觉:可以不用考虑内存管理
现代浏览器已经够聪明了,从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
所以引用计数收集与循环引用之类的都不再是问题了,过去导致内存泄漏的许多经典模式在现代浏览器中以不再导致泄漏内存。
但是,如今有一种不同的趋势影响着内存泄漏。许多人正设计用于在没有硬页面刷新的单页中运行的 Web 应用程序。在那样的单页中,从应用程序的一个状态到另一个状态时,很容易保留不再需要或不相关的内存。
典型的就是: 单页面模拟多页面的行为
简单的内存管理测试
视图:
HTML结构:
<button id="start_button">Start</button> <button id="destroy_button">Destroy</button>
脚本代码:
var Leaker = function() { this.name = 'aaron' }; $("#start_button").click(function(){ leak = new Leaker(); }); $("#destroy_button").click(function(){ leak = null; });
点击Start 产生一个对象leak = new Leaker();
点击Destroy 销毁这个对象 观察下内存中变化(工具后面会提到)
点击Start ,产生一个对象
点击Destroy,对象销毁
那么这个图很形象的说明了,#Delata释放了一个实例,就是内存被回收了
如果不做任何处理,那么这个对象leak始终最存在整个生命周期内(全局上下文的情况)
如果leak = null,内存确实是由浏览器GC 自动给回收了
闭包引起的内存泄漏:
代码:内部增加了一个定时器,递归调用
var count = 0; var Leaker = function(){}; Leaker.prototype = { init:function(){ this._interval = null; this.start(); }, start: function(){ var self = this; //递归调用自身 this._interval = setInterval(function(){ self.onInterval(); }, 100); }, destroy: function(){ if(this._interval !== null){ clearInterval(this._interval); } }, onInterval: function(){ count++; console.log("Interval",count); } };
从样的观察
我在按了销毁,leak = null了
可见代码依然还在走,可见此时内存绝对的溢出了,也就是失控了
但是监视器显示该对象回收了
那么这个问题就很明显了,通过leak = null 销毁的只是引用,内部如果还存在引用的话,这个heap是不会被回收的
此时这个内存我们已经管理不到了,会一直递归下去
要解决只能在销毁的时候先停止定时器了
由此可见,引用不仅仅只是外部的, 内部同样存在这样的问题,当然引用类型的机制本来就是这样的
所以在日常的代码编写方面,JS的坑确实不少,接下来看看我项目中的大坑吧!!!
应用截图:
内存使用检测:
Eclipse
Eclipse不熟悉的路过,我们还是回到前端的角度去处理
使用Chrome DevTools的Timeline和Profiles提高Web应用程序的性能
具体的使用就不介绍了,大家接着看
抓怕的heap快照,实时反馈的信息
系统的闭包数
加上JQuery
项目中的
视图解释
列字段解释:
Constructor -- 构造器
Distance -- 估计是对象到根的引用层级距离
Objects Count -- 给出了当前有多少个该类的对象
Shallow Size -- 对象所占内存(不包含内部引用的其它对象所占的内存)(单位:字节)
Retained Size -- 对象所占总内存(包含内部引用的其它对象所占的内存)(单位:字节)
小伙伴都吓呆了
项目中除去系统与一些插件的,至少有上千个闭包
分析堆快照
Object's retaining tree视图显示出了该对象被哪些对象引用了,以及这个引用的名称
关于XUTUTIL.Event类
XUTUTIL.Event是一个构函数函数,主要就是一个订阅/发布模式
那么这个图我的理解就是通过XUTUTIL构造生成的的对象都应该是放到这个里面,所以
根据分析图显示,这个类有208个对象,被实例了208次,也就是说存在这么多订阅者了
XUTUTIL部分源码(观察者模式)
XUTUTIL.Event
如图me.events[eventName]标记,是数组保存了观察对象了
点击图中的黑色实心圆圈按钮,即可得到第二个内存快照:
点击图中的“Summary”,可弹出一个列表,选择“Comparison”选项,然后选择对比第一个,结果如下图:
这个视图列出了当前视图与上一个视图的对象差异。
列名字段解释:
# New -- 新建了多少个对象
# Deleted -- 回收了多少个对象
# Delta -- 对象变化值,即新建的对象个数减去回收了的对象个数
ALLOC -- 变化的内存大小(字节)注意Delta字段,尤其是值大于0的对象
很明显翻一页就创建大量的观察对象
*注:因为是单页面应用,动态多页面的翻页算法,比如当前是从第2页到第3页,其实是预先创建第4页面,销毁第1页,保留234页,所以这个+14,不是这样算的
但是第一个很明显的问题就出来,为什么要动态创建这么多的观察对象,找到代码来源
找到问题了
注册了大量的观察者模式
销毁的代码,没有处理注销观察者事件
啪啪啪啪。。。。一阵修改之后
翻页的时候不处理了
在进入页面初始化的时候208变成18个了。。。在看看内存占用。。。45016---3240
PC上的消耗,在移动端就会被放大的,所以不要放过过任何一个可优化的地方
修改前
修改后
因为这个案例比较明显,还有的问题,要靠自己慢慢去分析引用情况了
那么很明显了:观察者模式引起的内存泄漏
需要观察者模式(Observer)来解藕一些模块,但如果使用不当,也会带来内存泄漏的问题。
排查这类型的内存泄漏问题,主要重点关注被引用的对象类型是闭包(closure)和数组Array的对象。
1.如果能避免观察模式的使用,就尽量避免,
2.避免不了一定要记得清理
总结出以下几种常见的情况:
1.闭包上下文绑定后没有释放;
2.观察者模式在添加通知后,没有及时清理掉;
3.定时器的处理函数没有及时释放,没有调用clearInterval方法;
4.视图层有些控件重复添加,没有移除。
大型应用,优化是任重道远的,本文只是希望起到一个抛砖引玉的作用。。。。。。。。。。。。。。
各位道友,你们怎么看?