背景
用户经常出现闪退的情况,并提示内存不足。根据用户操作场景,猜测页面存在内存泄露。
内存泄露是什么?
内存泄露是程序运行过程中产生的内存变量会一直存在,不会被垃圾回收机制检测到,导致一直不会被销毁,内存占用会越来越大。
比如说:
我们在运行小程序的时候会产生一个页面,小程序会给这个页面创建一个实例,当这个页面销毁的时候,这个实例应该会被销毁。
但是如果我们有个定时器(setInterval),定时器里面对这个页面实例存在引用,那这个页面实例就不会被销毁,因为有被用到。
当存在内存泄露的情况,用户长期使用我们的小程序会导致小程序占用的内存越来越大,最后会导致小程序闪退(被微信强制销毁)
排查内存泄露用到的工具-weakSet
先简单描述一下weakSet,让大家有个简单的认识,详细需要去看下文档。
weakSet 是一个可以存储唯一变量的集合,和Set不一样的是,weakSet存储的变量都是弱引用,就是不会影响垃圾回收,如果存储的变量被回收了,在这个集合里面就找不到。
所以weakSet不能被遍历,也没有长度的概念。但是我们可以通过控制台打印weakset的指向,知道里面有多少个元素。如下图:
通过展开,我们可以知道里面是哪个页面的实例,但是我们在控制台展开就意味着我们对这个页面实例存在引用,则无法被垃圾回收。所以在执行垃圾回收之前需要清空控制台的输出。
如何确定页面是否存在内存泄露
如果页面存在内存泄露则不会销毁页面实例。我们只需要判断页面实例有没有被销毁即可。
我们在一开始就把页面实例加到weakSet里面,当执行多次跳转页面之后,会存在多个页面实例,最后回到首页,触发小程序的垃圾回收。
如果不存在内存泄露,那weakSet集合里面只会存在两个页面实例(当前页面实例+返回回来的页面实例),比如下图的页面A和页面B。
如果存在内存泄露,那weakSet集合里面会存在多个页面实例(当前页面实例+存在内存泄露的页面实例*n),比如下图的页面A、页面B、页面C和页面D.
具体如下图:
如何主动触发小程序的垃圾回收
小程序没有api可以让我们触发小程序的垃圾回收,我们目前可以通过开发者工具的performance面板或memory的垃圾回收(collect garbage 垃圾桶图标)按钮。
触发垃圾回收之后的结果如图:
这个需要手动触发才可以,我们在测试的时候需要手动点击,无法自动触发,所以我们想了个方案自动触发垃圾回收。
通过给内存塞很多数据,然后将这些数据标为无用的,当内存达到500m左右小程序就会触发垃圾回收。这个办法会导致我们内存一段时间激增,建议尽量在跳转页面的时候不要开启,只有在最后页面跳转回首页才进行。
// 主动触发垃圾回收
setInterval(()=>{
if(!global.startGC){
return
}
let a = []
for (let i = 0; i < 10000000; i++) {
a.push({ name: "pling", age: Math.random() * 10000 })
}
console.log("length", a.length)
a = []
}, 3000)
如何定位页面内存泄露的原因
内存泄露的情况举例:
global.list = []
Page({
// ...
onLoad() {
// ... 省略其他代码
// 将页面实例挂载到全局对象,如没有清理,则页面实例会一直不被销毁
global.list.push(this)
// 存在Interval计时器,则会一直存在对页面实例的引用
setInterval(() => {
console.log("test", this.data)
}, 5000);
// 通过settimeout的循环调用,实现了类似于interval的效果也会导致页面实例不会被销毁
this.testLoop()
const that = this
function test(){
console.log(that.data)
}
// 将内部函数挂载到全局变量,则会导致函数的作用域链都会存在引用,不会被销毁
global.logThis = test
},
testLoop(){
setTimeout(() => {
this.testLoop()
}, 10000);
}
})
通过上面我们可以知道一般会有上面四种情况导致内存泄露。
- 将对象挂载到全局对象上,页面写在没有清楚
- 通过暴露内部函数给外部对象,导致存在作用域的引用,页面卸载没有清楚内部函数
- 存在定时执行的函数存在对页面实例的引用,页面销毁没有清除定时器
- 通过延时执行的函数循环调用,并存在对页面实例的引用,页面销毁没有停止调用。
第一第二种情况会比较少出现,目前暂时还没考虑如何去排查。
第三第四种都会对页面实例存在调用,所以我们在页面实例销毁之后对页面实例上的属性进行监听,如果一直存在调用则会有问题。
具体实现代码:
// 检查页面卸载后对页面实例调用
Page({
data: {
test: "111"
},
onLoad() {
global.pageSet.add(this)
setInterval(() => {
console.log("test", this.__wxExparserNodeId__, this.data.test)
}, 5000);
},
// ....
onUnload(){
console.log("unload");
const that = this
// 获得可以枚举的属性列表
const keys = Object.keys(that)
// 加入data 因为data 不是可以枚举的属性
keys.push("data")
console.log(keys);
keys.map(key=>{
// 获得原本的属性描述
const property = Object.getOwnPropertyDescriptor(that, key)
// 保留原有的值
const origin = that[key];
// 获得属性的get方法 有可能没有
const getter = property && property.get
// 获得属性的set方法 有可能没有
const setter = property && property.set
const isFunction = typeof origin === "function"
// 如果是function的话 需要绑定this
if(isFunction){
origin.bind(that)
}
const newThis = {}
// 拦截属性
Object.defineProperty(that, key, {
get: function(){
console.log(`调用了this.${key}的getter`);
// 有getter 调用getter
if(getter){
return getter.call(that)
}
return newThis[key] || origin
},
set: function(newVal){
console.log(`调用了this.${key}的setter`);
if(setter){
return setter.call(that, newVal)
}
newThis[key] = newVal
}
})
})
}
})
测试demo
我们在自己项目里面测试会比较麻烦,一开始可能会有干扰,所以我这边弄了个代码片段,先校验一下这个方法是否可行,如果可行再加到自己的项目里面。
请问我的小程序有个页面是用来拍照上传图片,每拍一张图片就会先进行压缩,压缩到大概400k之后会回显缩略图到本页面; 小程序在部分手机会在这个页面闪退; 有的手机拍80张照片也没问题,有的手机拍个10来张就闪退(均为iphone11/13之类),请问这种问题如何定位呢? 因为此功能之前也没见报问题,5月份开始有这种问题,是不是微信基础库升级导致的? 或者开发者有什么手段去定位?
如果没有这个提示,直接闪退的情况,通常都是触发了某个异常bug,个人开发者比较难定位,需要寻求微信开发者的协助。
如果是安卓,可以抓安卓的运行日志,分析问题。
建议处理方式:
1. 录制一个复现视频
2. 通过微信上传一下微信日志,记录微信号,出现问题的时间节点
3. 最好还有一个复现demo,用最简单的代码片段复现问题。
4. 当前交流论坛发个bug反馈,提供相关信息。
不存在内存泄漏的普通页面 也会在WeakSet一直输出 能代表什么呢?微信没有回收该页面?但是不能表明该页面存在内存泄漏
在页面B的时候,通过api(getCurrentPages) 可以拿到当前的页面栈,意味着root节点对当前页面栈存在引用, 首页,页面A,页面B都存在引用,触发垃圾回收,肯定不会清除页面实例的。
除非将页面栈清空,但是这样的话就不能返回上一个页面了。
回到首页之后,页面栈只有首页,不会存在对其他页面实例的引用,触发垃圾回收之后,就能把其他页面实例回收。
至于为什么还会保留多一个页面实例,这个目前不是很明确。
大佬,我是长列表,不断下拉,内存不断增加,用你这个内存回收的办法没有用,还是会闪退。还有其他办法吗
正在为这个发愁呢,就找到了这个文章,感谢作者
已确认是微信基础库的问题,将基础库还原到2.10.0和之前的版本内存就稳定了。
无法在体验小程序版本vConsole里面查看weakSet。
(本地开发者里面小程序内存使用是正常的,但是上线之后查看性能发现一直增长。)
内存增长不一定是js内存,也有可能是渲染内存增长,需要检查一下页面节点是否过多,在不可见的时候是否能隐藏节点