☰
Android WiFi关闭流程源码深度解析:从WifiManager到HAL层
2026/10/6 3:47:47 网站建设 项目流程

做Android系统开发的同学对WiFi的开启流程应该都不陌生,网上相关的源码分析文章也不少。但“关闭”这个方向,能说清楚的文章就少多了。其实做系统定制、做车机、做企业设备管理的时候,关闭流程比开启流程更容易遇到奇怪问题——比如明明调用了WifiManager.setWifiEnabled(false),通知栏图标迟迟不变;或者关闭后系统还在偷偷扫描;再或者跟热点、飞行模式一叠加,整个WiFi状态直接卡住。这篇文章就基于我实际跟过的代码,把从上层WifiManager一路到HAL的关闭链路完整拆一遍,把我自己踩过的坑也一并交代清楚。

需要提前说明的是,Android WiFi的代码在不同版本里变化非常大,我这里的分析以Android 10之后、Android 14之前的主流架构为准。这个区间内WiFi模块已经完成了从WifiStateMachine一家独大到WifiController+WifiStateMachine+WifiNative+WifiVendorHal分层协作的演进,关闭流程的主体框架是稳定的。太老的Android 8/9和更新的Android 15(WiFi相关代码向模块化迁移更大),我会在关键差异处单独标注。

1. 关闭请求的入口:从 WifiManager 到 WifiServiceImpl

1.1 应用层调用与Binder链路

大多数上层应用关闭WiFi,调用的都是这一行:

WifiManager wifiManager = context.getSystemService(WifiManager.class); boolean result = wifiManager.setWifiEnabled(false);

这个接口看起来简单,但从应用进程到系统进程,一共跨越了两次Binder调用。WifiManager本身是应用框架层的代理类,真正的逻辑在系统进程的WifiServiceImpl里。WifiManager.setWifiEnabled的代码大致是:

@Override public boolean setWifiEnabled(boolean enabled) { try { return mService.setWifiEnabled(mPackageName, enabled); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } }

这里的mService是IWifiManager的Binder代理,接口实现在系统Server端的WifiServiceImpl。第一次Binder调用到这里就结束了,WifiServiceImpl会做权限检查、前置状态校验,然后真正的工作通过消息机制交给WiFi内部的状态机去执行。

这里有个很多初学者会忽略的点:setWifiEnabled返回值只代表“请求被系统接受了”,不代表WiFi已经关掉了。系统真正完成关闭动作需要几百毫秒甚至几秒,中间涉及Supplicant退出、驱动停止、网络注销等一系列操作。如果你在上层业务里用这个返回值判断“WiFi已经关了”,那大概率会踩坑。

1.2 WifiServiceImpl 的权限检查与前置条件

WifiServiceImpl.setWifiEnabled的完整逻辑比很多人想象的复杂。它不仅仅是发个消息,而是有一连串的检查。关键代码逻辑如下:

