network-connection-class阅读笔记
简述
GitHub地址
一个用于检测带宽等级变化的辅助工具,并且在带宽等级发生变化的时候可以进行一些回调处理。
原理
检测带宽,简单的一个理解就是检查下载速度,比方说1ms可以下载多少字节数的数据,也就是1ms可以收到多少网络传输过来的包的大小。
那么需要检测带宽等级变化,首先需要定义带宽等级以及动态监测。
先看预定义的带宽等级
public enum ConnectionQuality {
/**
* Bandwidth under 150 kbps.
* 当前带宽在1.5m以下
*/
POOR,
/**
* Bandwidth between 150 and 550 kbps.
* 当前带宽在1.5m和5.5m之间
*/
MODERATE,
/**
* Bandwidth between 550 and 2000 kbps.
* 当前带宽在5.5m和20m之间
*/
GOOD,
/**
* EXCELLENT - Bandwidth over 2000 kbps.
* 当前带宽在20m以上
*/
EXCELLENT,
/**
* Placeholder for unknown bandwidth. This is the initial value and will stay at this value
* if a bandwidth cannot be accurately found.
* 初始值,或者说当前计算带宽还未得到结果
*/
UNKNOWN
}
这个还是需要根据具体的场景情况去修改的。
接着看是如何采样的:
public class DeviceBandwidthSampler {
/**
* The DownloadBandwidthManager that keeps track of the moving average and ConnectionClass.
*/
private final ConnectionClassManager mConnectionClassManager;
//原子Integer操作类
private AtomicInteger mSamplingCounter;
//当前采样的线程
private HandlerThread mThread;
//在子线程(mThread)中执行的Handler,用于进行定时的采样
private SamplingHandler mHandler;
//记录上一次读取的时间,这个是系统开机到现在的时间
private long mLastTimeReading;
//用于记录上一次采样的时候设备所从网络上收到的包的字节数
private static long sPreviousBytes = -1;
// Singleton.静态内部类单例,实际上ConnectionClassManager也是单例
private static class DeviceBandwidthSamplerHolder {
public static final DeviceBandwidthSampler instance =
new DeviceBandwidthSampler(ConnectionClassManager.getInstance());
}
/**
* Retrieval method for the DeviceBandwidthSampler singleton.
*
* @return The singleton instance of DeviceBandwidthSampler.
*/
@Nonnull
public static DeviceBandwidthSampler getInstance() {
return DeviceBandwidthSamplerHolder.instance;
}
private DeviceBandwidthSampler(
ConnectionClassManager connectionClassManager) {
mConnectionClassManager = connectionClassManager;
//初始为0
mSamplingCounter = new AtomicInteger();
//初始化一个子线程并开启
mThread = new HandlerThread("ParseThread");
mThread.start();
//当前SamplingHandler运行在子线程中
mHandler = new SamplingHandler(mThread.getLooper());
}
/**
* Method call to start sampling for download bandwidth.
* 开始进行带宽的测量
*/
public void startSampling() {
//通过原子增长操作来保证只运行一次
//实际上用AtomicBoolean也行
//采样的轮询操作只能开始一次,必须先停止之前的采样,才可以开始新的采样
if (mSamplingCounter.getAndIncrement() == 0) {
mHandler.startSamplingThread();//开始进行带宽计算的轮询
//记录采样开始时间
mLastTimeReading = SystemClock.elapsedRealtime();
}
}
/**
* Finish sampling and prevent further changes to the
* ConnectionClass until another timer is started.
* 停止采样轮询操作
*/
public void stopSampling() {
//当前采样进行中
if (mSamplingCounter.decrementAndGet() == 0) {
mHandler.stopSamplingThread();//停止采样的轮询操作
//后续虽然不在进行轮询计算,但是当前时刻要作最后一次带宽计算,这意味着stop之后有可能有一次的带宽等级变化回调
addFinalSample();
}
}
/**
* Method for polling for the change in total bytes since last update and
* adding it to the BandwidthManager.
* 计算当前带宽
* 实际上就是通过每隔一段时间的轮询,进行带宽的计算,从而进行带宽等级变化的回调
*/
protected void addSample() {
//先返回当前设备从开机到现在为止所收到的网络传过来的字节数,包括TCP和UDP传输
long newBytes = TrafficStats.getTotalRxBytes();
//与上次记录的收到的字节数做差,可以得到这段时间内所收到的字节数
long byteDiff = newBytes - sPreviousBytes;
if (sPreviousBytes >= 0) {//当前有旧的数据进行对比
synchronized (this) {
//获取当前开机到现在过去的毫秒数
long curTimeReading = SystemClock.elapsedRealtime();
//这里就是实际处理变化和计算的逻辑
mConnectionClassManager.addBandwidth(byteDiff, curTimeReading - mLastTimeReading);
//记录上一次进行的时间
mLastTimeReading = curTimeReading;
}
}
//第一次采样的时候没有旧的数据对比,直接记录就好,等待下一次采样的时候
sPreviousBytes = newBytes;
}
/**
* Resets previously read byte count after recording a sample, so that
* we don't count bytes downloaded in between sampling sessions.
*/
protected void addFinalSample() {
addSample();
sPreviousBytes = -1;
}
/**
* 当前是否采样中
* @return True if there are still threads which are sampling, false otherwise.
*/
public boolean isSampling() {
return (mSamplingCounter.get() != 0);
}
/**
* 计算用的Handler,不过在这里用是运行在HandlerThread开启的子线程中的
*/
private class SamplingHandler extends Handler {
/**
* Time between polls in ms.
* 1s轮询一次
*/
static final long SAMPLE_TIME = 1000;
static private final int MSG_START = 1;
public SamplingHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_START:
addSample();//进行带宽计算
//1s发送一条信号,相当于轮询,时间间隔为1s
//实际上就是如果开始采样,那么就每隔1s计算一次
sendEmptyMessageDelayed(MSG_START, SAMPLE_TIME);
break;
default:
throw new IllegalArgumentException("Unknown what=" + msg.what);
}
}
/**
* 开始采样线程,实际上就是发一个消息到handler中
* 然后后续handler会隔一段事件发送同一个消息进行轮询
*/
public void startSamplingThread() {
sendEmptyMessage(SamplingHandler.MSG_START);
}
/**
* 停止采样线程的进行,handler通过唯一的消息,隔一段时间执行一次
* 那么只需要将这个唯一的消息移除即可停止
*/
public void stopSamplingThread() {
removeMessages(SamplingHandler.MSG_START);
}
}
}
1.首先开启一个子线程,然后定义一个子线程的Handler。
2.开始采样之后,发送一条消息到Handler中,然后Handler中进行带宽计算等处理,处理完成之后再发送一条延时1s的消息到Handler中,从而实现了1s的轮询。
从代码中可以看到,Handler中进行处理的时候实际上是通过addSample处理,然后在里面将任务交给了ConnectionClassManager 处理:
public class ConnectionClassManager {
/*package*/
//在检测带宽变化的时候,因为有的时候可能因为波动等原因导致过于短暂的变化
//检测时候是采用一定时间自动检测,那么就需要定义一个基础的检测次数
//用于规定什么时候带宽值的变化可以认为有效
static final double DEFAULT_SAMPLES_TO_QUALITY_CHANGE = 5;
//这个就是一个字节有8位的意思
private static final int BYTES_TO_BITS = 8;
/**
* Default values for determining quality of data connection.
* Bandwidth numbers are in Kilobits per second (kbps).
*/
/*package*/ static final int DEFAULT_POOR_BANDWIDTH = 150;
/*package*/ static final int DEFAULT_MODERATE_BANDWIDTH = 550;
/*package*/ static final int DEFAULT_GOOD_BANDWIDTH = 2000;
/*package*/ static final long DEFAULT_HYSTERESIS_PERCENT = 20;
private static final double HYSTERESIS_TOP_MULTIPLIER = 100.0 / (100.0 - DEFAULT_HYSTERESIS_PERCENT);
private static final double HYSTERESIS_BOTTOM_MULTIPLIER = (100.0 - DEFAULT_HYSTERESIS_PERCENT) / 100.0;
/**
* The factor used to calculate the current bandwidth
* depending upon the previous calculated value for bandwidth.
*
* The smaller this value is, the less responsive to new samples the moving average becomes.
*/
private static final double DEFAULT_DECAY_CONSTANT = 0.05;
/**
* 实际进行当前带宽多大的计算器
* 内部有存储当前带宽大小
* */
private ExponentialGeometricAverage mDownloadBandwidth
= new ExponentialGeometricAverage(DEFAULT_DECAY_CONSTANT);
//用于标记当前带宽是否发生变化
private volatile boolean mInitiateStateChange = false;
//下面很多都是原子操作,简单的理解就是不用考虑多线程的问题
//当前网络连接带宽的质量,具体看ConnectionQuality里面定义的参数
private AtomicReference<ConnectionQuality> mCurrentBandwidthConnectionQuality =
new AtomicReference<ConnectionQuality>(ConnectionQuality.UNKNOWN);
private AtomicReference<ConnectionQuality> mNextBandwidthConnectionQuality;
private ArrayList<ConnectionClassStateChangeListener> mListenerList =
new ArrayList<ConnectionClassStateChangeListener>();
private int mSampleCounter;
/**
* The lower bound for measured bandwidth in bits/ms. Readings
* lower than this are treated as effectively zero (therefore ignored).
* 测量的时候可以接受的在当前测量间隔内收到的最小字节位数
*/
static final long BANDWIDTH_LOWER_BOUND = 10;
// Singleton.
//静态内部类的单例实现模式,这种不需要考虑线程同步以及同步的多余开销
private static class ConnectionClassManagerHolder {
public static final ConnectionClassManager instance = new ConnectionClassManager();
}
/**
* Retrieval method for the DownloadBandwidthManager singleton.
* @return The singleton instance of DownloadBandwidthManager.
*/
@Nonnull
public static ConnectionClassManager getInstance() {
return ConnectionClassManagerHolder.instance;
}
// Force constructor to be private.
private ConnectionClassManager() {}
/**
* Adds bandwidth to the current filtered latency counter. Sends a broadcast to all
* {@link ConnectionClassStateChangeListener} if the counter moves from one bucket
* to another (i.e. poor bandwidth -> moderate bandwidth).
* @param bytes timeInMs这段时间内所收到的字节数
* @param timeInMs 计算的时间
*/
public synchronized void addBandwidth(long bytes, long timeInMs) {
//1.当前计算时间必须>0
//2.当前间隔内所收到的包的字节数的位数必须大于预定义的最小值,默认10
if (timeInMs == 0 || (bytes) * 1.0 / (timeInMs) * BYTES_TO_BITS < BANDWIDTH_LOWER_BOUND) {
return;
}
//获得当前每毫秒所收到的字节位数
double bandwidth = (bytes) * 1.0 / (timeInMs) * BYTES_TO_BITS;
//将当前数据传入计算器中进行计算,后续计算结果会保留在计算器中
mDownloadBandwidth.addMeasurement(bandwidth);
if (mInitiateStateChange) {//当前带宽发生变化
mSampleCounter += 1;//带宽变化采样次数+1
//之前带宽变化的时候记录了带宽等级
//如果这次采样的时候带宽等级再一次发生变化
if (getCurrentBandwidthQuality() != mNextBandwidthConnectionQuality.get()) {
//还原数据,等待之后的采样,因为认为当前是带宽波动,之前的计算无效
mInitiateStateChange = false;
mSampleCounter = 1;
}
//1.至少要保持5次相同的带宽状态才认为这种状态是处于稳定的状况,否则可能存在偶然的情况,一般来说测量时间为1s的话,则这种稳定范围任务是5s
//2.进行状态变动回调的时候有一个最小变化大小范围
// 默认如果是变大,要求超过原来最大值 * 1.25
// 如果变小,要求至少小于等于原来的80%
if (mSampleCounter >= DEFAULT_SAMPLES_TO_QUALITY_CHANGE && significantlyOutsideCurrentBand()) {
//还原标记
mInitiateStateChange = false;
mSampleCounter = 1;
//修改当前带宽状态
mCurrentBandwidthConnectionQuality.set(mNextBandwidthConnectionQuality.get());
//通知观察者带宽发生变化
notifyListeners();
}
return;
}
//如果当前带宽状态发生了变化
if (mCurrentBandwidthConnectionQuality.get() != getCurrentBandwidthQuality()) {
//标记状态改变
mInitiateStateChange = true;
//记录下一个带宽状态
mNextBandwidthConnectionQuality =
new AtomicReference<ConnectionQuality>(getCurrentBandwidthQuality());
}
}
/**
* 校验变化的正确性和确立变化的范围
* @return true认为是有效的变化
*/
private boolean significantlyOutsideCurrentBand() {
if (mDownloadBandwidth == null) {
// Make Infer happy. It wouldn't make any sense to call this while mDownloadBandwidth is null.
return false;
}
ConnectionQuality currentQuality = mCurrentBandwidthConnectionQuality.get();
double bottomOfBand;
double topOfBand;
switch (currentQuality) {
case POOR:
bottomOfBand = 0;
topOfBand = DEFAULT_POOR_BANDWIDTH;
break;
case MODERATE:
bottomOfBand = DEFAULT_POOR_BANDWIDTH;
topOfBand = DEFAULT_MODERATE_BANDWIDTH;
break;
case GOOD:
bottomOfBand = DEFAULT_MODERATE_BANDWIDTH;
topOfBand = DEFAULT_GOOD_BANDWIDTH;
break;
case EXCELLENT:
bottomOfBand = DEFAULT_GOOD_BANDWIDTH;
topOfBand = Float.MAX_VALUE;
break;
default: // If current quality is UNKNOWN, then changing is always valid.
return true;
}
double average = mDownloadBandwidth.getAverage();
//简单说就是如果当前带宽变高了,那么至少也要比之前高25个百分比,低的话至少低20个百分比
if (average > topOfBand) {
if (average > topOfBand * HYSTERESIS_TOP_MULTIPLIER) {
return true;
}
} else if (average < bottomOfBand * HYSTERESIS_BOTTOM_MULTIPLIER) {
return true;
}
return false;
}
/**
* Get the ConnectionQuality that the moving bandwidth average currently represents.
* 通过计算器中计算的结果得到当前带宽等级
* @return A ConnectionQuality representing the device's bandwidth at this exact moment.
*/
public synchronized ConnectionQuality getCurrentBandwidthQuality() {
if (mDownloadBandwidth == null) {
return ConnectionQuality.UNKNOWN;
}
return mapBandwidthQuality(mDownloadBandwidth.getAverage());
}
/**
* 根据当前带宽的平均值进行映射
* 然后返回预定义的带宽等级
* @param average 当前带宽的平均值
* @return 当前带宽的预定义等级
*/
private ConnectionQuality mapBandwidthQuality(double average) {
//这个定义实际上看ConnectionQuality也明白
if (average < 0) {
return ConnectionQuality.UNKNOWN;
}
if (average < DEFAULT_POOR_BANDWIDTH) {
return ConnectionQuality.POOR;
}
if (average < DEFAULT_MODERATE_BANDWIDTH) {
return ConnectionQuality.MODERATE;
}
if (average < DEFAULT_GOOD_BANDWIDTH) {
return ConnectionQuality.GOOD;
}
return ConnectionQuality.EXCELLENT;
}
/**
* Interface for listening to when {@link com.facebook.network.connectionclass.ConnectionClassManager}
* changes state.
* 接口用于监听连接状态的改变
*/
public interface ConnectionClassStateChangeListener {
/**
* The method that will be called when {@link com.facebook.network.connectionclass.ConnectionClassManager}
* changes ConnectionClass.
* @param bandwidthState The new ConnectionClass.
*/
public void onBandwidthStateChange(ConnectionQuality bandwidthState);
}
/**
* Method for adding new listeners to this class.
* 添加监听用于在网络状态变化的时候进行处理
* @param listener {@link ConnectionClassStateChangeListener} to add as a listener.
*/
public ConnectionQuality register(ConnectionClassStateChangeListener listener) {
if (listener != null) {
mListener
进行回调的原则:
1.首先当前时间间隔内应该收到一定量的网络包,否则当前相当于没有数据从网络传来,那么就不用提带宽等级变化。
2.当前时间间隔的带宽等级相对之前记录的带宽等级要发生变化。
3.当前带宽等级变化必须稳定,这里的稳定在默认实现上是要求5次同一个等级,而且当前带宽大小的变化还要达到小于之前等级最小值的80%或者超过之前等级最大值25%。
如果满足这些条件,那么就会进行接口回调,通知观察者带宽等级发生变化。
最后看一下当前带宽大小的计算,在ConnectionClassManager中是委托ExponentialGeometricAverage 进行计算的:
public void addMeasurement(double measurement) {
//0.95
double keepConstant = 1 - mDecayConstant;
//因为在确信带宽状态稳定的情况下会进行多次计算,这里在确定这一段时间内的带宽平均大小
//注意这里有计算一个偏移量keepConstant,这个可以在ConnectionClassManager中定义
//这里在计算的时候没有直接均分,而是采用了比例
//直观地理解就是计算的次数越多,之前计算的结果占比就越大,新的带宽大小占比就低
//这个是用于计算在整个采样过程中的带宽大小,那么旧的结果占比大是正常的
if (mCount > mCutover) {
mValue = Math.exp(keepConstant * Math.log(mValue) + mDecayConstant * Math.log(measurement));
} else if (mCount > 0) {
//keepConstant - (keepConstant)/(mCount + 1.0)
//mCount越大retained越大
double retained = keepConstant * mCount / (mCount + 1.0);
double newcomer = 1.0 - retained;
mValue = Math.exp(retained * Math.log(mValue) + newcomer * Math.log(measurement));
} else {//初始化count==0
mValue = measurement;
}
//注意这个如果不手动reset的话,是会一直进行累加
mCount++;
}
其中可以指定mDecayConstant,默认为0.05,这意味着旧的计算结果的占比。
实际上可以看到如果为0的话,那么每一次计算都是retained和newcomer 都是0.5,这样就是普通的算数平均值。
结语
network-connection-class的作用就是一段时间检测带宽
那么在实际使用中,一般就是用来记录每次网络请求发生的时候的带宽,当然这个计算的结果是整个设备在一段时间内的,并不是当前app所使用的,但是作为一个评估当前手机所处于的带宽状态还是没有问题的,比方说可以预估当前网速来限速、判断是否弱网等等。