微信 7.0.3 事故 Postmortem

这次春节前微信 7.0.3 版本发布之后,开发者们遇到的模糊、卡顿等问题,以及开发者们连日来的反馈让我们寝食难安,好在目前终于针对所有问题,都有了相应的解决方案。但是由于事发突然,很多开发者心急的状况下不太能理解为什么需要做这些修改。因此本文将围绕技术层面和大家分享一些经验,也鞭策引擎团队更加重视平台的兼容性问题。

首先是在微信 7.0.3 版本暴露的问题:

  1. 部分安卓机型上画面变得模糊
  2. Label 贴图变黑的问题
  3. 稳定持续的掉帧问题
  4. 打开开放数据域或大量文字变化的场景的间歇性卡顿问题

让我们挨个来做纯技术层面的分析和解决方案的解答:


1. 部分安卓机型上画面变得模糊

原因

由于微信调整了 “在屏 Canvas“ 的放缩策略,从微信 wx.getSystemInfo 接口获取的 screenWidth 和 screenHeight 比以往要小很多,而引擎对屏幕的适配依赖于这两个接口作为屏幕尺寸来设置 “在屏Canvas” 的尺寸,最终导致 Canvas 分辨率降低,虽然 Canvas 仍然会被缩放到全屏,但视觉效果上就变模糊了。

解决方案

在发布后的 main.js 文件中的 onStart 函数里添加下列代码(主域与开放数据域都需要添加该代码,否者开放数据域触摸区域会有问题)

cc.ContainerStrategy.prototype._setupContainer = function (view, w, h) {
    var locCanvas = cc.game.canvas, locContainer = cc.game.container;

    if (!CC_WECHATGAME && cc.sys.os === cc.sys.OS_ANDROID) {
        document.body.style.width = (view._isRotated ? h : w) + 'px';
        document.body.style.height = (view._isRotated ? w : h) + 'px';
    }
    // Setup style
    locContainer.style.width = locCanvas.style.width = w + 'px';
    locContainer.style.height = locCanvas.style.height = h + 'px';
    // Setup pixel ratio for retina display
    var devicePixelRatio = view._devicePixelRatio = 1;
    if (view.isRetinaEnabled()) {
        devicePixelRatio = view._devicePixelRatio = window.devicePixelRatio || 1;
    }
    // Setup canvas
    locCanvas.width = w * devicePixelRatio;
    locCanvas.height = h * devicePixelRatio;
};
cc.view.enableRetina(true);

解决方案解析

引擎其实早已为浏览器环境提供了 cc.view.enableRetina(true) 接口,这个接口背后其实影响的是 Canvas 尺寸的计算,开启的时候用下面的公式来计算:

canvasSize = screenSize * Math.min(2, devicePixelRatio)

使用 Math.min 是为了避免过高的缩放比例导致像素填充率飙升带来的性能问题,比如 3 倍 devicePixelRatio 相当于 9 倍的像素填充率。解决方案中重写 cc.ContainerStrategy.prototype._setupContainer 就是为了突破这个最高 2 倍的限制,开发者可以自己测试并斟酌是否约束 devicePixelRatio 或是约束在多少。

引擎内部会在后续版本中,研究更稳定可靠的缩放策略。


2. Label 贴图变黑的问题

相关版本

Cocos Creator v2.0.0 - v2.0.6,以及 v2.1.0 有可能遇到这个问题,引擎的最新版本 v2.0.7 和 v2.1.1 开发分支都修复了这个问题。

背景及原因

众所周知,微信小游戏开放早期曾提供一个 weapp-adapter 适配层,用来将微信 API 适配到部分 DOM API 上,以便于大量基于 Web 技术开发的游戏可以尽快接入。很可惜的是这个工具从诞生之初就被告知不会持续维护,所以我们采用了 @finscn(大城小胖)的一个维护版本继续迭代。对 Cocos 引擎来说,大城小胖这个版本最有帮助的一个改动就是 “修改 HTMLImageElement / HTMLCanvasElement / HTMLVideoElement 的实现。可通过 instanceof 检测”。具体来说,就是让下面的代码结果与 Web 端一致:

(canvas instanceof HTMLCanvasElement) === true

我们引擎一直基于这个判断来做贴图提交的分支判断:

if (
    img instanceof HTMLCanvasElement || 
    img instanceof HTMLImageElement ||    
    img instanceof HTMLVideoElement
) {
    // 使用 DOM Element 作为贴图上传 GPU
}
else {
    // 使用 RAW Data(ArrayBuffer) 作为贴图上传 GPU
}

