飞行模式核心逻辑:状态机设计、状态切换条件、状态保持机制
飞行模式这东西,说白了就是一个有限状态机。你按一下开关,它从一个状态跳到另一个状态。听起来简单?嗯,我当年第一次做这个功能时也这么想,结果被各种边界条件折腾得够呛。
今天咱们就把状态机的设计思路、切换条件、还有那个容易被忽略的「状态保持」机制,一次性讲透。
状态机设计:三个核心状态
飞行模式的状态机,我习惯只保留三个状态。多了反而乱,少了又不够用。你想想看:
| 状态名称 | 枚举值 | 含义 |
|---|---|---|
| 飞行模式关闭 | FLIGHT_MODE_OFF |
所有无线模块正常工作 |
| 飞行模式开启 | FLIGHT_MODE_ON |
所有无线模块关闭,仅保留紧急呼叫 |
| 飞行模式切换中 | FLIGHT_MODE_TRANSITIONING |
正在关闭或开启无线模块,中间态 |
关键点:「切换中」这个状态很多人会漏掉。我在项目中遇到过,如果不加这个中间态,用户在切换过程中反复点击开关,会导致无线模块的开启/关闭指令乱序,最后系统直接卡死。
状态切换条件:谁触发了状态变化?
状态不会自己跳,得有触发条件。我归纳了四种常见的触发源:
- 用户手动切换 — 设置页面的开关、快捷面板的图标
- 定时任务 — 比如凌晨自动开启飞行模式省电
- 外部事件 — 插入SIM卡、检测到机场区域(通过基站识别)
- 系统广播 — 关机前保存状态、开机后恢复状态
每种触发源对应的处理逻辑其实不一样。举个例子,用户手动切换时,通常需要弹个确认对话框;但定时任务触发时,直接静默切换就行,别打扰用户睡觉。
我的习惯:把触发源也作为状态机的一个输入参数记录下来。这样调试时,一看日志就知道「哦,是定时器让它飞的,不是用户手滑」。这个习惯帮我省了不少排查时间。
状态保持机制:别让状态「乱跑」
状态保持,说白了就是「状态一旦确定,就别轻易变」。但现实很骨感——系统重启、应用被杀、配置被篡改,哪个都能让状态乱掉。
我一般用三层保障:
| 层级 | 存储方式 | 恢复时机 |
|---|---|---|
| 第一层 | 内存变量(最快) | 应用运行时 |
| 第二层 | SharedPreferences / UserDefaults | 应用冷启动 |
| 第三层 | 系统属性 / 内核参数 | 系统重启后 |
三层同步策略是这样的:每次状态变化,先写内存,再异步写持久层。读取时,优先读内存,内存没有就从持久层恢复。
我曾经踩过的坑:只做了内存和SharedPreferences两层。结果有一次系统OTA升级后,SharedPreferences被清空了,飞行模式状态丢失。用户醒来发现手机没信号,投诉电话打爆了。从那以后,我坚持加第三层——系统属性级别的存储,OTA升级也不会丢。
状态机代码骨架
下面是一个精简的状态机实现,去掉业务细节,只保留核心逻辑。你可以直接拿去改:
// 状态枚举
enum FlightModeState {
OFF,
ON,
TRANSITIONING
}
// 触发源
enum TriggerSource {
USER_MANUAL,
SCHEDULED_TASK,
EXTERNAL_EVENT,
SYSTEM_BROADCAST
}
// 状态机核心
class FlightModeStateMachine {
private FlightModeState currentState = FlightModeState.OFF;
private FlightModeState previousState = FlightModeState.OFF;
// 状态切换入口
public void transitionTo(FlightModeState target, TriggerSource source) {
if (currentState == FlightModeState.TRANSITIONING) {
// 正在切换中,忽略新请求
log("Ignored: already transitioning, source=" + source);
return;
}
if (currentState == target) {
// 目标状态与当前相同,不处理
log("Ignored: same state, source=" + source);
return;
}
// 记录切换前状态
previousState = currentState;
currentState = FlightModeState.TRANSITIONING;
// 执行切换动作(开启或关闭无线模块)
boolean success = performTransition(target, source);
if (success) {
currentState = target;
persistState(target); // 写入三层存储
log("Transition success: " + previousState + " -> " + target);
} else {
// 切换失败,回滚
currentState = previousState;
log("Transition failed: rollback to " + previousState);
}
}
// 从持久层恢复状态
public void restoreState() {
FlightModeState saved = loadFromPersistentStorage();
if (saved != null) {
currentState = saved;
log("State restored: " + saved);
}
}
}
注意看:我在切换失败时做了回滚。这个回滚逻辑,说实话,一开始我也没写。直到有一次测试发现,蓝牙模块关闭超时,状态卡在TRANSITIONING,用户再也打不开飞行模式了。回滚虽然不能解决问题,但至少让系统回到一个可用状态。
状态机流程图
下面这张图把三个状态和它们之间的切换关系画清楚了。我建议你把它贴在工位旁边,写代码时瞄一眼,不容易乱。
状态保持的「防抖」设计
最后聊一个容易被忽视的点:防抖。用户快速点击开关时,状态机可能在一秒内收到十几次切换请求。如果不做防抖,无线模块会被反复开关,轻则耗电,重则烧硬件。
我的做法是加一个「最小切换间隔」:
// 防抖参数:两次切换之间至少间隔 500ms
private static final long MIN_TRANSITION_INTERVAL_MS = 500;
private long lastTransitionTime = 0;
public boolean canTransition() {
long now = System.currentTimeMillis();
if (now - lastTransitionTime < MIN_TRANSITION_INTERVAL_MS) {
return false; // 太频繁了,忽略
}
lastTransitionTime = now;
return true;
}
注意:这个防抖时间不能设得太长。我见过有人设了3秒,结果用户觉得手机反应慢,疯狂点击,反而触发了更多问题。500ms 是个比较合理的值,既防抖又不影响体验。
好了,状态机的设计、切换条件、保持机制,这三块拼起来就是飞行模式的核心骨架。下一块咱们要聊的是「无线模块的开关顺序与依赖关系」——嗯,这部分才是真正考验细节的地方。