论: 为什么每个程序员都需要有一台破电脑, 我与 10 后收银机的二三事

查看 100|回复 6
作者:retrocode   
前言
最近我在做收银机相关的自研项目, 此前外包的原生代码过于烂到了无法修改的底部, 加上 deadline 临近. 我临时上马开始做翻版, 由于是安卓/windows 双端的, 最后整合出的方案是接入 webview, 优先接管主要页面, 解决业务 bug 及性能问题, 后续逐步接管其他页面.
整下来等于是推倒重来了, 时间紧任务重, 我们基本上的工作就是将 安卓端的 java 代码翻译成 typescript 扔进 vue 里, 然后 c#开 webview2/安卓接腾讯 X5 去嵌套调用. 在此先感谢亲爱的chatgpt的代码翻译, 极大减少了我的工作量.
性能瓶颈
讲真此前我是没有特别关注过性能问题的, 相当多的性能问题, 在当代硬件上都不是问题(几毫秒和几十毫秒除非是高频操作,一般的确是没人关心的), 以下的这些问题均是在我们理论上需要兼顾的最低配置硬件上压测发现的, 测试机大概配置如下:
[ol]
  • 灵动 D525(10 年 Q2)
  • 服役估计有 10 年的内存和硬盘
  • win7 32 位系统
  • webview2 109(最后一个支持 win7 的版本)
    [/ol]
    过去俩月我感觉我把此生学到的知识算是都用上了. 期间遇到了一堆性能问题.我大致总结下比较典型的几个问题如下:
    [ol]
  • new 对象时向原型链挂载函数的性能损耗
  • 扫码枪输入过快引起的css 重排卡顿
  • JSBridge 沟通原生的调用耗时
  • vuex 对大对象的劫持时间损耗以及内存损耗
  • webview 所有逻辑运行在UI 线程导致与原生 UI 线程抢占CPU 问题
    [/ol]
    逐个解释
    1. new 对象时向原型链挂载函数的性能损耗
    这点是在极限优化时发现的, 由于是从安卓移植过来的 java 代码, 移植的时候是直接沿用的面向对象模式.所以代码中存在大量的 new 对象操作, 在性能优化到达某一步时我们发现, 遍历构建对象也是个性能点, 100 来个对象在开发机上数毫秒, 在测试机上却 100 毫秒起步, 起初我们是决定先略过的, 直到最后回顾时才开始着手解决, 经过测试最后发现是 typescript 的问题, ts 编译后的代码会将对象方法挂载在原型链上, 而耗时就出在这一步上.
    最终解决方案是 将此处的对象改为直接使用 {} 字面量对象, 耗时一下降低到了 20 毫秒左右
    顺带一提, 测试发现在 10W 数据量下创建 带 get set 的 {} 对象, 耗时比不带的也有数倍提升, 我们是直接将 get 只读改为了普通的属性, 并留下注释表面不可修改解决这一题.
    2. 扫码枪输入过快引起的 css 重排卡顿
    新版本的 webview 页面是采用 flex 和 grid 混用开发完成的, 所以全部布局都是动态的, 当我们最终解决完所有 js 层的性能问题, 绝大多数测试机型上都测试通过后, 最低配置的测试机上不出意外的没让我们失望, 在这台老爷机上, input 内容变更带来的单次 css 重排达到了恐怖的 86 毫秒, 而扫码枪是毫秒级输入的, 在扫码时瞬间输入了十几位数的条码, 于是出现了接近 1 秒的卡顿.
    解决 css 重排问题的方法倒也简单, 使用绝对定位和相对定位将 input 输入框跳出文档流即可, 这样重排将只发生在 input 内, 而不会带没动其他元素发生重排现象.
    3. JSBridge 沟通原生的调用耗时
    这里也是老生常谈的问题了, 由于多了层桥接, webview 沟通原生的速度始终为人诟病, 不过精测在绝大多数机器上这点时间是可接受范围内的, 又是我们的老爷机, 在其他一众几年内的机器上沟通原生的速度都可以控制在 10 毫秒内, 而这机器由于硬件严重老化, 沟通成本达到了 50 毫秒, 而部分业务中有单次执行中查询多次数据库的操作, 导致最终耗时整体达到了 500 毫秒.
    解决方案只能是, 优化代码减少查询次数, 构建针对性的函数, 在原生一次性将所有需要数据查询完成一并返回, 以及将配置类参数提前读取全局持有.
    4. vuex 对大对象的劫持时间损耗以及内存损耗
    这个点应是最离谱的了, 考虑到硬件性能的确不好, 对于绝大多数的数据, 一般的做法是应用启动时读整表然后缓存到内存全局持有的, 其中就有 10W 加的商品数据, 当最终我们将其余优化完成后, 发现启动加载数据还是慢, 在开发机上加载数据可能只需要 100 毫秒, 一般的测试机也可以在 2-4 秒内完成数据加载, 而老爷机上这个时间则是 13-15 秒.
    最终我们定位到了问题在 vuex 上, 整个过程大部分时间都花在了 vuex 的 state.obj = data 上, 最终挂载到 vuex 上的数据是以 kv 和数组形式保存的, 即:
    {
        key: {...},
        ...
    }
    [
        {...},
        {...},
        ...
    ]
    而 vuex 会对对象的所有属性进行劫持处理, 以保证响应式. 这一过程在测试机的 10W+个属性上最终耗时达到了十几秒, 而我们是整表缓存,也就是这些数据理论上是不需要响应式的, 每次都是重新整表赋值即state.obj = data, 此时我们可以使用Object.freeze对对象进行冻结, 以停止 vuex 的响应式开销.
    最终结果喜人, 这一过程的耗时从十几秒降低到 几毫秒 😅
    同时附带的好处, 当项目加载完成后整个 webview 的内存占用从 330m 降低到了 100m
    5. webview 所有逻辑运行在 UI 线程导致与原生 UI 线程抢占 CPU 问题
    webview 运行在 UI 线程, 理所当然的 webview 中的 js 也运行在 ui 线程, 理所当然的 js 调用的原生也会在 UI 线程, 理所当然 js 调用原生的数据库查询也在 UI 线程.
    那么原生拉起 webview 的同时后台渲染下一页以加快速度, webview 页面的一切初始化操作都会与原生 UI 强制 cpu 资源, 造成实际在前台的 webview 的页面加载卡顿.
    最终的解决方案是 webview 页面加载完成后, 之后在由原生执行预加载操作, 与 webview 至少在页面加载时错开以完成首屏加载.
    总结
    经过优化后的成果是喜人的, 即时在服役 10 年的老爷机上依然纵享丝滑, 原先各种诡异的卡顿掉帧也都销售了, 扫描条码终于可以看到数字输入了, 原先由于卡顿条码甚至来不及渲染就会清理掉.心情舒畅可以交差了.
    随着硬件发展, 很多以前的性能瓶颈已经不是瓶颈了,像我开头说的几毫秒和几十毫秒绝大多数情况下没差, 但是一些该了解的知识还是需要了解做知识储备的, 不然碰上了到处撞墙是真的难受.

    WebView, 原生, vuex, 毫秒

  • leon233333   
    🐂
    rimworld   
    🐂,我大概会选择升级配置来解决。
    retrocode
    OP
      
    @rimworld #2 升配置简单, 这种老机器花几块钱换个新的内存条都能带来大幅度, 主要要是更新配置就得去线下一家店一家店的更新, 就比较蛋疼了
    chingyat   
    🐮,佩服 op 的探究精神
    SeanTheSheep   
    牛逼
    taotaodaddy   
    前来学习
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部