H5页游戏内存溢出问题

  • 记录自己解决的第一个H5页的性能问题, 关于内存溢出
  • 拼字游戏

问题表现

  • 初始化后, 第一次拼字并不卡.

  • 随着拼的次数越来越多, 越来越卡

  • 浏览器任务管理器中可以看出, 内存持续升高

  • 确定内存问题, 即是卡顿第一问题

    内存飙升

代码层面问题

  • 首先希望从代码层面排查内存问题
  • 思考代码后, 发现以下两点

第二舞台直接删除Canvas的Dom

  • 问题:
    • 每次都会重新建立第二个分享舞台.
    • 上一次的canvas直接删除了Dom.
  • 推断:
    • 推断直接删除DOM, 并不能释放舞台上一系列的Bitmap图像等
    • 上几次图像继续占用内存
  • 排查与解决:
    • 游戏舞台与分享舞台只保留一个
    • 每次游戏, 通过指针清空bitmap
  • 结果:
    • 内存无明显变化

loader进来的临时图片未删除

  • 问题:
    • 每次完成一次拼字都会请求后台这次结果需要加载的资源
    • 资源中包含大量的图片等, 都是通过new Image()的src加载过来的
  • 推断:
    • 每次浏览器加载大量图片, 展示后, 并未释放图片空间
  • 排查与解决:
    • 找出不能重复利用的图片, 使用新的加载器
    • 一次性使用图片后, 让加载器为null
  • 结果:
    • 内存少量减少
    • 游戏已经卡顿

每次游戏会重新new一些对象

  • 问题: 因每次进行游戏都会new一些对象出来
  • 推断: 怀疑是游戏过程中js内存溢
  • 排查: js内存变化
  • 结论: js中的内存变化不能引起内存卡顿问题.

GPU层面问题

  • 直接贴链接了: Chromium Graphics // Chrome GPU
  • 这次通过任务管理器可以直接看出是GPU缓存的溢出
  • 哭了, 为什么上次我测得时候不显示... 仅仅是一个内存的表示, 给我好找啊..
  • 最后还是在安卓中进行排查, 才知道是GPU / chromium grapics的问题

观察GPU表现体征

  • 我们在游戏中发现内存的溢出主要是GPU的变化
  • 在游戏中去除所有的序列帧后, 只保留最基本的游戏, 任何手机都不卡.
  • 那就一定是这些图片搞得鬼.
  • 已经把加载的图片缓存清除掉了.
  • 我们排除了canvas, 排除了图片加载, 剩下的就是最基本的图片展示了.
  • 现在唯一剩下的就是浏览器自己做的事情了.
  • 展示图片, 然后清理DOM
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <style>
  
  #stage {
    width: 100%;
    height: 100%;
  }
  </style>
  <title>测试内存</title>
</head>

<body>
    <button id="add">增加图片</button>
    <button id="clear">清除内存</button>
    <button id="addTen">增加十倍图片</button>
    <div id="stage"></div>
    <script>
    var count = 0
    var stage = document.getElementById('stage')
    document.getElementById('add').onclick = function () {
      for (var i = 0; i < 23; i++) {
        stage.innerHTML += `<img src='./img${count}/${i}.jpg' />`
      }
      ++count
    }
    document.getElementById('clear').onclick = function () {
      stage.innerHTML = ''
      count = 0
    }
    document.getElementById('addTen').onclick = function () {
      for (var j = 0; j < 10; j++) {
        for (var i = 0; i < 23; i++) {
          stage.innerHTML += `<img src='./img${count}/${i}.jpg' />`
        }
      }
    }
    </script>
</body>

</html>

chrome中测试

  • 在添加三组十倍图片后, 清除DOM
  • GPU和Image cache, 在浏览器中并不能总结出规律

webview表现特征

  • 我们写了一个最简单的demo, 在安卓上的webview进行了测试
  • 测试结果如下:
    • 初始状态下的内存: init
    • 添加图片并浏览器后的内存: add
    • 清理页面dom后的内存: clear

总结

个人理解原理

浏览器中的GPU会自动缓存一段时间展示过的 "图像". 我们称为: "GPU Program Cache"

  • 浏览器在读取图片之后会生成GPU可以使用的着色器代码
  • 在GPU使用的时候, 会自动缓存这些着色器代码.

最后解决办法

  • 去掉大量序列帧, 序列帧尽量用小图
  • 动画尽量用代码实现

附带一个测试着色器的代码, 也是当时测试webgl的代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>

<body>
  <canvas id="glcanvas" width="700" height="500"></canvas>
  <script>
    // 获取WebGL
    var canvas = document.getElementById('glcanvas')
    gl = canvas.getContext("webgl")

    // 创建顶点着色器
    var VSHADER_SOURCE =
      'void main() {\n' +
      '  gl_Position = vec4(0.0, 0.0, 0.0, 1.0);\n' +
      '  gl_PointSize = 10.0;\n' +
      '}\n';
    // 创建片元着色器
    // var FSHADER_SOURCE =
    //   'void main() {\n' +
    //   '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
    //   '}\n';

    // 绘制圆
    var FSHADER_SOURCE = `
      #ifdef GL_ES
        precision mediump float;
      #endif
        void main() {
          float d = distance(gl_PointCoord, vec2(0.5,0.5));
          if(d < 0.5){
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
          }else{ discard;} 
        }`;

    // 着色器代码需要放到一个程序中
    var program = gl.createProgram()
    // 创建顶点着色器
    var vShader = gl.createShader(gl.VERTEX_SHADER)
    // 创建片元着色器
    var fShader = gl.createShader(gl.FRAGMENT_SHADER)

    // shader容器与着色器绑定
    gl.shaderSource(vShader, VSHADER_SOURCE)
    gl.shaderSource(fShader, FSHADER_SOURCE)

    // 将GLSE语言编写成浏览器可用的代码
    gl.compileShader(vShader)
    gl.compileShader(fShader)

    // 将着色器添加到程序上
    gl.attachShader(program, vShader)
    gl.attachShader(program, fShader)

    // 链接程序
    // 在链接操作以后, 可以任意修改shader代码.
    // 对shader重新编译不会影响整个程序, 除非重新链接程序
    gl.linkProgram(program)

    // 加载并使用链接好的程序
    gl.useProgram(program)

    // 绘制一个点
    gl.clearColor(0.0, 0.0, 0.0, 1.0)
    gl.clear(gl.COLOR_BUFFER_BIT)
    gl.drawArrays(gl.POINTS, 0, 1)
  </script>
</body>

</html>
posted @ 2019-07-03 17:13  张润昊  阅读(1323)  评论(0编辑  收藏  举报