美文网首页
Wifi扫描流程解析

Wifi扫描流程解析

作者: 健身营养爱好者 | 来源:发表于2023-10-12 16:10 被阅读0次

    前言

    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各种状态是如何切换的,发送了一个消息该由哪个状态去执行。走读源码是深入学习的必经之路,阅读安卓源码的过程,也是学习设计模式的过程,比如谷歌为什么这么设计,为什么需要这么多的类,它是如何做到面向抽象,保持类的功能单一的,这些都值得我们去掌握。感谢大家的阅读,我们下周再见。

    相关文章

      网友评论

          本文标题:Wifi扫描流程解析

          本文链接:https://www.haomeiwen.com/subject/zfbaydtx.html