前几天项目组一个app内嵌h5页面在长时间使用后(半个小时),就会出现页面卡顿的状况。页面内元素的点击事件须要等待5秒多才能进行响应,在排除了原生app形成问题的状况以后我基本判断应该是前端本身的问题——内存泄漏。html
前几天组内一个前端妹子火急火燎的跑了过来讲,她所负责的一个app内嵌h5项目首页在持续使用了一段时间以后会变得异常卡顿。一个简单的页面跳转或者点击行为都会卡5秒多才会进行响应。前端
最终通过层层排查确认是内存泄漏形成的问题,出现的bug则是由于对于js中的this指向掌握不充分致使this指向丢失,出现问题的代码出如今这里:node
//h5调用原生 经过iframe urlScheme的方法
callNativeByUrlScheme(eventName: string, data: any) {
const url = this.getUrlScheme(eventName, data);
const iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(iframe.remove, 100);
}
复制代码
在项目封装的jsBridge中,前端通知原生的交互方式是经过url scheme
的形式来实现的(h5建立一个隐藏的iframe标签,原生经过劫持url的形式来进行通讯处理)。这是项目中建立iframe所封装的一个函数,不知道你是否能发生发现这个隐晦的bug呢?android
在通过简单的排查以后,在发现v-console
也是卡顿56秒以后基本确认是由于内存泄漏问题的形成的卡顿问题了。可是由于咱们项目首页的逻辑异常沉重,涉及到大量的原生交互事件以及回调,并且也有大量的定时器进行轮询获取数据进行处理。**如何快速的定位是否内存泄漏形成的问题?如何快速的定位到是那一行代码形成的内存泄漏?**就成了如今的难题之一。git
问题的解决办法:获取到当前项目的堆栈快照加以分析处理。在nodejs中咱们能够经过node-heapdump
这个库来获取nodejs的堆栈快照文件,在浏览器中咱们能够经过chrome的开发者工具memory
模块来获取当前页面的内存快照信息。github
接下来我会大体讲述是如何一步一步解决这个内存泄漏的问题的。web
由于须要调试的h5页面是app里内嵌的webview页面,依赖于native提供的能力,这种状况在pc上很难模拟因此须要使用真机调试方式。若是是比较独立的页面的话能够放到chrome浏览器中打开直接进行调试。面试
由于当前项目是内嵌在app里面的,涉及到一些加密以及鉴权的流程并且为了更好的模拟实际环境,咱们决定放弃经过chrome打开页面而是直接使用真机调试的方式来解决bug。chrome
由于Android webview
已经帮咱们开启了调试模式,为了方便直接使用android手机 + 电脑chrome浏览器
的组合来方便真机调试。具体的调试方法能够参考阮一峰老师的这篇博客api
按上面的步骤链接完手机以后,为了确认是否内存泄漏的问题咱们须要打印内存快照
::
Memory
模块。Head snapshot
模式而且点击Take heap snapshot
按钮开始打印第一份快照,这时候获得了第一份快照Snapshot1
。Collect garbage
手动执行垃圾回收策略,再次点击Take heap snapshot
按钮打印第二分内存快照获得两份快照以后,咱们发现第二份快照的内存占用明显比第一份增长了不少,在手动执行了垃圾回收后占用的内存依然没被释放。至此确认:确实是内存泄漏问题形成的页面卡顿现象。
为了百分百确认,咱们打了第三份快照。确认内存占用依然明显增长而且通过垃圾回收没有释放,至此问题停留在了——如何经过内存快照定位到具体的形成内存泄漏的代码。
获取到了两分内存快照以后,chrome的memory
模块提供了不一样的快照类型查阅模式:
在这里,咱们使用Comparison
模式,以快照2为基准对比快照1的内存变化差别:
在进行对比中,咱们发现内存增长最明显的两块地方system
以及closure
。
system
栏目看不出什么有效的信息,所以咱们展开closure
栏目,咱们不难发现内存中增长了许多document
元素节点,可是通常来讲一个html页面通常来讲应该只会出现一个document
节点。
忽然间临机一动,经过iframe
标签能够将另外一个HTML页面嵌入到当前页面中,这样子就会在iframe上下文中也会建立对应的document节点。而后又联想到在和原生端进行通讯的过程当中咱们是经过建立iframe的形式来进行通讯的。猜想多是用于原生通讯的iframe标签在建立完成以后没有被清除仍然停留在dom树中致使没法被垃圾回收。
为了验证上面的猜测,咱们只须要在chrome的控制台中获取dom树中的iframe标签到底有多少个就能够了:
$$('iframe')
复制代码
全局搜索找到相关的建立iframe
的代码,最终定位到这一个函数上:
callNativeByUrlScheme(eventName: string, data: any) {
const url = this.getUrlScheme(eventName, data);
const iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(iframe.remove, 100);
}
复制代码
从上可知,内存泄漏的缘由出现于:用于跟原生通讯交互而建立的iframe标签并无从dom树中删除致使没法被v8进行垃圾回收。
回到这行代码上,iframe
的节点上是经过iframe.remove
方法进行删除的。这个方法在MDN上确认是没问题的:ChildNode.remove()
方法,把对象从它所属的 DOM 树中删除。
callNativeByUrlScheme(eventName: string, data: any) {
const url = this.getUrlScheme(eventName, data);
const iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(iframe.remove, 100);
}
复制代码
确认api的使用是确实没有问题以后,咱们把焦点汇集在了setTimeout
上,是否是由于异步的问题致使remove
方法调用失败, 因而乎咱们尝试使用同步的方式来调用remove
方法。
// 改动代码
document.body.appendChild(iframe);
//setTimeout(iframe.remove, 100);
// 改用同步的方式
iframe.remove()
复制代码
改用同步以后从新打包。。。问题解决了!也没有再出现内存泄漏的问题,dom树中建立的iframe
节点每次建立成功也被及时的清理了。WTF???? 难道ChildNode.remove()
不方法不支持异步调用?
思考了一段时间,而后把目光聚焦在了变化的两端代码上发现了除了异步以外的另一个差一点:setTimeout中传入的是remove的函数引用,在setTimeout中本质上是函数自调用,而同步的写法则是对象语法调用
// 函数自调用
setTimeout(iframe.remove, 100);
// setTimeout大体模拟实现
//async function setTimeout(fn,delay) {
// await delay()
// fn &&fn()
//}
// 对象语法调用
iframe.remove()
复制代码
熟悉this指向的同窗们会清楚,在js语法中this的指向大概能够分为四种:
构造函数绑定(new)
:绑定到新建立的对象,注意:显示return函数或对象,返回值不是新建立的对象,而是显式返回的函数或对象。显示绑定(call,apply,bind)
:严格模式下,绑定到指定的第一个参数。非严格模式下,null和undefined,指向全局对象(浏览器中是window),其他值指向被new Object()包装的对象。隐性绑定(对象上的函数调用)
: 绑定到那个对象。默认绑定(函数自调用)
:在严格模式下绑定到 undefined,不然绑定到全局对象。setTimeout中传入的是remove的函数引用,在setTimeout中本质上是函数自调用,因此remove方法中的this指向的是undefined或者全局对象
iframe.remove()是对象调用因此this指向的是iframe元素自己
为此我找到MDN上的ChildNode.remove()
profilly代码,确认了内部实现中确实使用到了this变量来指向元素节点:
//https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md
(function (arr) {
arr.forEach(function (item) {
if (item.hasOwnProperty('remove')) {
return;
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
// 注意这里
value: function remove() {
this.parentNode.removeChild(this);
}
});
});
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
复制代码
最终咱们肯定了内存泄漏发生的真正缘由:this指向丢失形成建立的iframe元素没法从dom树中删除致使的内存泄漏。
在确认了问题的缘由以后,解决问题的办法就容易多了:
iframe.remove()
或者setTimeout(() => iframe.remove(), 100);
setTimeout(iframe.remove.bind(iframe), 100);
至此,一次从发现问题——查找问题——解决问题的链路式解决内存泄漏的旅程结束了。归根到底,出现这个bug的主要缘由有两个:
内存泄漏出现不可怕,如何解决如何定位到问题的缘由才可怕。感谢各位小伙伴们的查阅,但愿这篇文章能给遇到内存泄漏困扰的同窗们提供帮助啦!