评论

【小程序代码自查】小程序闪退-内存泄露导致

小程序内存占用过大导致异常退出的问题排查。排查代码存在的内存泄露情况,查看是否存在内存泄漏,导致内存占用过大小程序异常退出

背景

用户经常出现闪退的情况,并提示内存不足。根据用户操作场景,猜测页面存在内存泄露。

内存泄露是什么?

内存泄露是程序运行过程中产生的内存变量会一直存在,不会被垃圾回收机制检测到,导致一直不会被销毁,内存占用会越来越大。

比如说:

我们在运行小程序的时候会产生一个页面,小程序会给这个页面创建一个实例,当这个页面销毁的时候,这个实例应该会被销毁。

但是如果我们有个定时器(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);
  }
})

通过上面我们可以知道一般会有上面四种情况导致内存泄露。

  1. 将对象挂载到全局对象上,页面写在没有清楚
  2. 通过暴露内部函数给外部对象,导致存在作用域的引用,页面卸载没有清楚内部函数
  3. 存在定时执行的函数存在对页面实例的引用,页面销毁没有清除定时器
  4. 通过延时执行的函数循环调用,并存在对页面实例的引用,页面销毁没有停止调用。

第一第二种情况会比较少出现,目前暂时还没考虑如何去排查。

第三第四种都会对页面实例存在调用,所以我们在页面实例销毁之后对页面实例上的属性进行监听,如果一直存在调用则会有问题。

具体实现代码:

// 检查页面卸载后对页面实例调用
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

我们在自己项目里面测试会比较麻烦,一开始可能会有干扰,所以我这边弄了个代码片段,先校验一下这个方法是否可行,如果可行再加到自己的项目里面。

小程序代码片段


最后一次编辑于  2021-05-12  
点赞 5
收藏
评论

6 个评论

  • Lai
    Lai
    2022-06-03

    请问我的小程序有个页面是用来拍照上传图片,每拍一张图片就会先进行压缩,压缩到大概400k之后会回显缩略图到本页面; 小程序在部分手机会在这个页面闪退; 有的手机拍80张照片也没问题,有的手机拍个10来张就闪退(均为iphone11/13之类),请问这种问题如何定位呢? 因为此功能之前也没见报问题,5月份开始有这种问题,是不是微信基础库升级导致的? 或者开发者有什么手段去定位?

    2022-06-03
    赞同
    回复 1
    • 沛林
      沛林
      2022-06-06
      先确认问题,是否有提示因为内存占用过大,导致的异常退出。
      如果没有这个提示,直接闪退的情况,通常都是触发了某个异常bug,个人开发者比较难定位,需要寻求微信开发者的协助。
      如果是安卓,可以抓安卓的运行日志,分析问题。
      建议处理方式:
      1. 录制一个复现视频
      2. 通过微信上传一下微信日志,记录微信号,出现问题的时间节点
      3. 最好还有一个复现demo,用最简单的代码片段复现问题。
      4. 当前交流论坛发个bug反馈,提供相关信息。
      2022-06-06
      回复
  • Null
    Null
    2022-01-21

    不存在内存泄漏的普通页面 也会在WeakSet一直输出 能代表什么呢?微信没有回收该页面?但是不能表明该页面存在内存泄漏

    2022-01-21
    赞同
    回复 5
    • 沛林
      沛林
      2022-02-14
      不存在内存泄露的页面实例,在触发垃圾回收之后,这个页面实例应该会被回收。在weakSet中一直输出,代表存在有引用,这个引用有可能是程序的引用,也有可能是控制台的引用,所以要清空console的输出,再触发垃圾回收,再观察weakSet。
      2022-02-14
      回复
    • Null
      Null
      2022-04-07回复沛林
      如何在真机上vConsole中查看weakSet
      2022-04-07
      回复
    • 沛林
      沛林
      2022-04-08回复Null
      目前没办法在真机上查看,只能通过开发者工具查看
      2022-04-08
      回复
    • Null
      Null
      2022-04-11回复沛林
      发现个现象,首页->A页面->B页面->C页面,回到A或者B页面手动触发垃圾回收,并不会减少weakSet,只有回到首页,触发垃圾回收,才会减少weakSet。有没有遇到这个现象过。
      2022-04-11
      回复
    • 沛林
      沛林
      2022-04-12
      这个首先要理解为什么触发垃圾回收之后会将一些页面实例清除掉。
      在页面B的时候,通过api(getCurrentPages) 可以拿到当前的页面栈,意味着root节点对当前页面栈存在引用, 首页,页面A,页面B都存在引用,触发垃圾回收,肯定不会清除页面实例的。
      除非将页面栈清空,但是这样的话就不能返回上一个页面了。
      回到首页之后,页面栈只有首页,不会存在对其他页面实例的引用,触发垃圾回收之后,就能把其他页面实例回收。
      至于为什么还会保留多一个页面实例,这个目前不是很明确。
      2022-04-12
      回复
  • 叶伟华
    叶伟华
    2021-07-28

    大佬,我是长列表,不断下拉,内存不断增加,用你这个内存回收的办法没有用,还是会闪退。还有其他办法吗

    2021-07-28
    赞同
    回复 1
    • 沛林
      沛林
      2021-08-02
      可以考虑一下用一个虚拟列表,微信有个api可以判断组件是显示还是隐藏,当组件处于3屏幕以外的时候,可以隐藏,这样会减少渲染内存
      2021-08-02
      回复
  •  王
洪
贺
     王 洪 贺
    2021-05-21

    正在为这个发愁呢,就找到了这个文章,感谢作者

    2021-05-21
    赞同
    回复 1
    • 沛林
      沛林
      2021-05-25
      有什么问题都可以随时沟通呀 文章写得不完善,希望可以一起完善一下
      2021-05-25
      回复
  • 子寒
    子寒
    2021-05-17

    已确认是微信基础库的问题,将基础库还原到2.10.0和之前的版本内存就稳定了。

    2021-05-17
    赞同
    回复 1
    • 沛林
      沛林
      2021-05-18
      这么奇怪
      2021-05-18
      回复
  • 子寒
    子寒
    2021-05-14

    无法在体验小程序版本vConsole里面查看weakSet。

    本地开发者里面小程序内存使用是正常的,但是上线之后查看性能发现一直增长。

    2021-05-14
    赞同
    回复 13
    • 子寒
      子寒
      2021-05-14
      意思是,检查线上的情况比较重要,目前还找不到合适的方法(上面的方法也不合适)
      2021-05-14
      回复
    • 沛林
      沛林
      2021-05-17
      可以在开发者工具上看
      2021-05-17
      回复
    • 沛林
      沛林
      2021-05-17
      上线后性能一直增长,有记录对应的操作流么?以及按照相同的操作流程,可以在开发者工具查看么?
      内存增长不一定是js内存,也有可能是渲染内存增长,需要检查一下页面节点是否过多,在不可见的时候是否能隐藏节点
      2021-05-17
      回复
    • 子寒
      子寒
      2021-05-17回复沛林
      开发工具一切正常。就是上线就不行,就放着不动,就会慢慢撑爆内存。检查了没有循环操作代码
      2021-05-17
      回复
    • 子寒
      子寒
      2021-05-17回复沛林
      打开线上小程序性能可以看到内存2m左右慢慢增长,每释放一点点又继续撑爆内存,越来越大
      2021-05-17
      回复
    查看更多(8)
登录 后发表内容