花了两天时间,解决了一个“崩溃”的问题

2,752 阅读3分钟

我正在参加「掘金·启航计划」

操作流程:H5 中进行业务操作,会进行弹框预览 pdf 文件(react-pdf),进行签字;也会用弹框进行多次视频流截屏处理(canvas)。弹框组件用的是 antd-mobile 的 Modal 组件。

问题描述:在微信浏览器中,会直接强制刷新页面;在 ios 浏览器中,也会直接强制刷新页面;在谷歌模拟器中,浏览器直接崩溃了,看控制台之后,才知道是内存溢出了。

发现问题

在微信浏览器中,每次签字后,再截取视频流,几次操作后,点击“确定”按钮,会导致强制刷新页面。

在客户 IOS 手机的微信公众号中运行 H5 页面发现该问题,我们这边的测试机无法复现,复现步骤也是按他们的固定方法,增加了问题排查难度。只能让驻场测试人员帮忙验证、复现,但是中间的沟通交流啥的总会有差异,验证过程需要我们改了,发布到测试,他们再验证,导致效率很低。

最开始是在微信公众号中发现该问题的,发现微信会直接强制刷新页面。到微信公众号交流社区查了一下,有很多这种类似的问题,例如频繁切换摄像头进行拍照、使用微信的 chooseImage 等。具体文章,例如:webview 组件内调用拍照功能返回页面时造成页面刷新。但是没看见官方的解决回复。。。

  • 问题原因:手机瞬时内存达到峰值,导致系统崩溃了,微信这边主动触发页面刷新重新加载了。如果不做刷新操作,那就会显示空白页了
  • 解决尝试:
    1. 重启手机系统;
    2. 清除一点系统存储空间;
    3. 清除微信缓存;
    4. 用 canvas 截取视频流时,需要进行压缩处理;

复现问题

我们经过一段时间处理解决后,慢慢缩小问题范围,最终在我们手机复现出来了,而且简化了复现步骤。但是具体定位还不准确

在谷歌模拟器中复现了问题,但是浏览器的表现是直接崩溃了,然后排查浏览器的内存表现,发现“截屏”按钮的操作时间是 3-5s,存在明显卡顿,主要关注 canvas 截图导致的卡顿和崩溃问题

谷歌模拟器崩溃截图.png

以上处理均未解决

3. 定位问题

最终,用最简单的步骤复现出了问题,是 modal 预览 pdf 弹框的问题。

最开始以为是 pdf 预览的问题,最后才定位在 modal 弹框,虽然关闭销毁了,但是占据的内存空间没有释放,感觉是假的关闭。

最终改了一行代码,解决了问题,如下所示:{show ? children : <></>}

import React, { useState, useImperativeHandle, forwardRef, useRef, useEffect } from 'react'
import styles from './index.less'
import { Modal, Icon } from 'antd-mobile'
interface iprops {
    title?: string
    closable?: boolean
    children: React.ReactElement | Array<React.ReactElement>
    hiddenCloseButton?: boolean
    footer?: React.ReactElement | Array<React.ReactElement>
    openCallBack?: (arg1: any) => void
    closeCallBack?: () => void
    className?: string
}

interface iDetailRef {
    current: {
        dialogShowOrHide: (arg: boolean) => void
        container: {
            current: HTMLElement
        }
    }
}

function detailDialog(props: iprops, ref: iDetailRef) {
    const { title, children, closable, hiddenCloseButton, footer, openCallBack, className, closeCallBack } = props
    const [show, dialogShowOrHide] = useState(false)
    const container = useRef(null)

    // 暴露给外部的方法
    useImperativeHandle(ref, () => ({
        dialogShowOrHide,
        container
    }))

    useEffect(() => {
        // 打开弹框后的回调事件
        if (show) {
            openCallBack && openCallBack(container)
        }
    }, [show])

    // 弹框默认关闭事件
    function handleModalClose() {
        if (closable) {
            dialogShowOrHide(false)
        }
    }

    // 自定义关闭按钮事件
    function handleClose() {
        closeCallBack && closeCallBack()
        dialogShowOrHide(false)
    }

    return (
        <>
            <Modal
                className={`${className} ${styles.detail_modal}`}
                visible={show}
                title={title}
                transparent
                onClose={handleModalClose}
                closable={closable}
                popup
                animationType="slide-up"
            >
                <div className={styles.detail_modal_widget}>
                    {
                        // 关闭按钮
                        !hiddenCloseButton ? (
                            <div className={styles.deatil_modal_close} onClick={handleClose}>
                                <Icon type="cross" color="#999999" size="md" />
                            </div>
                        ) : (
                            <></>
                        )
                    }
                    <div className={styles.detail_modal_content} ref={container}>
                        {/* 最终就是改了一行代码,不显示时,销毁内容 */}
                        {show ? children : <></>}
                    </div>
                    {/* footer内容 */}
                    {footer ? <div className={styles.detail_modal_footer}>{footer}</div> : <></>}
                </div>
            </Modal>
        </>
    )
}

export default forwardRef(detailDialog)