1.4 发布之后,我自己用了一阵,发现一个之前没有的毛病:双击图标想隐藏 App,它有时不隐藏,有时闪一下又回来。双击隐藏在 1.1 就有,一直没问题,所以这次几乎可以肯定是新加的东西造成的。

这一篇记录它的原因、修法,以及一条以后加“自动纠正”类机制时要记住的规则:任何会自己动手的机制,都要想清楚用户的每一种主动操作对它意味着什么。

现象和推断

双击在 Touch Bar 上是两次点击:第一下和普通单击完全一样,立刻切换到这个 App;第二下(0.35 秒内、同一个图标)触发隐藏。单击不为了等双击而延迟,这是从第一版就定下的原则,所以第一下的切换一定会先发生。

1.4 新加的“盯着结果并纠正”,在每次切换之后的约 2.4 秒里,每 0.12 秒核对一次:前台是不是目标 App、当前桌面上有没有它的窗口。不一致就重新把它提到前面。双击的第二下把 App 隐藏了,恰好满足“前台不是目标”,盯梢就认为焦点被抢走了,尽职地把它又提了回来。我把这个推断当作假设,改之前先对照代码确认:隐藏的实现只是延迟 0.2 秒调用系统的隐藏,完全没有告诉盯梢“这次是用户主动要隐藏”。

为什么之前没测出来

上一阶段的压力测试和“捣乱模式”都是围绕“切换”的:连点、故意抢焦点、把桌面切回去。没有一条用例是“切换之后马上隐藏”。盯梢机制的所有停止条件也都是围绕“别的切换”设计的:有更新的点击、或者键盘鼠标有动作才放弃。而 Touch Bar 上的触摸既不是键盘也不是鼠标,隐藏也不是“点击”,所以没有任何一个条件能让它停下来。

这类问题的共同点是:新机制的测试只覆盖了它想解决的场景,没有盘点“用户还能做哪些操作”。手势有单击、双击、长按三种,我只把其中一种放进了它的世界里。

修法:隐藏也是一次新点击

评估之后,我没有考虑去掉双击。双击隐藏的第二下几乎总是落在同一个 App 上,没有理由为它拖慢单击;只要让第二下正确地取消第一下的收尾就行。用户也给了同样的取舍:单击的响应速度优先,双击可以妥协。

具体做了三件事。第一,隐藏被记为一次新点击:每次点击都有一个递增的序号,切换和盯梢都靠“有没有更新的序号”判断自己是否已经过时,隐藏也递增序号,并且标成“不属于任何 App”,这样两种过时判断都会命中,在途的切换和盯梢立刻退出。第二,隐藏不再抢在前面,而是排到同一个串行队列的后面,等在途的提升退出之后再执行,避免第一下的最后一个动作在隐藏之后才落地。第三,隐藏之后核对结果:0.25 秒后看 App 是不是真的被隐藏了,没有就再隐藏一次,最多重试两次。系统的隐藏接口返回值本来就不可靠,所以只能看结果。

验证:这一次我没有拿到自动化数据

我写了一个复现脚本:让第一下切换、隔 150 或 300 毫秒之后发第二下隐藏,3 秒后看 App 是不是处于隐藏状态,覆盖“目标不在前台”和“目标本来就在前台”两种起点。脚本第一次运行时,我自己的键盘鼠标有动作,被输入检测当成干扰中止了,没有产出任何结果。

所以这次的修复,在动手之前没有拿到失败的数字,也没有拿到修复后的数字。我把它如实交给用户手动测试:在别的桌面和别的 App 上双击,以及在已经在前台的 App 上双击。用户测试之后的反馈是“基本流畅了”。这是一次手动确认,不是压力测试,我不会把它写成“已经验证稳定”。

留下的规则

给任何“自动纠正”“自动重试”类的机制加上停止条件时,先把用户能做的所有主动操作列出来,逐条问:这一步会不会被它误认为异常?在这个项目里,就是单击、双击、长按退出、键鼠操作和菜单里的开关。任何一个会让 App 离开前台的主动操作,都必须能让盯梢停下来。

另外,发布前的测试清单要有一条“新机制和已有手势的组合”,而不只是新机制本身。这次的问题,本来在发布前跑一遍“切换后立刻隐藏”就能发现。