public boolean setWifiEnabled(String packageName, boolean enable) { if (enforceChangePermission(packageName) != MODE_ALLOWED) { return false; } if (mAirplaneModeObserver.isAirplaneModeOn()) { // 飞行模式打开时,直接拒绝或记录 } if (!mSettingsStore.isWifiToggleEnabled() && enable) { // 更新用户Toggle设置 mSettingsStore.setWifiToggleState(enable); } mWifiController.sendMessage(CMD_WIFI_TOGGLED); return true; }

第一个重点是权限检查。setWifiEnabled需要CHANGE_WIFI_STATE权限,但实际执行时还要区分调用方身份。系统应用、预置应用和普通第三方应用的待遇完全不同。尤其从Android 13开始,setWifiEnabled对第三方应用的限制大幅收紧,普通应用在后台或者targetSdk较高时调用,几乎必然被拒绝。如果你在处理系统级App或者车机方案,这块要特别注意——很多设备厂商在定制时都会在这里打补丁,把某些白名单应用放行。

第二个重点是飞行模式。飞行模式下WiFi控制器会进入受限状态,此时即使来了CMD_WIFI_TOGGLED消息,WifiController内部也会根据飞行模式的开关状态决定是否真正执行关闭动作。早期Android版本里存在飞行模式和WiFi状态互相干扰的bug,就是因为在WifiController里对这两个状态的处理不够严谨。

第三个重点是mSettingsStore.setWifiToggleState。这行代码写入了用户的WiFi开关偏好,也就是设置里那个“WiFi”开关的持久化状态。注意这里的逻辑是:只有从关到开的时候才写入,从开到关则不写。这个设计初看反直觉,其实是因为Android要区分“用户主动关闭”和“系统临时停用”两种情况。比如短按飞行模式触发的WiFi关闭,就不应该覆盖用户之前设置“WiFi保持开启”的偏好。

2. WifiController 状态机:整个关闭过程的“调度中枢”

如果你跟过WiFi源码,一定会发现整个WiFi框架里状态机无处不在。WifiController是WiFi模块的顶层状态机,负责协调WiFi开关、热点、P2P、扫描等大方向的行为。关闭流程的主要调度就发生在这里。

2.1 CMD_WIFI_TOGGLED 消息的流转

WifiServiceImpl.setWifiEnabled发出去的CMD_WIFI_TOGGLED消息,最终会被WifiController的处理线程收走。WifiController本身继承自StateMachine,默认运行在“WiFi服务线程”上。消息收进来后,会根据当前所处的状态决定如何处理。

WifiController的几个核心状态包括StaEnabledState(WiFi完全开启)、StaDisabledWithScanState(WiFi关闭但允许扫描)、StaDisabledState(WiFi完全关闭)、ApEnabledState(热点开启)、EcmState(紧急呼叫模式)等。不同状态的processMessage里对CMD_WIFI_TOGGLED的处理逻辑完全不同。

这里最核心的一点是:关闭WiFi这个动作,最终要由StaEnabledState自己来裁定。也就是说,WiFi当前处于开启状态,收到关闭指令后,StaEnabledState需要决定自己接下来转到哪个状态。代码逻辑大致如下:

class StaEnabledState extends State { @Override public boolean processMessage(Message msg) { switch (msg.what) { case CMD_WIFI_TOGGLED: if (mSettingsStore.isWifiToggleEnabled()) { // 用户偏好显示WiFi仍要开启,不处理 } else if (mSettingsStore.isScanAlwaysAvailable()) { transitionTo(mStaDisabledWithScanState); } else { transitionTo(mDisabledState); } break; ... } } }

看到没有,“关闭WiFi”在状态机层面其实是一个状态迁移动作。CMD_WIFI_TOGGLED消息本身只是触发器,真正决定去哪个状态的,是当前用户偏好和系统策略的组合。

2.2 EnabledState → DisablingState → DisabledState

从StaEnabledState迁出后,WifiController会先进入DisablingState。这是一个中间过渡状态,它的enter()就干了一件事:通知WifiStateMachine停止Supplicant。

class DisablingState extends State { @Override public void enter() { mWifiStateMachine.setSupplicantRunning(false); } @Override public boolean processMessage(Message msg) { switch (msg.what) { case WifiStateMachine.WIFI_STATE_CHANGED: if (msg.arg1 == WifiManager.WIFI_STATE_DISABLED) { transitionTo(mDisabledState); } break; } return HANDLED; } }

这段代码值得多敲两行理解一下。DisablingState进入后立刻发起Supplicant停止请求,然后自己不再做实质工作,而是等WifiStateMachine上报WIFI_STATE_CHANGED事件。只有当WifiStateMachine那边确认WiFi已经完全关闭(WIFI_STATE_DISABLED),DisablingState才会带着这个结果迁入DisabledState。

这样设计的好处是状态机之间的解耦非常干净:WifiController只管“什么时候关”、“关完进哪个状态”,真正的关闭执行细节全在WifiStateMachine和它下游的WifiNative里,上层不关心。坏处就是排查问题的时候链路很长,一个问题要跨三四个模块追。

2.3 ScanAlwaysAvailable 带来的分支

如果你的设备开启了“随时可扫描”(scan_always_available),关闭流程会走另一条分支——进入StaDisabledWithScanState而不是DisabledState。这个状态很特殊:WiFi整体是关着的,用户也看到图标是灰的,但底层扫描功能仍然工作。

class StaDisabledWithScanState extends State { @Override public void enter() { mWifiStateMachine.setOperationalMode(ScanOnlyMode); mWifiStateMachine.setSupplicantRunning(true); } }

注意这里,setSupplicantRunning(true)又被调用了。也就是说,在这种场景下Supplicant不是被停止,而是被降级为“只扫描不连接”的模式。很多厂商在定制系统时会把这个功能关掉,因为用户经常反馈“WiFi关了还在被扫描”,隐私上说不清楚。但从系统框架角度讲,这个设计是为了支持基于WiFi扫描的位置服务——Andoid的位置服务依赖WiFi扫描结果,即使WiFi关闭,系统也可以只扫描不关联。

在StaDisabledWithScanState下,用户虽然不能连接任何WiFi,但系统可以持续拿到周围的BSSID列表用于定位。如果你的产品对功耗或者隐私有要求,关闭这个特性的落点就在WifiController的StaEnabledState处理逻辑里,把对mSettingsStore.isScanAlwaysAvailable()的判断强行置为false即可。

3. 真正的断连:WifiNative 与 HAL 层做了什么

进入DisablingState之后,关闭动作从“调度”进入了“执行”。所有脏活累活都在WifiStateMachine和WifiNative这一层完成。

3.1 WifiStateMachine 的 setSupplicantRunning(false)

WifiStateMachine收到CMD_SET_SUPPLICANT_RUNNING消息并携带false参数时,会进入SupplicantStoppingState。这个状态做的事情很直白:让WifiNative把Supplicant停掉。但停掉之前,WifiStateMachine要先做一系列连接清理工作。

第一个清理的是网络连接。如果当前WiFi已经连上某个AP,WifiStateMachine需要先把这条连接断开,触发L2ConnectedState退出,回到DisconnectedState。如果当前正在做DHCP或者正在认证,也要分别做超时取消和认证中断。

第二个清理的是底层接口。WifiStateMachine会调用mWifiNative.teardownInterface(),把WiFi网络接口(一般是wlan0)从框架层摘除。这个动作直接对接到Vendor HAL。

这段逻辑在排查问题时要特别留意:Supplicant的停止和接口的teardown是两件独立的事,但正常情况下它们必须按顺序完成。在我处理过的一个车机项目里,vendor驱动对teardown接口的响应特别慢,导致WifiStateMachine一直卡在SupplicantStoppingState,最后上层超时,整个WiFi服务被重启了一遍。

3.2 teardownInterface 与 driver/supplicant 的退出

WifiNative.teardownInterface()是Java层和Native层的一个关键分水岭。到这一步,调用链开始进入HAL层。

public void teardownInterface() { if (mIfaceIsUp) { mWifiVendorHal.teardownIface(mClientInterfaceName, mIfaceType); mIfaceIsUp = false; } mWifiSupplicantControl.teardownSupplicantIfNecessary(); mWifiCondControl.teardownInterfaces(); }

先看mWifiVendorHal.teardownIface。WifiVendorHal是WiFi HAL的封装层,Android 10之后通过HIDL(新版是AIDL)和wifi.hal服务通信。teardownIface底层调用的是IWifi.stop()或者按接口实例执行remove操作。这一步做完,驱动层面的WiFi接口就被移除,数据通路彻底断开。

再往下是Supplicant。mWifiSupplicantControl.teardownSupplicantIfNecessary()做的事情是:如果已经没有其他接口依赖Supplicant,就让wpa_supplicant进程退出。注意这里“IfNecessary”的考量——Android里Supplicant不是只服务一个接口的,它同时管着STA、P2P等。如果P2P接口还活着,Supplicant就不能直接杀掉,只能先把STA接口从Supplicant里摘掉。

最后mWifiCondControl.teardownInterfaces()是清理wificond进程的。wificond是Android 8之后引入的独立进程,负责处理NL80211相关的内核通信。关闭WiFi时,它监听的接口事件也要一并移除,否则内核侧还有残留的接口引用,下次开启时可能出现“interface already exists”之类的错误。

3.3 超时与清理机制

看了上面这几步,你可能已经意识到:关闭WiFi不是一个瞬时动作,而是一系列Native操作的组合。任何一个环节卡住,都会导致状态机停滞。WifiStateMachine用了一个很原始但有效的机制来兜底——消息超时。

SupplicantStoppingState在进入时会发送一个延迟消息:

sendMessageDelayed(SUPPLICANT_STOP_TIMEOUT, SUPPLICANT_STOP_TIMEOUT_MS);

如果Supplicant停止流程超过预期时间(通常是几秒),超时消息就会被处理,WifiStateMachine强制把状态推到WIFI_STATE_DISABLED。这个兜底保证了上层用户至少能看到WiFi图标熄灭,但代价是底层可能残留异常状态,比如Supplicant进程没杀掉、接口没释放干净等。

这里有一个我在项目里真实遇到过的坑:某个厂商的WiFi驱动在异常断电后,HAL层重启了但驱动固件没有加载完整,teardownIface调用后HAL返回成功,但实际上接口的引用计数没减干净。结果就是WiFi能正常关闭一次,但第二次开启时wlan0就起不来了,log里报Supplicant start failed。最后是让vendor在HAL层做了接口存在性检查才解决。

4. 系统级联动:关闭WiFi不只是“自己的事”

很多做上层应用的同学会有个误解:关闭WiFi就是系统WiFi模块内部的事,跟其他服务没关系。实际上,WiFi关闭会牵动ConnectivityService、BatteryStatsService、LocationManager、SystemUI等一大票系统服务。任何一个环节联动出问题,用户看到的就是“WiFi图标不消失”、“网络突然卡一下”这类诡异现象。

4.1 ConnectivityService 与 NetworkAgent 的注销

WifiStateMachine内部维护着一个NetworkAgent对象,它负责向ConnectivityService注册WiFi网络。连接成功时,NetworkAgent会把网络的能力、链路属性等同步给ConnectivityService;关闭时,则要把这个网络注销掉。

网络注销的触发点在断开连接流程中,大致调用链是:

mNetworkAgent.unregister();

ConnectivityService收到NetworkAgent的注销后,会做默认网络切换的评估。如果设备当前只有WiFi一个网络,注销后默认网络变为空,所有依赖网络的业务会立刻收到CONNECTIVITY_ACTION广播和NetworkCallback.onLost()回调。如果设备同时有蜂窝网络,这里还会触发一次网络切换,把数据链路从WiFi迁到蜂窝。这个切换过程如果出现竞争条件,就会出现用户感知的“断网几秒钟”。

排查这类问题有个经验:抓log时不能只看WiFi相关tag,还要看ConnectivityService的日志。很多WiFi关闭后的“疑似网络问题”,根因其实在ConnectivityService的网络评估和切换逻辑里,而不是WiFi模块本身。

4.2 电池统计、通知栏与UI刷新

WiFi关闭涉及的用户可见变化,主要是通知栏图标。这个变化的链路是:WifiStateMachine将状态置为WIFI_STATE_DISABLED后,WifiServiceImpl会发送WIFI_STATE_CHANGED_ACTION广播,SystemUI的WifiIcons相关代码收到广播后刷新图标。

广播是异步的,所以从“底层WiFi真正关闭”到“通知栏图标变化”,中间存在一个时间窗口。这个窗口通常只有几十毫秒,但如果SystemUI进程卡顿或者广播队列堆积,用户就会看到WiFi已经断了但图标还是满格信号——这种问题上报到厂商那边,十有八九最后定位到SystemUI的消息队列或者广播接收器的ANR上。

再看看电池统计。WifiServiceImpl在状态变化时会调用BatteryStatsService:

mBatteryStats.noteWifiOff();

这行代码虽然只是记录一个统计点,但它的作用很实际:电量统计页面里“WiFi使用时间”的计算就依赖这个标记。如果你在定制ROM时绕过WifiController直接操作底层关闭WiFi(比如某些省电策略直接kill了wpa_supplicant进程),电池统计就会失准,显示WiFi一直在开着耗电。

4.3 热点、P2P、飞行模式的交互

WiFi关闭流程跟热点和P2P的交互也是个重灾区。WifiController里StaEnabledState在处理CMD_WIFI_TOGGLED时,会先看当前有没有其他模块占着WiFi资源。比如热点开着的时候,框架层通常不允许直接关WiFi,因为softap和sta共用物理网卡。WifiController会把请求排队或者直接忽略,具体行为取决于vendor的实现。

还有飞行模式。飞行模式打开的时候,系统会强制关闭WiFi、蓝牙、蜂窝等所有无线通信。这个关闭动作不是走CMD_WIFI_TOGGLED,而是通过WifiController的另一个消息CMD_AIRPLANE_TOGGLED来触发的。两条路径最终都会走到WifiStateMachine.setSupplicantRunning(false),但前置处理逻辑完全不同。

我之前在一个平板上遇到过一个问题:打开飞行模式再关闭,WiFi就回不来了,必须重启才恢复。查了几天代码,最后发现是vendor的WiFi HAL在飞行模式关闭WiFi时没有释放一个内核锁,导致后续wlan0接口无法重新创建。这个问题的排查难点在于:从框架层看一切正常,日志里状态机流转全部正确,问题全在HAL层。所以排查WiFi关闭相关问题,一定要把framework层和HAL层日志同时抓取,对比着看。

5. 从源码到实战:常见关闭异常与排查思路

源码看完了,说点实际排查时能直接用上的东西。我把自己在多个项目里遇到的关闭流程问题归纳成几类,每类给出排查路径和定位建议。

5.1 关闭慢 / 卡在“正在关闭”

最常被反馈的问题就是WiFi关不掉,或者关了很久图标才灭。排查思路按调用链从下往上走。

第一步先看log里WifiStateMachine的SupplicantStoppingState是否长时间不退出。如果是,说明卡在Native层。这时候去看HAL层日志,重点确认teardownIface有没有调用、返回结果是什么。大多数卡住的情况都是vendor HAL对stop()或removeIfaceInstance的响应异常。

如果SupplicantStoppingState早就退出了,但WifiController的DisablingState一直收不到WIFI_STATE_DISABLED消息,问题可能在WifiServiceImpl的广播发送或者WifiStateMachine的状态上报逻辑上。这种场景下看wifi_state相关的状态变化日志,基本就能定位到是哪个环节把状态更新吞掉了。

还有一种常见的性能问题是:WiFi关闭慢,是因为网络栈在关之前还在做大量的DNS缓存清理、网络统计上报等工作。这部分工作在高通平台上尤其多。如果你不需要那么彻底的清理,可以在WifiStateMachine里找到对应的清理逻辑做裁剪,但这个改动要慎重,会影响网络统计和诊断功能的准确性。

5.2 关闭后仍能扫描 / 定位服务仍在用WiFi

这是隐私敏感产品上经常被挑战的一个点。用户明明把WiFi关了,但抓包发现设备还在发出Probe Request。这通常就是前文说的StaDisabledWithScanState。

Android默认在WiFi关闭后是否允许扫描,由Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE控制。但这里有个隐蔽的坑:不是只有这个开关控制扫描行为。如果你的系统集成了Google定位服务(GMS),或者使用了高通的Location扩展,即使这个开关关掉,定位服务仍然可能通过WifiManager.startScan()的接口触发单次扫描。

如果要彻底关闭“WiFi关闭后的所有扫描”,只改Settings开关是不够的,需要动WifiController,让WiFi关闭后直接进入DisabledState而不是StaDisabledWithScanState。同时还要检查一下WifiStateMachine里有没有其他入口能触发扫描,比如WifiConnectivityManager。这个管理器负责周期性的网络发现和连接恢复,如果它还在运行,即使状态机是DisabledState,也可能会因为定时任务去拉WiFi状态,间接触发底层扫描。

在Android 12之后,WifiConnectivityManager的行为受WifiTrafficPoller的影响,情况更复杂一些。建议在定制时直接把这个管理器的enable()入口在WiFi关闭时禁用掉。

5.3 setWifiEnabled 权限收紧与替代方案

Android 13之前,第三方应用只要持有CHANGE_WIFI_STATE权限就能调用setWifiEnabled。从Android 13开始,这个接口对普通应用基本关上了门,系统会检查调用方是否持有NETWORK_SETTINGS或MAINLINE_NETWORK_STACK这两个签名权限,没有就直接返回false。

如果你的业务是设备管理类App,需要远程关闭WiFi,现在官方推荐的替代方案是用DevicePolicyManager,因为设备所有者App本身有setWifiDisabled的权限。如果是普通App要做“省电时关闭WiFi”,更合适的做法是引导用户去系统设置里关,或者申请ACCESS_WIFI_STATE+CHANGE_WIFI_STATE后在Android 12L以下设备上处理,Android 13+则要考虑走辅助功能或者设备管理员通道。

从框架源码层面看,WifiServiceImpl.setWifiEnabled的权限检查逻辑如下:

private int enforceChangePermission(String packageName) { if (mAppOps.noteOp(AppOpsManager.OP_CHANGE_WIFI_STATE, ...) != MODE_ALLOWED) { return MODE_IGNORED; } if (checkCallerHasPermission(android.Manifest.permission.CHANGE_WIFI_STATE)) { return MODE_ALLOWED; } if (checkCallerHasPermission(android.Manifest.permission.NETWORK_SETTINGS)) { return MODE_ALLOWED; } return MODE_DENIED; }

这里可以看到,NETWORK_SETTINGS这个隐藏权限被当成了高优先级通行证。系统应用如果要在Android 13+设备上继续用setWifiEnabled,必须在Manifest里声明android:sharedUserId="android.uid.system"(虽然这个方法已经被标记废弃)或者直接持有NETWORK_SETTINGS签名权限。

5.4 关闭流程中的日志抓取要点

最后给一份排查关闭问题的log抓取清单,按效率排序:

日志分类关键tag主要看什么
WiFi框架wifi.WifiController状态机迁移是否正常,是否卡在DisablingState
WiFi状态机wifi.WifiStateMachineSupplicant停止、接口teardown、状态上报
WiFi Nativewifi.WifiNative、wificondteardownInterface是否执行,Supplicant是否退出
HAL层wifi.hal、wifi_vendorteardownIface返回,驱动响应时间
系统服务ConnectivityServiceNetworkAgent注销、默认网络切换
上层UISystemUI、WifiIcons图标刷新是否及时

抓log的时候,建议用logcat -b all把所有buffer都拉出来,因为WiFi关闭问题往往涉及多个进程和多个buffer。只抓mainbuffer经常漏掉system和events里的关键信息。

另外补充一个小技巧:复现问题前先执行adb shell cmd wifi status记录一下当前WiFi状态,复现后再执行一次,对比状态机的变迁。这个命令在Android 10+上非常有用,比我见过很多开发者在代码里打一堆临时log效率高得多。

我在多个项目里跟过WiFi关闭流程,整体的感受是:这套框架的清晰度在Android系统各个模块里算中等偏上,分层合理、状态机逻辑严谨,但正因为它跨的层太多,很多问题从表面上看是指向南辕北辙的方向的。遇到WiFi关闭相关的bug,别急着怀疑某一行代码,先从WifiController的状态机入手,顺着CMD_WIFI_TOGGLED的消息流一路捋下去,把每个状态迁移的触发条件都确认一遍,大多数问题都能在这个过程里露出原形。这也算是我在源码里泡了几年之后总结出来的最实用的方法论了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询