而在微信 7.0.3 中,这个判断失效了,canvas instanceof HTMLCanvasElement 的结果是 false,同时 Label 又是通过离屏 Canvas 来渲染并作为贴图上传到 GPU 的,这就导致 Label 的贴图 Canvas 对象都错误地被当成 ArrayBuffer 来上传了。

解决方案和解析

知道了原因,解决方案就非常简单直接了,我们将贴图提交的分支判断改为了下面的版本(这个问题已经预知,并在 2.0.7 中修复了):

if (img && !ArrayBuffer.isView(img) && !(img instanceof ArrayBuffer)) {
    // 使用 RAW Data(ArrayBuffer) 作为贴图上传 GPU
}
else {
    // 使用 DOM Element 作为贴图上传 GPU
}

注意,判断条件中有三个部分:

  1. img:确保对象存在
  2. ArrayBuffer.isView(img):检查是否是 TypedArray
  3. img instanceof ArrayBuffer:检查是否是 ArrayBuffer

遇到这个问题的开发者如果不愿意升级,可以尝试定制引擎并自己应用相关 PR 中的修改。


3. 稳定持续的掉帧问题

相关版本

只有 v2.0.7 受到影响

原因

引擎的微信适配器中,对 DeviceMotionEvent 的这个修改触发了 v2.0.7 中引擎会默认调用 wx.onAccelerometerChange 并直接监听了微信平台的 Accelerometer 事件,并在整个游戏运行过程中持续响应事件,这带来了不小的损耗,直接导致了部分机型上游戏的丢帧。

解决方案

可以点击编辑器右上角的 “编辑器目录” 按钮进入编辑器目录,找到其中的 “builtin/weapp-adapter/wechatgame/libs/engine/” 文件夹,替换 DeviceMotionEvent.js 文件为最新版本。记得替换后需要重新构建微信主域和开放数据域,打包后必须在微信开发者工具中确保文件替换生效。

解决方案解析

引擎组在调整代码的过程中,并未意识到 wx.onAccelerometerChange 会自动开启事件监听,误以为只是注册监听器,导致了不必要的性能开销,这完全是引擎工作中的疏忽。


4. 打开开放数据域或大量文字变化的场景的间歇性卡顿问题

影响范围

低端机上开启开放数据域或大量文字变化的场景,文字变化包含:文字内容变化、排版变化、颜色变化等。

原因

这个问题背后的原因可能非常复杂,由于我们不了解微信的原生层实现,只能根据已知信息做一些猜测。我们在针对 Demo 进行远程调试并 profile 过程中发现,卡顿时消耗最大的一直都是引擎的 Texture2D.prototype.handleLoadedTexture 接口,这是用来更新贴图的接口。除了资源加载以外,只有文字的变化和开放数据域的更新会调用到这个接口。进一步测试和开发者反馈的信息都印证了的确是打开开放数据域或大量文字变化的场景与卡顿现象有直接关联。与此同时,微信 7.0.3 的一个重大改进就是增加了多线程渲染的支持。这里的多线程渲染,我们猜测应该就是将运行游戏和引擎逻辑的 JS 线程与执行 GL 指令的渲染线程拆分开。JS 线程提交的渲染指令会以某种格式打包并发送给 GL 渲染线程,发送后就可以继续执行,享受到并行计算带来的性能增益。然而在我们出问题的使用场景中,每一帧的渲染,必须等待 handleLoadedTexture 提交贴图给 GL 线程完成,才能执行帧渲染,因为渲染过程依赖 Label 或开放数据域的贴图。这就在多线程渲染的过程中引入了同步等待,对于低端设备来说,很可能产生线程间的资源竞争并导致线程阻塞,最终导致卡顿。

解决方案

从目前的猜测来看,这个问题是微信底层实现导致的,开发者并不需要处理,也无从处理,相信微信团队会尽快修复问题。


总结

以上就是这次事故的技术层面分析汇总,希望大家都已经按照我们之前提供的方案重新提交修复版本,并通过审核,也衷心祝愿所有开发者能在解决问题后过个安心快乐的新年!

13赞

大大们幸苦了

点个赞!

:disappointed_relieved::disappointed_relieved::disappointed_relieved::disappointed_relieved::disappointed_relieved::disappointed_relieved:

:joy:还好你们没抛弃我们啊,太TN的坑了:sob:

辛苦了,年后发版本

哎呀,才注意到这个问题,我说这两天的活跃怎么掉了一千多么,也不知道现在改还能不能来得及过审!!!

mark
请问creator V1.9.3的版本也是按照以上处理吗
@panda

