外观
跨屏取消、并发和异常恢复
这里描述建议方案。核心目标是无论成功、取消还是失败,任务、输入、图层和资源最终都进入可解释的稳定状态。
1. 为什么跨屏需要状态机?状态应该怎样划分?
**标准回答:**同一次转场会收到手势、窗口、应用绘制、动画、显示变化和超时等异步事件。如果只使用几个分散的 boolean,就容易出现“动画结束了,但任务还没接管”或“取消后又收到成功回调”。状态机明确每个阶段可以接收哪些事件、产生哪些动作,并为所有结束路径规定清理责任。
可以从 PREPARING、ANIMATING、HANDOFF、COMPLETED、CANCELLED、FAILED 开始,按实际系统的同步能力调整。状态不宜为了看起来复杂而膨胀,但目标窗口接管应有独立条件。
**追问:FAILED 必须移回源屏吗?**不一定。要看失败发生在任务移动前还是后,以及哪一端仍有有效真实画面。恢复策略应基于当前可确认的稳定状态,不能无条件执行一次反向迁移。
2. 旧转场的延迟回调,为什么会破坏新转场?
**标准回答:**转场 A 被取消后,捕获、IPC 或绘制回调可能仍在途中。若回调只带 displayId,没有会话身份,就可能误把旧快照或旧就绪信号交给转场 B。每个异步结果应携带 transitionId,必要时再携带 taskId、窗口身份和配置代次,接收方检查它属于当前会话和允许的状态。
取消后应该失效该会话、停止后续提交,并释放旧会话自己的资源。不能仅把 ready 标志清零;否则晚到的旧回调又会把它设回来。
**Java 思路示例:**由一个 Handler 所在线程串行处理状态,外部回调只投递事件。实际图层和 IPC 动作交给明确的执行器,避免控制线程长期阻塞。
java
void onTargetReady(long transitionId, int configurationGeneration) {
controlHandler.post(() -> {
Session s = currentSession;
if (s == null || s.id != transitionId) return;
if (s.state != State.PREPARING) return;
if (s.targetGeneration != configurationGeneration) return;
s.targetReady = true;
tryAdvance(s); // 检查所有条件,再提交下一阶段动作
});
}**追问:丢弃旧回调就够了吗?**不够。旧回调可能携带刚创建的快照或资源句柄;丢弃结果时仍须由对应拥有者释放,不能因为不使用就泄漏。
3. 应用死亡、屏幕断开和超时,怎样处理?
**标准回答:**把这些事件接入同一个转场管理路径。应用死亡时,不再等待它报告目标就绪;屏幕断开时,撤销相关显示上的操作,并选择仍存在的显示或结束会话;准备或接管超时时,进入有明确视觉和任务行为的降级路径。显示重新连接后还要重新验证身份与配置,不能认为旧 displayId 永远代表同一实体。
跨进程对象可用 Binder 死亡通知辅助发现服务死亡,但业务仍需超时、幂等和恢复策略。清理函数应可以重复调用:只释放本会话拥有的对象,并记录是否已经结束。
**追问:超时长一点,能避免故障吗?**它只能降低某些慢操作被提前终止的概率,却可能让用户长时间停在快照上。应该测量准备和接管的分布,给出可解释的期限、降级反馈和日志。
4. 连续三指手势、N 屏并发和资源冲突,怎么协调?
**标准回答:**先确定冲突单位,例如同一个 task 或同一份业务会话是否允许同时流转。冲突请求可选择拒绝、排队或替换,依据用户意图和状态阶段决定;不同任务的独立操作可以并发。若需要同时锁定两个显示,必须有确定的加锁顺序,并避免持锁执行慢 IPC、捕获或等待回调。
不建议为了实现简单,让所有 N 屏操作都争抢一把全局大锁。可以用短临界区更新会话登记,或用串行控制线程处理状态,耗时工作放到外部执行器。系统全局锁的约束仍需按源码规则遵守。
**追问:把旧动画 cancel 后立即开新动画,可以吗?**必须先定义取消后的图层、任务与输入状态。若旧动画还在收尾,新动画复用了同一图层句柄,就可能互相修改。可以让新会话创建自己的资源,或等到旧会话完成明确的交接屏障后再复用。