背景
经过上一次排查了内存泄露之后,内存泄漏的问题好了很多。但是最近有用户反馈还是会有内存告警的问题,所以有可能会存在内存泄露,经过排查,确实存在内存泄露,但是内存泄露的原因不明,并非是在延时调用页面实例对象。
如何确定问题
使用方法
二分debugger + 在内存中查找对象最短引用路径
具体操作
- 找到页面开始调用的生命周期(onload),将生命周期内部调用的方法全部注释。查看页面实例是否会被回收。
- 如果没被回收,则注释下一个生命周期执行的代码。
- 如果页面实例被回收,则将生命周期内部调用的方法注释一半。
- 重复123 直到最小的一行代码。
通过二分debugger法,查找到是调用了this.setData({a: “xxxxx”}),才会导致内存泄露的。
所以我猜测是因为有组件没被回收,才导致的页面实例没被回收,因为组件会存在对页面实例的引用。
查找setData对应的属性值,找到影响的组件,看下是否有监听页面属性的操作,如果有监听的操作,查看监听操作内部做了什么逻辑,然后再通过二分debugger,找到真正影响的代码。比如下图的示例代码中的:
this.cData[onlyKey] = new Move(this)
按道理,页面被销毁,自己的cData 属性也会被销毁才对,就算存在循环应用也没什么问题。在不了解底层逻辑情况下,我通过查找组件实例的最短引用路径,发现组件实例存在于底层逻辑的闭包函数中。如下实例的:
// 处理原有的自定义属性,等组件创建再把自定义属性放到组件上
function handleCompObj(compObj){
// xxxxxxxx
}
最终发现底层逻辑有对于针对自定义属性做缓存,相当于全部组件实例都共用一份自定义属性。
下面是示例代码:
// components/comp.js
console.log("comp");
class Move{
constructor(config){
this.config = config
}
}
let numId = 0
// 组件对象类
class Comp {
cData = {}
constructor(){
}
created = function(){
this.key = "comp" + numId++
// 创建唯一key
const onlyKey = "onlyKey" + new Date()
// 用组件自定义属性存储 一个带有组件实例的对象,存在循环引用
this.cData[onlyKey] = new Move(this)
console.log("this.cData", this.cData)
}
/**
* 组件的初始数据
*/
data = {
}
}
const compObj = new Comp()
console.log("compObj", compObj);
// 处理原有的自定义属性,等组件创建再把自定义属性放到组件上
function handleCompObj(compObj){
// 自定义属性
const customPropties = {
}
const compPropertyKeys= [
"externalClasses",
"behaviors",
"relations",
"data",
"properties",
"methods",
"lifetimes",
"pageLifetimes",
"definitionFilter",
"options",
"setData",
"observers",
]
Object.keys(compObj).map((key)=>{
// 固定属性排除
if(compPropertyKeys.includes(key)){return}
customPropties[key] = compObj[key]
})
// 原有组件创建生命周期
const originCreated = compObj.created
function newCreated(){
// 组件创建的时候,将自定义属性赋给组件实例对象
Object.assign(this, customPropties)
// 运行原有的组件逻辑
originCreated.call(this)
}
compObj.created = newCreated
return compObj
}
// 处理组件类的实例,给组件增加自定义属性
const newCompObj = handleCompObj(compObj)
// 注册组件
Component(newCompObj)
解决办法
在给组件增加自定义属性的时候,如果是对象,则需要深复制。