【安卓Framework学习】Wifi框架学习之核心类
【安卓Framework学习】Wifi框架学习之wifi状态机
【安卓Framework学习】Wifi框架学习之连接与断开流程
【安卓Framework学习】Wifi框架学习之扫描模式及扫描流程.
【安卓Framework学习】Wifi框架学习之开启与关闭流程.
【安卓Framework学习】安卓连接管理(ConnectivityService)之wifi连接及注册.
之前分析过wifi框架中的连接断开、开启关闭以及扫描相关的流程,本篇将继续分析wifi框架中隐藏较深的一个机制——热点的评分机制。在安卓wifi框架中,这一套机制实现了对设备周围的热点在一定规则下进行打分,然后根据分数高低决定是否需要连接。由于此机制中包含的规则较多,如有不正确之处望大家及时指出。文中出现的代码大部分基于android11源码。
接【安卓Framework学习】Wifi框架学习之开启与关闭流程.中wifi开启后ClientModeImpl
会进入到ConnectModeState
中,看看ConnectModeState.enter()
做了啥。
public void enter() {
/*省略部分代码*/
mWifiConnectivityManager.setWifiEnabled(true);
mNetworkFactory.setWifiState(true);
/*省略部分代码*/
}
进入状态的方法中调用了WifiConnectivityManager.setWifiEnabled()
方法,在看其中实现了什么内容。
public void setWifiEnabled(boolean enable) {
/*省略部分代码*/
mWifiEnabled = enable;
updateRunningState();
}
会调用到内部私有方法updateRunningState()
中,这个方法内部较为简单,这里假定是已经设置了需要自动连接的情况下,会继续进入到start()
方法中,如下。
private void start() {
if (mRunning) return;
retrieveWifiScanner();
/*省略部分代码*/
}
在retrieveWifiScanner()
方法中,想服务端注册了回调类WifiConnectivityManager.AllSingleScanListener
。
WifiConnectivityManager.java
private void retrieveWifiScanner() {
/*省略部分代码*/
mScanner.registerScanListener(new HandlerExecutor(mEventHandler), mAllSingleScanListener);
}
WifiScanner.java
public void registerScanListener(@NonNull @CallbackExecutor Executor executor,
@NonNull ScanListener listener) {
/*省略部分代码*/
int key = addListener(listener, executor);
if (key == INVALID_KEY) return;
validateChannel();
mAsyncChannel.sendMessage(CMD_REGISTER_SCAN_LISTENER, 0, key);
}
调用了WifiScanner.registerScanListener()
方法,在WifiScanner
中又通过异步通信将回调注册到WifiScanningServiceImpl中
,回调注册就到这结束了。更详细的回调注册和调用在【安卓Framework学习】Wifi框架学习之扫描模式及扫描流程中已经详细说明了,后面开始自动评分也是从回调方法中开始。
由上小结可以看出,在扫描结束后会通过调用注册的AllSingleScanListener
回调类的onResults()
方法,将扫描结果往上传给应用层,AllSingleScanListener.onResults()
方法如下。
public void onResults(WifiScanner.ScanData[] results) {
if (!mWifiEnabled || !mAutoJoinEnabled) {
clearScanDetails();
mWaitForFullBandScanResults = false;
return;
}
/*省略部分代码*/
boolean wasConnectAttempted = handleScanResults(mScanDetails,
ALL_SINGLE_SCAN_LISTENER, isFullBandScanResults);
clearScanDetails();
/*省略部分代码*/
}
在调用回调方法时,首先会去判断是否已经打开wifi和是否支持自动连接,如果其中一个条件不满足都不会进行接下来的自动连接操作。这里认定开启了wifi并且支持自动连接,那么接下来将会调用handleScanResults()
方法。
private boolean handleScanResults(List<ScanDetail> scanDetails, String listenerName,
boolean isFullScan) {
/*省略部分代码*/
/*热点筛选*/
List<WifiCandidates.Candidate> candidates = mNetworkSelector.getCandidatesFromScan(
scanDetails, bssidBlocklist, mWifiInfo, mStateMachine.isConnected(),
mStateMachine.isDisconnected(), mUntrustedConnectionAllowed);
mLatestCandidates = candidates;
mLatestCandidatesTimestampMs = mClock.getElapsedSinceBootMillis();
if (mDeviceMobilityState == WifiManager.DEVICE_MOBILITY_STATE_HIGH_MVMT
&& mContext.getResources().getBoolean(
R.bool.config_wifiHighMovementNetworkSelectionOptimizationEnabled)) {
candidates = filterCandidatesHighMovement(candidates, listenerName, isFullScan);
}
/*热点评分优选*/
WifiConfiguration candidate = mNetworkSelector.selectNetwork(candidates);
mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
mWifiLastResortWatchdog.updateAvailableNetworks(
mNetworkSelector.getConnectableScanDetails());
mWifiMetrics.countScanResults(scanDetails);
if (candidate != null) {
/*热点连接*/
localLog(listenerName + ": WNS candidate-" + candidate.SSID);
connectToNetwork(candidate);
return true;
} else {
/*省略部分代码*/
}
}
此方法包含了热点筛选、热点评分以及连接优选出来的热点等核心过程。其中热点筛选主要是通过wifi网络选择器WifiNetworkSelector
中的getCandidatesFromScan()
方法选择出候选热点列表。
public List<WifiCandidates.Candidate> getCandidatesFromScan(
List<ScanDetail> scanDetails, Set<String> bssidBlacklist, WifiInfo wifiInfo,
boolean connected, boolean disconnected, boolean untrustedNetworkAllowed) {
/*省略部分代码*/
WifiConfiguration currentNetwork =
mWifiConfigManager.getConfiguredNetwork(wifiInfo.getNetworkId());
String currentBssid = wifiInfo.getBSSID();
/*给当前给的扫描热点信息进行评分*/
for (NetworkNominator registeredNominator : mNominators) {
registeredNominator.update(scanDetails);
}
/*省略部分代码*/
/*筛选掉黑名单*/
mFilteredNetworks = filterScanResults(scanDetails, bssidBlacklist,
connected && wifiInfo.getScore() >= WIFI_POOR_SCORE, currentBssid);
if (mFilteredNetworks.size() == 0) {
return null;
}
WifiCandidates wifiCandidates = new WifiCandidates(mWifiScoreCard, mContext);
/*添加当前的wifi到WifiCandidates中*/
if (currentNetwork != null) {
wifiCandidates.setCurrent(currentNetwork.networkId, currentBssid);
// We always want the current network to be a candidate so that it can participate.
// It may also get re-added by a nominator, in which case this fallback
// will be replaced.
MacAddress bssid = MacAddress.fromString(currentBssid);
WifiCandidates.Key key = new WifiCandidates.Key(
ScanResultMatchInfo.fromWifiConfiguration(currentNetwork),
bssid, currentNetwork.networkId);
wifiCandidates.add(key, currentNetwork,
NetworkNominator.NOMINATOR_ID_CURRENT,
wifiInfo.getRssi(),
wifiInfo.getFrequency(),
calculateLastSelectionWeight(currentNetwork.networkId),
WifiConfiguration.isMetered(currentNetwork, wifiInfo),
isFromCarrierOrPrivilegedApp(currentNetwork),
0 /* Mbps */);
}
/*通过三个提名器,将其他热点筛选后添加到WifiCandidates中*/
for (NetworkNominator registeredNominator : mNominators) {
localLog("About to run " + registeredNominator.getName() + " :");
registeredNominator.nominateNetworks(
new ArrayList<>(mFilteredNetworks), currentNetwork, currentBssid, connected,
untrustedNetworkAllowed,
(scanDetail, config) -> {
WifiCandidates.Key key = wifiCandidates.keyFromScanDetailAndConfig(
scanDetail, config);
if (key != null) {
boolean metered = isEverMetered(config, wifiInfo, scanDetail);
// TODO(b/151981920) Saved passpoint candidates are marked ephemeral
boolean added = wifiCandidates.add(key, config,
registeredNominator.getId(),
scanDetail.getScanResult().level,
scanDetail.getScanResult().frequency,
calculateLastSelectionWeight(config.networkId),
metered,
isFromCarrierOrPrivilegedApp(config),
predictThroughput(scanDetail));
if (added) {
mConnectableNetworks.add(Pair.create(scanDetail, config));
mWifiConfigManager.updateScanDetailForNetwork(
config.networkId, scanDetail);
mWifiMetrics.setNominatorForNetwork(config.networkId,
toProtoNominatorId(registeredNominator.getId()));
}
}
});
}
/*省略部分代码*/
return wifiCandidates.getCandidates();
}
这里其实主要的还是筛选出非黑名单的热点以及确认是否添加不受信用的热点,其他筛选条件主要针对不同的提名器进行筛选。在安卓wifi框架中有三个提名器,SavedNetworkNominator
主要筛选当前已经保存过的热点信息,NetworkSuggestionNominator
主要筛选出一些被推荐的热点,ScoredNetworkNominator
筛选的是在过滤黑名单之前打过分的热点。这三个提名器的具体筛选规则较多,如有需要可以详细分析,这里不赘述。接下来继续回到handleScanResults()
方法中。在调用getCandidatesFromScan()
后得到一个新的候选热点列表,然后再调用WifiNetworkSelector.selectNetwork()
方法,选择出一个最后需要自动连接的热点。继续分析WifiNetworkSelector.selectNetwork()
方法。
public WifiConfiguration selectNetwork(List<WifiCandidates.Candidate> candidates) {
/*省略部分代码*/
WifiCandidates wifiCandidates = new WifiCandidates(mWifiScoreCard, mContext, candidates);
/*选出当前激活的评分器*/
final WifiCandidates.CandidateScorer activeScorer = getActiveCandidateScorer();
/*将候选热点按照networkId分组*/
Collection<Collection<WifiCandidates.Candidate>> groupedCandidates =
wifiCandidates.getGroupedCandidates();
/*每组id选出一个最优的,然后并更新WifiConfiguration的NetworkSelectionStatus*/
for (Collection<WifiCandidates.Candidate> group : groupedCandidates) {
WifiCandidates.ScoredCandidate choice = activeScorer.scoreCandidates(group);
if (choice == null) continue;
ScanDetail scanDetail = getScanDetailForCandidateKey(choice.candidateKey);
if (scanDetail == null) continue;
mWifiConfigManager.setNetworkCandidateScanResult(choice.candidateKey.networkId,
scanDetail.getScanResult(), 0);
}
/*省略部分代码*/
ArrayMap<Integer, Integer> experimentNetworkSelections = new ArrayMap<>(); // for metrics
int selectedNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
boolean legacyOverrideWanted = true;
/*通过遍历评分器,分别传给wifiCandidates进行评分选择,然后将当前激活的评分器选择出来的热点networkId保存下来*/
for (WifiCandidates.CandidateScorer candidateScorer : mCandidateScorers.values()) {
WifiCandidates.ScoredCandidate choice;
try {
/*将遍历出来的评分器传给wifiCandidates,由wifiCandidates调用评分器的scoreCandidates()方法对扫描结果列表进行选择*/
choice = wifiCandidates.choose(candidateScorer);
} catch (RuntimeException e) {
Log.wtf(TAG, "Exception running a CandidateScorer", e);
continue;
}
int networkId = choice.candidateKey == null
? WifiConfiguration.INVALID_NETWORK_ID
: choice.candidateKey.networkId;
String chooses = " would choose ";
/*然后将当前激活的评分器选择出来的热点networkId保存下来*/
if (candidateScorer == activeScorer) {
chooses = " chooses ";
legacyOverrideWanted = choice.userConnectChoiceOverride;
selectedNetworkId = networkId;
updateChosenPasspointNetwork(choice);
}
String id = candidateScorer.getIdentifier();
int expid = experimentIdFromIdentifier(id);
localLog(id + chooses + networkId
+ " score " + choice.value + "+/-" + choice.err
+ " expid " + expid);
experimentNetworkSelections.put(expid, networkId);
}
/*省略部分代码*/
/*根据前面由当前激活的评分器选择出来的networkId获得其WifiConfiguration然后返回*/
WifiConfiguration selectedNetwork =
mWifiConfigManager.getConfiguredNetwork(selectedNetworkId);
if (selectedNetwork != null && legacyOverrideWanted) {
selectedNetwork = overrideCandidateWithUserConnectChoice(selectedNetwork);
}
if (selectedNetwork != null) {
mLastNetworkSelectionTimeStamp = mClock.getElapsedSinceBootMillis();
}
return selectedNetwork;
}
此方法较长,涉及到的流程也比较多,首先会将扫描得到的结果根据networkId
进行分组打分,并更新相同networkId
组中评分最高的NetworkSelectionStatus
,然后再挨个遍历当前已有的评分器并传给WifiCandidates
让其调用scoreCandidates()
方法进行评分选择,但是由于评分器不止一个,而最后只能得出一个最优热点,所以在各个评分器选出自己评分最优的热点后,最后决定使用当前激活的评分器选出来的最优热点作为最终的热点用于自动连接。这里重点关注 wifiCandidates.choose()
方法,是用于评分选择最高分的热点。
public @NonNull ScoredCandidate choose(@NonNull CandidateScorer candidateScorer) {
/*省略部分代码*/
Collection<Candidate> candidates = new ArrayList<>(mCandidates.values());
ScoredCandidate choice = candidateScorer.scoreCandidates(candidates);
return choice == null ? ScoredCandidate.NONE : choice;
}
可以看到在WifiCandidates
中仍然是调用了评分器的scoreCandidates()
方法,所以直接看评分器的评分方法scoreCandidates()
。当前安卓wifi框架中已实现的评分器有CompatibilityScorer
、ScoreCardBasedScorer
、BubbleFunScorer
、ThroughputScorer
这四种,分别表示这不同的评分策略,但是四种评分器都是基于热点信号和热点频率的前提下进行的有策略的倾斜评分。
public ScoredCandidate scoreCandidates(@NonNull Collection<Candidate> candidates) {
ScoredCandidate choice = ScoredCandidate.NONE;
for (Candidate candidate : candidates) {
ScoredCandidate scoredCandidate = scoreCandidate(candidate);
if (scoredCandidate.value > choice.value) {
choice = scoredCandidate;
}
}
return choice;
}
根据评分器选出来的最后的热点信息,再回到handleScanResults()
方法中,选出WifiConfiguration
后,调用了connectToNetwork()
方法进行连接,最终还是会走到ClientModeImpl.startConnectToNetwork()
连接方法中,后面的就是和wifi连接相关的过程了,详情转到【安卓Framework学习】Wifi框架学习之连接与断开流程。接下来分析每个评分器的评分规则。
由于每个评分器都是调用scoreCandidates()
方法,而scoreCandidates()
方法中会调用评分器中的scoreCandidate()
方法对候选热点列表进行遍历评分,然后保存分数最高的一个热点。
private ScoredCandidate scoreCandidate(Candidate candidate) {
/*计算出一个初始分数,主要是根据热点当前的信号强度和热点的频率值进行计算*/
int rssiSaturationThreshold = mScoringParams.getGoodRssi(candidate.getFrequency());
int rssi = Math.min(candidate.getScanRssi(), rssiSaturationThreshold);
int score = (rssi + RSSI_SCORE_OFFSET) * RSSI_SCORE_SLOPE_IS_4;
/*根据热点的频率不同,对其进行适当加分*/
if (ScanResult.is6GHz(candidate.getFrequency())) {
score += BAND_6GHZ_AWARD_IS_40;
} else if (ScanResult.is5GHz(candidate.getFrequency())) {
score += BAND_5GHZ_AWARD_IS_40;
}
/*根据当前评分热点是否为上一次连接热点,并且上一次选择此热点到目前不超过480分钟,可以认为其为最近连接的热点给与适当加分*/
score += (int) (candidate.getLastSelectionWeight() * LAST_SELECTION_AWARD_IS_480);
/*判断当前评分热点是否为当前连接热点,进行适当加分*/
if (candidate.isCurrentNetwork()) {
score += CURRENT_NETWORK_BOOST_IS_16 + SAME_BSSID_AWARD_IS_24;
}
/*判断当前评分热点是否为开放热点,若不是则进行适当加分*/
if (!candidate.isOpenNetwork()) {
score += SECURITY_AWARD_IS_80;
}
/*根据不同的提名器,需要减分,这里减分最重的就是当前连接的热点*/
score -= 1000 * candidate.getNominatorId();
double tieBreaker = candidate.getScanRssi() / 1000.0;
return new ScoredCandidate(score + tieBreaker, 10,
USE_USER_CONNECT_CHOICE, candidate);
}
此评分器根据其注释可以知道应该是为了兼容以前的评分规则而产生,侧重点在于不同提名器的惩罚而将分数拉开差距。其主要的加分项在信号强度、热点频率、是否为最近连接、是否为当前连接热点、是否为开放热点。
此评分器会根据一个评分卡对热点信号强度的计算,其他的都基本大同小异,也是基本和信号强度、热点频率、是否为最近连接、是否为当前连接热点、是否为开放热点有强关联,重点看对信号强度做计算的方法。
private int estimatedCutoff(Candidate candidate) {
int cutoff = -RSSI_SCORE_OFFSET;
int lowest = cutoff - RSSI_RAIL;
int highest = cutoff + RSSI_RAIL;
WifiScoreCardProto.Signal signal = candidate.getEventStatistics(Event.SIGNAL_POLL);
if (signal == null) return cutoff;
if (!signal.hasRssi()) return cutoff;
if (signal.getRssi().getCount() > MIN_POLLS_FOR_SIGNIFICANCE) {
double mean = signal.getRssi().getSum() / signal.getRssi().getCount();
double mean_square = signal.getRssi().getSumOfSquares() / signal.getRssi().getCount();
double variance = mean_square - mean * mean;
double sigma = Math.sqrt(variance);
double value = mean - 2.0 * sigma;
cutoff = (int) Math.min(Math.max(value, lowest), highest);
}
return cutoff;
}
这里具体没找到下层的代码实现,不过从代码上看,像是针对某个热点取样了一组信号强度值,然后对这一组数据进行求均值和平方和,然后用这两个值计算出一个最后的值用于计算基础分数,整体来说还是根据信号强度值使用一定的算法计算出来的一个结果。(这部分如有大佬指导详细原理的可以随时指出)
这个评分器其实和CompatibilityScorer
差不多,但是它对信号强度做了一次指数函数的运算,将最后的评分压缩在一个较小的范围内,主要的因素还是信号强度、热点频率、是否为最近连接、是否为当前连接热点、是否为开放热点。其中计算指数函数的方法如下。
private static double unscaledShapeFunction(double rssi) {
return -Math.exp(-rssi * BELS_PER_DECIBEL);
}
private static final double BELS_PER_DECIBEL = 0.1;
private static final double RESCALE_FACTOR = 100.0 / (unscaledShapeFunction(0.0) - unscaledShapeFunction(-85.0));
private static double shapeFunction(double rssi) {
return unscaledShapeFunction(rssi) * RESCALE_FACTOR;
}
这个评分器通过其注释可以知道,在前面都那些共同指标的前提下,结合了信号强度的基础分和网络吞吐量的分得出来的评分。注释如下。
/**
* A candidate scorer that combines RSSI base score and network throughput score.
*/
此评分器除了上述其他评分器都会用到的指标外,还会用到是否需要连接互联网、是否是临时连接、是否是受信用的热点这三项指标进行了适当加分。
private ScoredCandidate scoreCandidate(Candidate candidate) {
/*根据信号强度和热点频率计算一个基础分*/
int rssiSaturationThreshold = mScoringParams.getSufficientRssi(candidate.getFrequency());
int rssi = Math.min(candidate.getScanRssi(), rssiSaturationThreshold);
int rssiBaseScore = (rssi + RSSI_SCORE_OFFSET) * RSSI_SCORE_SLOPE_IS_4;
/*根据热点的网络吞吐量计算出一个吞吐量的加分值*/
int throughputBonusScore = calculateThroughputBonusScore(candidate);
int rssiAndThroughputScore = rssiBaseScore + throughputBonusScore;
/*确定当前被评分热点是否需要连接互联网*/
boolean unExpectedNoInternet = candidate.hasNoInternetAccess() && !candidate.isNoInternetAccessExpected();
/*计算当前连接的热点的加分值*/
int currentNetworkBonusMin = mScoringParams.getCurrentNetworkBonusMin();
int currentNetworkBonus = Math.max(currentNetworkBonusMin, rssiAndThroughputScore
* mScoringParams.getCurrentNetworkBonusPercent() / 100);
int currentNetworkBoost = (candidate.isCurrentNetwork() && !unExpectedNoInternet)? currentNetworkBonus : 0;
/*确定当前别评分热点是否为开放热点、被测量热点、被保存热点然后进行适当加分*/
int securityAward = candidate.isOpenNetwork()? 0: mScoringParams.getSecureNetworkBonus();
int unmeteredAward = candidate.isMetered()? 0: mScoringParams.getUnmeteredNetworkBonus();
int savedNetworkAward = candidate.isEphemeral() ? 0 : mScoringParams.getSavedNetworkBonus();
/*确定热点是被信任的加分值*/
int trustedAward = TRUSTED_AWARD;
if (!candidate.isTrusted()) {
savedNetworkAward = 0; // Saved networks are not untrusted, but clear anyway
unmeteredAward = 0; // Ignore metered for untrusted networks
if (candidate.isCarrierOrPrivileged()) {
trustedAward = HALF_TRUSTED_AWARD;
} else if (candidate.getNominatorId() == NOMINATOR_ID_SCORED) {
Log.e(TAG, "ScoredNetworkNominator is not carrier or privileged!");
trustedAward = 0;
} else {
trustedAward = 0;
}
}
int score = rssiBaseScore + throughputBonusScore
+ currentNetworkBoost + securityAward + unmeteredAward + savedNetworkAward
+ trustedAward;
/*在近480分钟内有热点被选择连接,那么就重新更新分值*/
if (candidate.getLastSelectionWeight() > 0.0) {
// Put a recently-selected network in a tier above everything else,
// but include rssi and throughput contributions for BSSID selection.
score = TOP_TIER_BASE_SCORE + rssiBaseScore + throughputBonusScore;
}
/*省略部分代码*/
double tieBreaker = candidate.getScanRssi() / 1000.0;
return new ScoredCandidate(score + tieBreaker, 10,
USE_USER_CONNECT_CHOICE, candidate);
}
private int calculateThroughputBonusScore(Candidate candidate) {
int throughputScoreRaw = candidate.getPredictedThroughputMbps()
* mScoringParams.getThroughputBonusNumerator()
/ mScoringParams.getThroughputBonusDenominator();
return Math.min(throughputScoreRaw, mScoringParams.getThroughputBonusLimit());
}
主要细节说明都在上述代码中体现,至于调用的calculateThroughputBonusScore()
方法中ScoringParams
类调用出来的值其实都是固定值,然后主要的变量就是candidate.getPredictedThroughputMbps()
得到的数值。而这个值对应的是在WifiNetworkSelector.getCandidatesFromScan()
方法中调用WifiNetworkSelector.predictThroughput()
方法计算出来的,再看WifiNetworkSelector.predictThroughput()
方法。
private int predictThroughput(@NonNull ScanDetail scanDetail) {
/*省略部分代码*/
int channelUtilizationLinkLayerStats = BssLoad.INVALID;
if (mWifiChannelUtilization != null) {
channelUtilizationLinkLayerStats = mWifiChannelUtilization.getUtilizationRatio(scanDetail.getScanResult().frequency);
}
return mThroughputPredictor.predictThroughput(
mWifiNative.getDeviceWiphyCapabilities(mWifiNative.getClientInterfaceName()),
scanDetail.getScanResult().getWifiStandard(),
scanDetail.getScanResult().channelWidth,
scanDetail.getScanResult().level,
scanDetail.getScanResult().frequency,
scanDetail.getNetworkDetail().getMaxNumberSpatialStreams(),
scanDetail.getNetworkDetail().getChannelUtilization(),
channelUtilizationLinkLayerStats,
mIsBluetoothConnected);
}
这里我们只看与什么因素相关,里面具体的计算就不往下追了,感兴趣的同学可以往下继续分析是如何计算出来的。从方法中可以看出,这个预计吞吐量带宽值与热点标准、通道带宽、信号等级、热点频率和是否连接蓝牙(因为蓝牙和wifi是同一个芯片)有关,其他和NetworkDetail
有关的参数目前还没搞懂是啥,具体应该是和物理通道的利用率有关,后续弄清楚再补上。上述即为wifi框架中自动连接流程和热点的评分机制,其中有些没明确的地方,如有不对大家可随时提出。
wifi框架中的扫描、热点评分机制和自动连接机制是紧密相关的,扫描结果会触发热点评分,评分结束后会触发连接操作,所以热点评分单独拿出来是没有任何意义的,因此需要结合热点扫描一起学习分析。评分机制中有较多的提名器和评分器,但是最后只会使用一个评分器选择出来的热点进行连接,每个评分器评分规则都会基于热点信号强度、热点频率、是否为最近连接等基本指标上再做特定的指标分数倾斜,所以每个评分器也各有特点,希望这一篇能够让大家对wifi框架有更加深入的了解。