美文网首页Android进阶
network-connection-class阅读笔记

network-connection-class阅读笔记

作者: 范锦浩 | 来源:发表于2017-07-28 12:05 被阅读29次

    简述

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

    相关文章

      网友评论

        本文标题:network-connection-class阅读笔记

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