按照上面提供的方式修复之后,一些老旧机型升级到微信7.0.3后截图分享直接闪退,一般第一次成功,多试几次之后就闪退了。没升级到7.0.3是没有问题的,打开性能监控没有发现异常。文字越多的地方越容易触发截图闪退。引擎版本是2.1.0。@panda。截图代码


    capture() {
        //截图初始化
        let texture = new cc.RenderTexture();
        let gl = cc.game._renderContext;
        // this.shareCard 是要截图的节点
        texture.initWithSize(this.shareCard.width, this.shareCard.height, gl.STENCIL_INDEX8);
        this.cam.targetTexture = texture;
        let width = texture.width;
        let height = texture.height;
        if (!this.ifCard) {
            this.ifCard = true;
            this.shareActionStart();
        }
        if (cc.sys.platform == cc.sys.WECHAT_GAME && this.nowShare) {
            this.shareCard.active = true;
            this.nowShare = false;

            this.cam.render();
            let shareData = {
                width: width,
                height: height,
                Pixels: texture.readPixels(),
                title: "连胜挑战分享"
            }
           // 实际截图分享,该方法的实现在下面
            app.recordShareController.cameraShare(shareData, () => {
                console.warn(`am`, this.cam.containsNode(cc.find(`Canvas`)))
                this.scheduleOnce(() => {
                    this.shareCard.active = false;
                    this.nowShare = true;
                },1)  
                app.shareController.shareGameMission(core.constants.hd.missionShareType.challengeShare)
            });
        }
    }

cameraShare(shareData, cb){
        let config = app.cfg.get("config_other")
        let wechatShare = config.shareConfig;
        let canvas = wx.createCanvas();
        let ctx = canvas.getContext('2d');
        canvas.width = cc.view.getCanvasSize().width;
        canvas.height = cc.view.getCanvasSize().height;

        let rowBytes = shareData.width * 4;
        for (let row = 0; row < shareData.height; row++) {
            let srow = shareData.height - 1 - row;
            let imageData = ctx.createImageData(shareData.width, 1);
            let start = srow*shareData.width*4;
            for (let i = 0; i < rowBytes; i++) {
                imageData.data[i] = shareData.Pixels[start+i];
            }
            ctx.putImageData(imageData, 0, row);
        }

        canvas.toTempFilePath({
            x: 0,
            y: 0,
            width: shareData.width,
            height: shareData.height,
            destWidth: 500,
            destHeight: 400,
            success:(res) => {     
                core.dispatch("rank.ui.resetPosition");
                console.warn("分享截图路径:", res);
                //self.wxShare();
                let msg = {      
                    title: wechatShare.shareInfo.title,
                    imageUrl: res.tempFilePath
                }

                let newSpreader = app.utilController.getNewSpreader();
                if (newSpreader && newSpreader !== "") {
                    msg.query = `action=share&newSpreader=${newSpreader}`;
                }

                wx.shareAppMessage(msg);
            },
            fail(res){
                console.error("截图失败原因:", res, res.errMsg);
                core.dispatch("rank.ui.resetPosition");
            },
            complete(){
                console.warn("cameraShare done1", cc.winSize.width, cc.winSize.height)
                cb()
                console.warn("cameraShare done2", cc.winSize.width, cc.winSize.height)
            }
          })
    }

1.9版本只需要按上面适配下,画面模糊的问题。卡顿的问题,微信已经通过热更解决了

卡顿什么时候解决了? 线上还是卡的啊

你是哪个版本啊?

请问一下,问题4现在有没有持续在跟微信沟通啊。

微信那边论坛里看了下,给的回复都是看这边的帖子的解决方案。
然而这边给的回复又是等微信处理。

现在重度点的游戏上微信卡的没有用户体验啊。

日活每天都再掉,卡顿反馈依然有。相当难受。这个帖子能同步下官方跟进的最新进展吗

cocos 这态度感人啊,远好于egret和laya,有这样的团队,不管解没解决问题都让开发者心里一暖

1赞

mark

我们是1.9.2版本,也是掉帧严重,尤其是播放动画的时候。请问有别的解决方案吗?(按2.0.7的方案,1.9.2版本/libs/engine/ 文件夹里没有DeviceMotionEvent.js文件)

我们一直在和微信保持紧密沟通,我们在尽我们所能进行优化。不过由于在微信上我们的引擎只能以 JS 的形态运行,很多底层(C++)部分不受控制,目前优化起来难度比较大,我们正在反复尝试。

1赞

就cocos团队解决问题的态度,比别的好太多了,顶!

膜拜~~~