飞行模式核心逻辑:状态机设计、状态切换条件、状态保持机制

飞行模式这东西,说白了就是一个有限状态机。你按一下开关,它从一个状态跳到另一个状态。听起来简单?嗯,我当年第一次做这个功能时也这么想,结果被各种边界条件折腾得够呛。

今天咱们就把状态机的设计思路、切换条件、还有那个容易被忽略的「状态保持」机制,一次性讲透。

状态机设计:三个核心状态

飞行模式的状态机,我习惯只保留三个状态。多了反而乱,少了又不够用。你想想看:

状态名称 枚举值 含义
飞行模式关闭 FLIGHT_MODE_OFF 所有无线模块正常工作
飞行模式开启 FLIGHT_MODE_ON 所有无线模块关闭,仅保留紧急呼叫
飞行模式切换中 FLIGHT_MODE_TRANSITIONING 正在关闭或开启无线模块,中间态

关键点:「切换中」这个状态很多人会漏掉。我在项目中遇到过,如果不加这个中间态,用户在切换过程中反复点击开关,会导致无线模块的开启/关闭指令乱序,最后系统直接卡死。

状态切换条件:谁触发了状态变化?

状态不会自己跳,得有触发条件。我归纳了四种常见的触发源:

  1. 用户手动切换 — 设置页面的开关、快捷面板的图标
  2. 定时任务 — 比如凌晨自动开启飞行模式省电
  3. 外部事件 — 插入SIM卡、检测到机场区域(通过基站识别)
  4. 系统广播 — 关机前保存状态、开机后恢复状态

每种触发源对应的处理逻辑其实不一样。举个例子,用户手动切换时,通常需要弹个确认对话框;但定时任务触发时,直接静默切换就行,别打扰用户睡觉。

我的习惯:把触发源也作为状态机的一个输入参数记录下来。这样调试时,一看日志就知道「哦,是定时器让它飞的,不是用户手滑」。这个习惯帮我省了不少排查时间。

状态保持机制:别让状态「乱跑」

状态保持,说白了就是「状态一旦确定,就别轻易变」。但现实很骨感——系统重启、应用被杀、配置被篡改,哪个都能让状态乱掉。

我一般用三层保障:

层级 存储方式 恢复时机
第一层 内存变量(最快) 应用运行时
第二层 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,用户再也打不开飞行模式了。回滚虽然不能解决问题,但至少让系统回到一个可用状态。

状态机流程图

下面这张图把三个状态和它们之间的切换关系画清楚了。我建议你把它贴在工位旁边,写代码时瞄一眼,不容易乱。

飞行模式状态机切换图 飞行模式关闭 FLIGHT_MODE_OFF 飞行模式开启 FLIGHT_MODE_ON 切换中 FLIGHT_MODE_TRANSITIONING 用户开启 / 定时任务 用户关闭 / 定时任务 切换成功(关闭无线) 切换成功(开启无线) 切换失败 → 回滚到之前状态 说明: 切换中状态不接受新请求 失败时自动回滚到 previousState

状态保持的「防抖」设计

最后聊一个容易被忽视的点:防抖。用户快速点击开关时,状态机可能在一秒内收到十几次切换请求。如果不做防抖,无线模块会被反复开关,轻则耗电,重则烧硬件。

我的做法是加一个「最小切换间隔」:

// 防抖参数:两次切换之间至少间隔 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 是个比较合理的值,既防抖又不影响体验。

好了,状态机的设计、切换条件、保持机制,这三块拼起来就是飞行模式的核心骨架。下一块咱们要聊的是「无线模块的开关顺序与依赖关系」——嗯,这部分才是真正考验细节的地方。