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所使用的,但是作为一个评估当前手机所处于的带宽状态还是没有问题的,比方说可以预估当前网速来限速、判断是否弱网等等。

    原文作者:范锦浩
    原文地址: https://www.jianshu.com/p/139ea6248279
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