前言
HI,欢迎来到《每周一博》。今天是十二月第一周,我给大家介绍一下安卓系统WiFi的扫描过程。
一. 痛点
为什么要走读Wifi源码,因为定位离不开Wifi,之前在解答问题的时候,总是会有用户报无法获取Wifi的问题,包括发起Wifi扫描,获取Wifi结果和系统缓存的一系列问题,所以我决定带着这些疑问去看一下Wifi的基本流程到底是什么样的,如何发起了扫描,获得结果什么时候用的是缓存,究竟是否有3分钟清缓存的限制。
二. 状态机
Wifi的工作过程使用了状态机,为了了解Wifi工作过程,我特意先学习了一下状态机,否则真不懂,关于状态机我写了一篇文章《状态机工作原理》来介绍。
这里再简单介绍下帮助理解,状态机主要用到了状态模式,不同的状态行为也不同。状态树有很多节点,子节点继承自父亲节点,状态都有enter,exit,processMessage方法,如果当前状态不处理消息,就会由父状态去处理。状态机有deferMessage,transitTo,sendMessage方法,分别是把消息推迟到下一状态执行,切换到某个状态,发送消息方法。状态机内部是通过Handler来实现发送接收消息的。
在WifiStateMachine里面就有几十个状态机,每个状态机处理的消息类型和处理方法都不一样,这就需要搞清楚当前是什么状态,当前要处理什么消息,如何处理,处理完之后变成了什么状态。
三. Wifi上层架构图
我们和Wifi交互的入口就是WifiManager,它通过Binder机制和WifiService进行跨进程通讯,WifiService的具体实现是WifiServiceImpl,它内部有个重要的状态机WifiStateMachine,还有和底层交互的WifiNative类,这里面封装了一些命令,如doCommand(),和监听函数,如wifi_wait_for_event()。
和WifiNative交互的是wpa_supplicant,它是Linux上的一个开源项目,被谷歌修改后加入Android移动平台,用来支持WEP,WPA/WPA2和WAPI无线协议和加密认证,而实际上的工作内容是通过socket(不管是与上层还是与驱动)与驱动交互上报数据给用户,而用户可以通过socket发送命令给wpa_supplicant调动驱动来对WiFi芯片操作。 简单的说,wpa_supplicant就是WiFi驱动和用户的中转站外加对协议和加密认证的支持。
四. Wifi扫描
Wifi扫描的入口是WifiManager的startScan方法,它代理了mService的startScan方法,它们之间是通过Binder来传递消息的。
public boolean startScan(WorkSource workSource) {
try {
mService.startScan(workSource);
return true;
} catch (RemoteException e) {
return false;
}
}
先来看一下安卓4.4的实现,mService的实现是WifiService,它和WifiManger通过IWifiManager接口调用。
WifiService里有2个重要的状态机WifiControl,WifiStateMachine,当调用startScan时先回检查权限,然后执行WifiStateMachine的startScan方法。
public void startScan(WorkSource workSource) {
enforceChangePermission();
if (workSource != null) {
enforceWorkSourcePermission();
workSource.clearNames();
}
mWifiStateMachine.startScan(Binder.getCallingUid(), workSource);
}
在WifiStateMachine的构造函数里,初始化了mWifiNative和mWifiMonitor对象,构建了状态树,设置初始状态并开启状态机;
mWifiNative = new WifiNative(mInterfaceName);
mWifiMonitor = new WifiMonitor(this, mWifiNative);
addState(mDefaultState);
addState(mInitialState, mDefaultState);
addState(mSupplicantStartingState, mDefaultState);
addState(mSupplicantStartedState, mDefaultState);
addState(mDriverStartingState, mSupplicantStartedState);
addState(mDriverStartedState, mSupplicantStartedState);
addState(mScanModeState, mDriverStartedState);
addState(mConnectModeState, mDriverStartedState);
addState(mL2ConnectedState, mConnectModeState);
addState(mObtainingIpState, mL2ConnectedState);
addState(mVerifyingLinkState, mL2ConnectedState);
addState(mCaptivePortalCheckState, mL2ConnectedState);
addState(mConnectedState, mL2ConnectedState);
addState(mDisconnectingState, mConnectModeState);
addState(mDisconnectedState, mConnectModeState);
addState(mWpsRunningState, mConnectModeState);
addState(mWaitForP2pDisableState, mSupplicantStartedState);
addState(mDriverStoppingState, mSupplicantStartedState);
addState(mDriverStoppedState, mSupplicantStartedState);
addState(mSupplicantStoppingState, mDefaultState);
addState(mSoftApStartingState, mDefaultState);
addState(mSoftApStartedState, mDefaultState);
addState(mTetheringState, mSoftApStartedState);
addState(mTetheredState, mSoftApStartedState);
addState(mUntetheringState, mSoftApStartedState);
setInitialState(mInitialState);
start();
WifiStateMachine构建的状态机是这样的,每一个状态要切换到另一状态都需要走过该树上的所有相关节点,而不能直接跨越。比如从mInitialState状态切换到mDriverStartedState需要经历mDefaultState,mSupplicantStartedState,mDriverStartedState三个状态,接下来我们看WifiStateMachine的startScan方法。
sendMessage(CMD_START_SCAN, callingUid, 0, workSource);
只是发送了一个指令CMD_START_SCAN,那我们看一下不同状态对该指令的行为是什么;
DefaultState:break(不执行)
DriverStartingState:deferMessage(推迟到下一状态)
ObtainingIpState:deferMessage(推迟到下一状态)
在DriverStartedState中处理该消息
noteScanStart(message.arg1, (WorkSource) message.obj);
startScanNative(WifiNative.SCAN_WITH_CONNECTION_SETUP);
在ScanModeState中处理该消息
noteScanStart(message.arg1, (WorkSource) message.obj);
startScanNative(WifiNative.SCAN_WITHOUT_CONNECTION_SETUP);
都是调用startScanNative方法,只是参数不同而已,noteScanStart用于通知电量统计,startScanNative会调用 mWifiNative.scan(type),向wpa_supplicant发送SCAN的命令,至此一条发起wifi扫描的请求就走完了,那么扫描完后得到结果如何通知的呢?
可以想象这个过程是异步的,不是一发起扫描就会立刻得到结果,所以需要有一个监听器不断的去监听事件。在WifiMonitor里面有一个MonitorThread线程在不断的监听WifiNative上报的事件,这是个无限循环,当接收到事件时会做解析,然后根据不同的类型去调用dispatchEvent(eventStr)来分发事件。
for (;;) {
String eventStr = mWifiNative.waitForEvent();
}
当事件类型是扫描结果时,会执行handleEvent方法,在这里把消息发出去;
case SCAN_RESULTS:
mStateMachine.sendMessage(SCAN_RESULTS_EVENT);
break;
WifiStateMachine的SupplicantStartedState会处理SCAN_RESULTS_EVENT这个消息,它会做两件事,一是去获取scanResults,二是发送一个广播消息;
setScanResults();
sendScanResultsAvailableBroadcast();
setScanResult主要就是把从WifiNative获取到AP列表信息进行循环解析,然后赋值给系统缓存mScanResultCache和结果列表mScanResults, mScanResultCache使用了LRUCache,它以bssid+ssid做key值。
private void setScanResults() {
while (true) {
tmpResults = mWifiNative.scanResults(sid);
if (TextUtils.isEmpty(tmpResults)) break;
scanResultsBuf.append(tmpResults);
scanResultsBuf.append("\n");
String[] lines = tmpResults.split("\n");
sid = -1;
for (int i=lines.length - 1; i >= 0; i--) {
if (lines[i].startsWith(END_STR)) {
break;
} else if (lines[i].startsWith(ID_STR)) {
try {
sid = Integer.parseInt(lines[i].substring(ID_STR.length())) + 1;
} catch (NumberFormatException e) {
// Nothing to do
}
break;
}
}
if (sid == -1) break;
}
synchronized(mScanResultCache) {
mScanResults = new ArrayList<ScanResult>();
String[] lines = scanResults.split("\n");
final int bssidStrLen = BSSID_STR.length();
final int flagLen = FLAGS_STR.length();
for (String line : lines) {
if (line.startsWith(BSSID_STR)) {
bssid = new String(line.getBytes(), bssidStrLen, line.length() - bssidStrLen);
} else if (line.startsWith(FREQ_STR)) {
try {
freq = Integer.parseInt(line.substring(FREQ_STR.length()));
} catch (NumberFormatException e) {
freq = 0;
}
……………………………………
} else if (line.startsWith(SSID_STR)) {
wifiSsid = WifiSsid.createFromAsciiEncoded(
line.substring(SSID_STR.length()));
} else if (line.startsWith(DELIMITER_STR) || line.startsWith(END_STR)) {
if (bssid != null) {
String ssid = (wifiSsid != null) ? wifiSsid.toString() : WifiSsid.NONE;
String key = bssid + ssid;
ScanResult scanResult = mScanResultCache.get(key);
if (scanResult != null) {
scanResult.level = level;
scanResult.wifiSsid = wifiSsid;
// Keep existing API
scanResult.SSID = (wifiSsid != null) ? wifiSsid.toString() :
WifiSsid.NONE;
scanResult.capabilities = flags;
scanResult.frequency = freq;
scanResult.timestamp = tsf;
} else {
scanResult =
new ScanResult(
wifiSsid, bssid, flags, level, freq, tsf);
mScanResultCache.put(key, scanResult);
}
mScanResults.add(scanResult);
}
}
}
}
}
WifiNative.scanResut的返回结果格式如下,每个AP之间用"===="分割,末尾以“####”来表示结束。
id=1
bssid=68:7f:76:d7:1a:6e
freq=2412
level=-44
tsf=1344626243700342
flags=[WPA2-PSK-CCMP][WPS][ESS]
ssid=zfdy
====
id=2
bssid=68:5f:74:d7:1a:6f
req=5180
level=-73
tsf=1344626243700373
flags=[WPA2-PSK-CCMP][WPS][ESS]
ssid=zuby
####
这里对结果ScanResult做个说明,它描述了AP的信息,具体包含以下字段;
A. String SSID:网络名称
B. String BSSID:WiFi的mac地址,也是唯一id
C. String capabilities:描述认证、密钥管理以及加密方式
D. int level:信号等级,单位是dBm,也被称作RSSI,一般是个负数,越大信号越强,比如-50强过-100
E. int frequency:客户端与WiFi通信的频率,单位MHz,如果频率值在2400-2500之间是2.4GHz,如果频率值在4900-5900之间是5GHz
F. long timestamp:从启动开始到该扫描记录最后一次被发现经过的微秒数。
接下来就是去发广播了,广播的Action是WifiManager.SCAN_RESULTS_AVAILABLE_ACTION,然后通过getScanResults去取mScanResults信息就可以了,需要注意的是这里的广播设置了Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT这个属性,所以只有动态注册的broadcastReceive才会收到广播。
private void sendScanResultsAvailableBroadcast() {
noteScanEnd();
Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
至此一个发起wifi扫描的流程就算是走完了,从6.0开始发送结果的广播增加了EXTRA_RESULTS_UPDATED新字段,如果是true表示结果可用。
private void sendScanResultBroadcast(boolean scanSucceeded) {
Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, scanSucceeded);
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
所以我们在监听广播的时候先判断下系统版本,如果高于M,取出EXTRA_RESULTS_UPDATED字段,如果为true,再取结果。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
boolean hasResult=intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, false);
if (hasResult){
List<ScanResult> results = wifiManager.getScanResults();
}
}
一个安卓4.4发起wifi扫描的流程如下图;
另外8.0的扫描流程有了很大的变化,主要功能都是由WifiScanner来完成的,它的具体实现是WifiScanningServiceImpl,当调用WifiStateMachine的startScanNative时会走到WifiScanner的startScan方法。
private boolean startScanNative(final Set<Integer> freqs,
List<WifiScanner.ScanSettings.HiddenNetwork> hiddenNetworkList,
WorkSource workSource) {
WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
if (freqs == null) {
settings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;
} else {
settings.band = WifiScanner.WIFI_BAND_UNSPECIFIED;
int index = 0;
settings.channels = new WifiScanner.ChannelSpec[freqs.size()];
for (Integer freq : freqs) {
settings.channels[index++] = new WifiScanner.ChannelSpec(freq);
}
}
settings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN
| WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
settings.hiddenNetworks =
hiddenNetworkList.toArray(
new WifiScanner.ScanSettings.HiddenNetwork[hiddenNetworkList.size()]);
WifiScanner.ScanListener nativeScanListener = new WifiScanner.ScanListener() {
// ignore all events since WifiStateMachine is registered for the supplicant events
@Override
public void onSuccess() {
}
@Override
public void onFailure(int reason, String description) {
mIsScanOngoing = false;
mIsFullScanOngoing = false;
}
@Override
public void onResults(WifiScanner.ScanData[] results) {
}
@Override
public void onFullResult(ScanResult fullScanResult) {
}
@Override
public void onPeriodChanged(int periodInMs) {
}
};
mWifiScanner.startScan(settings, nativeScanListener, workSource);
mIsScanOngoing = true;
mIsFullScanOngoing = (freqs == null);
lastScanFreqs = freqs;
return true;
}
WifiScanner的startScan方法里会通过mAsyncChannel发送了一个CMD_START_SINGLE_SCAN的消息;
public void startScan(ScanSettings settings, ScanListener listener, WorkSource workSource) {
Preconditions.checkNotNull(listener, "listener cannot be null");
int key = addListener(listener);
if (key == INVALID_KEY) return;
validateChannel();
Bundle scanParams = new Bundle();
scanParams.putParcelable(SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
scanParams.putParcelable(SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
mAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, key, scanParams);
}
WifiScanningServiceImpl的ClientHandler接受到该消息后会调用状态机发消息的方法。
mSingleScanStateMachine.sendMessage(Message.obtain(msg));
WifiSingleScanStateMachine状态接收到消息后,会执行关键的方法tryToStartNewScan;
case WifiScanner.CMD_START_SINGLE_SCAN:
mWifiMetrics.incrementOneshotScanCount();
int handler = msg.arg2;
Bundle scanParams = (Bundle) msg.obj;
if (scanParams == null) {
logCallback("singleScanInvalidRequest", ci, handler, "null params");
replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "params null");
return HANDLED;
}
scanParams.setDefusable(true);
ScanSettings scanSettings =
scanParams.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
WorkSource workSource =
scanParams.getParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY);
if (validateScanRequest(ci, handler, scanSettings, workSource)) {
logScanRequest("addSingleScanRequest", ci, handler, workSource,
scanSettings, null);
replySucceeded(msg);
// If there is an active scan that will fulfill the scan request then
// mark this request as an active scan, otherwise mark it pending.
// If were not currently scanning then try to start a scan. Otherwise
// this scan will be scheduled when transitioning back to IdleState
// after finishing the current scan.
if (getCurrentState() == mScanningState) {
if (activeScanSatisfies(scanSettings)) {
mActiveScans.addRequest(ci, handler, workSource, scanSettings);
} else {
mPendingScans.addRequest(ci, handler, workSource, scanSettings);
}
} else {
mPendingScans.addRequest(ci, handler, workSource, scanSettings);
tryToStartNewScan();
}
} else {
logCallback("singleScanInvalidRequest", ci, handler, "bad request");
replyFailed(msg, WifiScanner.REASON_INVALID_REQUEST, "bad request");
mWifiMetrics.incrementScanReturnEntry(
WifiMetricsProto.WifiLog.SCAN_FAILURE_INVALID_CONFIGURATION, 1);
}
return HANDLED;
而它又会调用startSingleScan,最终调到了WifiNative的scan方法,一个安卓8.0的wifi扫描流程图如下。
五. 获取Wifi结果
获取WiFi结果需要调用WifiManager的getScanResults方法,这个实现在8.0上也有所不同,我们先来看4.4的实现。
它最终会走到WifiStateMachine的syncGetScanResultsList方法,它的实现如下;
public List<ScanResult> syncGetScanResultsList() {
synchronized (mScanResultCache) {
List<ScanResult> scanList = new ArrayList<ScanResult>();
for(ScanResult result: mScanResults) {
scanList.add(new ScanResult(result));
}
return scanList;
}
}
上面说到扫描到wifi结果后会把结果存到全局变mScanResults中,所以这里直接把该变量里的值返回去,比较简单。
在8.0上调用getScanResults时,它调用的是WifiScanner的getSingleScanResults方法,它是用mAsyncChannel发送了一个CMD_GET_SINGLE_SCAN_RESULTS的消息;
public List<ScanResult> getSingleScanResults() {
validateChannel();
Message reply = mAsyncChannel.sendMessageSynchronously(CMD_GET_SINGLE_SCAN_RESULTS, 0);
if (reply.what == WifiScanner.CMD_OP_SUCCEEDED) {
return Arrays.asList(((ParcelableScanResults) reply.obj).getResults());
}
OperationResult result = (OperationResult) reply.obj;
Log.e(TAG, "Error retrieving SingleScan results reason: " + result.reason
+ " description: " + result.description);
return new ArrayList<ScanResult>();
}
WifiScanningServiceImpl的ClientHandler接受到该消息后会用状态机发消息,mSingleScanStateMachine.sendMessage(Message.obtain(msg)),接着WifiSingleScanStateMachine状态接收到消息后,执行filterCachedScanResultsByAge这个方法来填充数据;
case WifiScanner.CMD_GET_SINGLE_SCAN_RESULTS:
msg.obj = new WifiScanner.ParcelableScanResults(
filterCachedScanResultsByAge());
replySucceeded(msg);
return HANDLED;
我们来看一下filterCachedScanResultsByAge这个方法;
private ScanResult[] filterCachedScanResultsByAge() {
// Using ScanResult.timestamp here to ensure that we use the same fields
// as WificondScannerImpl for filtering stale results.
long currentTimeInMillis = mClock.getElapsedSinceBootMillis();
return mCachedScanResults.stream()
.filter(scanResult
-> ((currentTimeInMillis - (scanResult.timestamp / 1000))
< CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS))
.toArray(ScanResult[]::new);
}
mCachedScanResults存了每一次获得到扫描结果的数据,在每次发广播前进行赋值。CACHED_SCAN_RESULTS_MAX_AGE_IN_MILLIS是180秒,也就是3分钟,所以3分钟后不发起wifi扫描直接取结果的话得到的是空。
if (results.isAllChannelsScanned()) {
mCachedScanResults.clear();
mCachedScanResults.addAll(Arrays.asList(results.getResults()));
sendScanResultBroadcast(true);
}
接下来就是返回结果了
void replySucceeded(Message msg) {
if (msg.replyTo != null) {
Message reply = Message.obtain();
reply.what = WifiScanner.CMD_OP_SUCCEEDED;
reply.arg2 = msg.arg2;
if (msg.obj != null) {
reply.obj = msg.obj;
}
try {
msg.replyTo.send(reply);
mLog.trace("replySucceeded recvdMessage=%").c(msg.what).flush();
} catch (RemoteException e) {
// There's not much we can do if reply can't be sent!
}
} else {
// locally generated message; doesn't need a reply!
}
}
会发送一个CMD_OP_SUCCEEDED的消息,WifiScanner接收到该消息后会继续上抛,直到返给WifiManger。
所以8.0新加了一个3分钟的时间限制,即只返回3分钟内的缓存结果,这点在定制Wifi策略的时候需要考虑一下。
六. 总结
本文介绍了wifi扫描和获得结果的流程,我觉得wifi源码要比网络定位复杂一些,网络定位主要是两层client-server调用,而wifi主要是先得搞懂状态模式,状态机这些东西,这样才能知道wifi各种状态是如何切换的,发送了一个消息该由哪个状态去执行。走读源码是深入学习的必经之路,阅读安卓源码的过程,也是学习设计模式的过程,比如谷歌为什么这么设计,为什么需要这么多的类,它是如何做到面向抽象,保持类的功能单一的,这些都值得我们去掌握。感谢大家的阅读,我们下周再见。
网友评论