不积跬步无以至千里
流量使用情况,好多软件都会带这个功能,比如360的流量监控,好多之类的,手机管家都会带上这个流量计算的功能,连系统应用设置里面也会带一个流量使用情况的查看功能,为什么呢?因为流量的使用关乎到用户使用流量的计费,当流量使用了很多,会给用户造成额外的损失,因此流量使用情况这个功能是好多手机管家不可或缺的一个功能。
而正好,我这个项目也要做一个流量使用情况的计算,在网上找了一番,好多推荐的都是TrafficStats这个类,这个类能获取到从开机到现在的总发送字节数和接收字节数,而加起来就是系统总消耗流量值(不包括wifi使用的消耗)和包含wifi的总发送消耗流量、总接收消耗流量,还能获取从开机到现在针对某个应用消耗的发送字节数和接收字节数。但是有两个缺点,一、只能是开机到现在的流量使用情况,如果中间开关机操作,以前的流量使用情况就被清零了。二、不能获取某段时间内的流量消耗情况。
根据上面对TrafficStats的简单分析,我发现我的这个项目需要某段时间(开始时间、结束时间、某个卡)、15分钟获取一次消耗的流量情况,因此不适合用这个类,而只能另寻他处,大家都知道,系统Settings里面有一个流量使用情况的功能,可以从这里看看它是怎么做的?接下来就说说。
如图,我们可以看到流量使用情况这个功能的截图,经过观察看到这一块如下图:
由图可知,这块不就是某卡某段时间消耗的总流量值吗?因此通过hierarchyviewer这个工具类获取那是个textview并获得这个控件的id为:usage_summary,并知道这个界面的类为:DataUsageSummary。路径为:
跟踪这个控件如下图:
找到了这个控件的关联,接下来看一下怎么对这个textview赋值的:
据图可知,可以看出显示的内容都是通过给开始时间和结束时间来获取的entry,然后设置上去的流量的消耗,然后通过开始和结束时间获取当前的日期时间段(先给大家分析一下,通过设置可知,它的开始时间和结束时间组成的周期都为1个月,而你可以截取一段时间,而最短为一天),标上是从哪天到哪天的日期消耗了多少流量。然后再分析一下这个entry怎么来的?
其实设置里面的加载机制用了LoaderManager,如下图:
由上图可知,先是通过new ChartDataLoader(getActivity(), mStatsSession, args);先看一下其中的参数,第一个是getActivity应该是获取上下文,第三个args获取的是bundle对象,看一下第二个对象mStatsSession怎么来的?
由图可知,先获取了NetworkStatsService的对象,然后再获取到了INetworkStatsSession对象,我们接着追LoaderManager的onCreateLoader的那个new ChartDataLoader这个:
接着配一张图用于解释获取的data.network对象:
由图可知,调用了传过去的参数mSession.getHistoryForNetwork(template,filed);
传了是取得哪个卡的数据,取传入的bundle的携带的参数(下面会说参数的意义)。 由上图可知,其实这里获取到的是NetworkStatsHistory,其实这里就能回到开始textview赋值那了,那个被包装的entry就是NetworkStatsHistory的一个包装着流量信息的对象。接下来给大家分一下NetworkStatsHistory这个类(全路径为:android/frameworks/base/core/java/android/net/NetworkStatsHistory.java)这个类的主要作用就是流量的记录和获取,而本身这个存取方式中的形式这个类中巧妙的称之为(桶)bucket,而每个桶都有个间隔,可以把它想成桶的大小来存储这段时间内的流量的,而桶的大小就是时间间隔,相当于这从几点到几点(例如桶为:3:00~4:00,装了500b流量)。
先看一下这个类的构造方法:
由图可以看出,其中有个参数为bucketDuration,这就是上边说的时间间隔,可以作为一个构造参数传进来,为桶制作大小。
接下来说一下用来包装桶的对象。
这里面都包含了桶的大小(即时间间隔)、桶的开始时间、桶的活动时间、桶的接受字节数、桶的接受数据包、桶的发送数据包、桶的操作。
下面通过截取的代码通过注释的方式说一下各个方法的作用
//存储中一共有多少个桶
public int size() {
return bucketCount;
}
//获取桶的间隔
public long getBucketDuration() {
return bucketDuration;
}
//获取存储的总开始时间
public long getStart() {
if (bucketCount > 0) {
return bucketStart[0];
} else {
return Long.MAX_VALUE;
}
}
//获取存储的总结束时间
public long getEnd() {
if (bucketCount > 0) {
return bucketStart[bucketCount - 1] + bucketDuration;
} else {
return Long.MIN_VALUE;
}
}
/**
* Return total bytes represented by this history.
* 翻译:返回历史中总消耗(存储)的流量字节
*/
public long getTotalBytes() {
return totalBytes;
}
/**
* Return index of bucket that contains or is immediately before the
* requested time.
* 翻译:给定一个时间点获取包含这个时间所在桶之前的下标值
*/
public int getIndexBefore(long time) {
int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time);
if (index < 0) {
index = (~index) - 1;
} else {
index -= 1;
}
return MathUtils.constrain(index, 0, bucketCount - 1);
}
/**
* Return index of bucket that contains or is immediately after the
* requested time.
* 翻译:给定一个时间点获取包含这个时间所在桶之后的下标值
*/
public int getIndexAfter(long time) {
int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time);
if (index < 0) {
index = ~index;
} else {
index += 1;
}
return MathUtils.constrain(index, 0, bucketCount - 1);
}
/**
* Return specific stats entry.
*翻译:返回明确的状态的对应的实体
*/
public Entry getValues(int i, Entry recycle) {
final Entry entry = recycle != null ? recycle : new Entry();
entry.bucketStart = bucketStart[i];
entry.bucketDuration = bucketDuration;
entry.activeTime = getLong(activeTime, i, UNKNOWN);
entry.rxBytes = getLong(rxBytes, i, UNKNOWN);
entry.rxPackets = getLong(rxPackets, i, UNKNOWN);
entry.txBytes = getLong(txBytes, i, UNKNOWN);
entry.txPackets = getLong(txPackets, i, UNKNOWN);
entry.operations = getLong(operations, i, UNKNOWN);
return entry;
}
/**
* Record that data traffic occurred in the given time range. Will
* distribute across internal buckets, creating new buckets as needed.
* 翻译:根据给定的时间范围内的数据的消耗,创建新桶来存储这些消耗。
*/
@Deprecated
public void recordData(long start, long end, long rxBytes, long txBytes) {
recordData(start, end, new NetworkStats.Entry(
IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, 0L, txBytes, 0L, 0L));
}
/**
* Record that data traffic occurred in the given time range. Will
* distribute across internal buckets, creating new buckets as needed.
* 翻译:根据给定的时间范围内的数据的消耗,创建新桶来存储这些消耗。
*/
public void recordData(long start, long end, NetworkStats.Entry entry) {
long rxBytes = entry.rxBytes;
long rxPackets = entry.rxPackets;
long txBytes = entry.txBytes;
long txPackets = entry.txPackets;
long operations = entry.operations;
if (entry.isNegative()) {
throw new IllegalArgumentException("tried recording negative data");
}
if (entry.isEmpty()) {
return;
}
// create any buckets needed by this range
ensureBuckets(start, end);
// distribute data usage into buckets
long duration = end - start;
final int startIndex = getIndexAfter(end);
for (int i = startIndex; i >= 0; i--) {
final long curStart = bucketStart[i];
final long curEnd = curStart + bucketDuration;
// bucket is older than record; we're finished
if (curEnd < start) break;
// bucket is newer than record; keep looking
if (curStart > end) continue;
final long overlap = Math.min(curEnd, end) - Math.max(curStart, start);
if (overlap <= 0) continue;
// integer math each time is faster than floating point
final long fracRxBytes = rxBytes * overlap / duration;
final long fracRxPackets = rxPackets * overlap / duration;
final long fracTxBytes = txBytes * overlap / duration;
final long fracTxPackets = txPackets * overlap / duration;
final long fracOperations = operations * overlap / duration;
addLong(activeTime, i, overlap);
addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes;
addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets;
addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes;
addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets;
addLong(this.operations, i, fracOperations); operations -= fracOperations;
duration -= overlap;
}
totalBytes += entry.rxBytes + entry.txBytes;
}
/**
* Ensure that buckets exist for given time range, creating as needed.
* 翻译:根据给定的时间范围,创建需要的桶(记录流量使用的时候会调用这个方法)
*/
private void ensureBuckets(long start, long end) {
// normalize incoming range to bucket boundaries
start -= start % bucketDuration;
end += (bucketDuration - (end % bucketDuration)) % bucketDuration;
for (long now = start; now < end; now += bucketDuration) {
// try finding existing bucket
final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now);
if (index < 0) {
// bucket missing, create and insert
insertBucket(~index, now);
}
}
}
/**
* Insert new bucket at requested index and starting time.
* 翻译:插入新桶根据坐标,并给予此桶的开始时间(创建新桶的时候调用)
*/
private void insertBucket(int index, long start) {
// create more buckets when needed
if (bucketCount >= bucketStart.length) {
final int newLength = Math.max(bucketStart.length, 10) * 3 / 2;
bucketStart = Arrays.copyOf(bucketStart, newLength);
if (activeTime != null) activeTime = Arrays.copyOf(activeTime, newLength);
if (rxBytes != null) rxBytes = Arrays.copyOf(rxBytes, newLength);
if (rxPackets != null) rxPackets = Arrays.copyOf(rxPackets, newLength);
if (txBytes != null) txBytes = Arrays.copyOf(txBytes, newLength);
if (txPackets != null) txPackets = Arrays.copyOf(txPackets, newLength);
if (operations != null) operations = Arrays.copyOf(operations, newLength);
}
// create gap when inserting bucket in middle
if (index < bucketCount) {
final int dstPos = index + 1;
final int length = bucketCount - index;
System.arraycopy(bucketStart, index, bucketStart, dstPos, length);
if (activeTime != null) System.arraycopy(activeTime, index, activeTime, dstPos, length);
if (rxBytes != null) System.arraycopy(rxBytes, index, rxBytes, dstPos, length);
if (rxPackets != null) System.arraycopy(rxPackets, index, rxPackets, dstPos, length);
if (txBytes != null) System.arraycopy(txBytes, index, txBytes, dstPos, length);
if (txPackets != null) System.arraycopy(txPackets, index, txPackets, dstPos, length);
if (operations != null) System.arraycopy(operations, index, operations, dstPos, length);
}
bucketStart[index] = start;
setLong(activeTime, index, 0L);
setLong(rxBytes, index, 0L);
setLong(rxPackets, index, 0L);
setLong(txBytes, index, 0L);
setLong(txPackets, index, 0L);
setLong(operations, index, 0L);
bucketCount++;
}
/**
* Remove buckets older than requested cutoff.
* 翻译:移除给定时间点之前的所有桶
*/
@Deprecated
public void removeBucketsBefore(long cutoff) {
int i;
for (i = 0; i < bucketCount; i++) {
final long curStart = bucketStart[i];
final long curEnd = curStart + bucketDuration;
// cutoff happens before or during this bucket; everything before
// this bucket should be removed.
if (curEnd > cutoff) break;
}
if (i > 0) {
final int length = bucketStart.length;
bucketStart = Arrays.copyOfRange(bucketStart, i, length);
if (activeTime != null) activeTime = Arrays.copyOfRange(activeTime, i, length);
if (rxBytes != null) rxBytes = Arrays.copyOfRange(rxBytes, i, length);
if (rxPackets != null) rxPackets = Arrays.copyOfRange(rxPackets, i, length);
if (txBytes != null) txBytes = Arrays.copyOfRange(txBytes, i, length);
if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length);
if (operations != null) operations = Arrays.copyOfRange(operations, i, length);
bucketCount -= i;
// TODO: subtract removed values from totalBytes
}
}
/**
* Return interpolated data usage across the requested range. Interpolates
* across buckets, so values may be rounded slightly.
* 翻译:根据你给的开始时间、结束时间、现在时间和一个桶的实体返回你这个时间段内消耗的流量值,单 轻微会有点误差
*/
public Entry getValues(long start, long end, Entry recycle) {
return getValues(start, end, Long.MAX_VALUE, recycle);
}
下面细说一下取开始时间到结束时间中消耗的总流量字节数这个方法:
图一:
图二:
图三:
图四:
/**
* Return interpolated data usage across the requested range. Interpolates
* across buckets, so values may be rounded slightly.
* 翻译:根据你给的开始时间、结束时间、现在时间和一个桶的实体返回你这个时间段内消耗的流量值,单 轻微会有点误差(这个是获取流量的最终实现方法,上面Settings最终调用的就是这个方法)
*/
public Entry getValues(long start, long end, long now, Entry recycle) {
//判断传入的实体为不为空
final Entry entry = recycle != null ? recycle : new Entry();
//你传入的时间差值
entry.bucketDuration = end - start;
//你传入的开始时间
entry.bucketStart = start;
entry.activeTime = activeTime != null ? 0 : UNKNOWN;
//接收字节数
entry.rxBytes = rxBytes != null ? 0 : UNKNOWN;
entry.rxPackets = rxPackets != null ? 0 : UNKNOWN;
//发送字节数
entry.txBytes = txBytes != null ? 0 : UNKNOWN;
entry.txPackets = txPackets != null ? 0 : UNKNOWN;
entry.operations = operations != null ? 0 : UNKNOWN;
//根据你传入的结束时间获取当前时间当前桶的坐标
final int startIndex = getIndexAfter(end);
//通过for循环去遍历,现在根据获取最后一个桶的坐标,向前推,获取之前在时间范围内的所有桶的总消耗流量值
for (int i = startIndex; i >= 0; i--) {
//获取这个桶的开始时间
final long curStart = bucketStart[i];
//获取加上时间间隔的结束时间
final long curEnd = curStart + bucketDuration;
// bucket is older than request; we're finished(如果你给的开始时间大于这个上边你计算的那个桶的结束时间,就停止计算,因为相当于这个桶太靠前了,不在你给的开始值的范围内)如上图一说明:
if (curEnd <= start) break;
// bucket is newer than request; keep looking(如果你给定的结束时间小于此次循环的桶的开始时间,则就需要向前推算桶了,舍弃这个,继续算(这种情况应该处于刚开始计算的时候末尾桶的情况))如上图二说明:
if (curStart >= end) continue;
// include full value for active buckets, otherwise only fractional
//翻译:满足这个条件即为true时,循环的当前桶的开始时间小于现在时间并且这个桶的结束时间大于现在时间,相当于你请求的时间段是被这个桶包括着或者左边开始时间小于这个桶的,这种情况如上图三、图四:
final boolean activeBucket = curStart < now && curEnd > now;
//这里的overlap就是针对你遍历的每一个桶该怎么取我们取得流量在当前桶所占的时间,中间的桶当然就是整个桶的大小时间,如果是两边开始时间和结束时间很有可能是从桶中间取值,就要用我们取得这个时间在当前桶占用的时间/整个桶的大小时间*整个桶的流量大小,才是我这个桶消耗的流量值,但是我们的这个桶的大小(即时间大小,系统默认是一个小时,这样当我截取的时候很容易会出现误差,为什么呢?下面会给你讲一下,这也是此次我遇到的问题)
final long overlap;
if (activeBucket) {
overlap = bucketDuration;
} else {
final long overlapEnd = curEnd < end ? curEnd : end;
final long overlapStart = curStart > start ? curStart : start;
overlap = overlapEnd - overlapStart;
}
if (overlap <= 0) continue;
// integer math each time is faster than floating point
//下面是每循环一次会加一次,把之前的发送、接收的流量加上本次循环的流量值(就是用这次占用时间比*这个桶的总流量)
if (activeTime != null) entry.activeTime += activeTime[i] * overlap / bucketDuration;
if (rxBytes != null) entry.rxBytes += rxBytes[i] * overlap / bucketDuration;
if (rxPackets != null) entry.rxPackets += rxPackets[i] * overlap / bucketDuration;
if (txBytes != null) entry.txBytes += txBytes[i] * overlap / bucketDuration;
if (txPackets != null) entry.txPackets += txPackets[i] * overlap / bucketDuration;
if (operations != null) entry.operations += operations[i] * overlap / bucketDuration;
}
return entry;
}
上面是对这个流量历史类(NetworkStatsHistory)的这个分析,其中记录和获取使用情况是对应的,就是记录比获取多了添加新桶。其实本次项目我就是仿照Settings的这个流量使用情况,做的处理。整理下来核心的请求代码如下:
public static long getMsimTotalData(final Context context,final int simNum,final long startTime,final long endTime){
long value= 0;
try {
// wait a few seconds before kicking off
INetworkStatsService mStatsService = INetworkStatsService.Stub.asInterface(ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
Thread.sleep(2 * DateUtils.SECOND_IN_MILLIS);
//强制更新
mStatsService.forceUpdate();
INetworkStatsSession mStatsSession = mStatsService.openSession();
NetworkTemplate mTemplate = buildTemplateMobileAll(getMsimActiveSubscriberId(context,simNum));
NetworkStatsHistory networkStatsHistory = mStatsSession.getHistoryForNetwork(mTemplate, /*FIELD_RX_BYTES | FIELD_TX_BYTES*/NetworkStatsHistory.FIELD_ALL);
NetworkStatsHistory.Entry entry = null;
long bucketDuration = networkStatsHistory.getBucketDuration();
entry = networkStatsHistory.getValues(startTime,endTime,System.currentTimeMillis(), entry);
value = entry != null ? entry.rxBytes + entry.txBytes : 0;
final String totalPhrase = Formatter.formatFileSize(context, value);
long totalBytes = networkStatsHistory.getTotalBytes();
int afterBucketCount2 = networkStatsHistory.getIndexAfter(startTime);
int beforeBucketCount2 = networkStatsHistory.getIndexBefore(startTime); android.util.Log.i("wdy","afterBucketCount2:"+afterBucketCount2+",beforeBucketCount2:"+beforeBucketCount2);
android.util.Log.i("wdy","bucketDuration ="+bucketDuration+"totalPhrase:"+totalPhrase+",totalBytes:"+totalBytes);
TrafficStats.closeQuietly(mStatsSession);
}catch (RemoteException e) {
e.printStackTrace();
} catch (InterruptedException e) {
}
finally {
android.util.Log.i("wdy","getMsimTotalData:finally");
}
android.util.Log.i("wdy","total_value1:"+value);
return value;
}
如上图所示,传入参数有上下文、哪一个卡、开始时间和结束时间,这些就是仿照Settings的逻辑流程写的代码。其实这样就完成了这次工作,但是这也是问题的开始,刚开始我发现了一个现象,每次获取流量不准,如下图:
如上图所示,开始流量为15.54Mb(这是Settings的流量使用情况的截图),我接下来做了消耗流量的操作,结果显示用到了74.83Mb流量,因此总消耗了60Mb流量,然后通过上面的那个接口给的开始时间为:是一小时之前的时间点,结束时间为:现在的时间,理应获取的是消耗了60Mb,但是我首次获取这个流量显示消耗了20.51Mb(有点蒙比,我开始怀疑是不是我给的时间(开始时间、结束时间)不对啊,结果通过时间转换器,发现传入的时间是对的),接下来我每分钟获取一次,就出现了如上图的现象,每次流量消耗都会+2Mb(先说明一下,消耗上一次流量后边我就没有消耗流量了),然后慢慢大概经过30次(相当于1个小时)这就和上边那个桶默认大小(即时间大小)相似,所以说上边对getValues这个方法作介绍时,有一个通过时间然后获取你所取时间占这个桶大小的比例再*这个桶的含有多少流量。因此这样就有误差了啊,而我的项目需求是15分钟就获取一次,而这个桶大小为1个小时,如果这一个小时里,只有前10分钟(剧烈的)消耗了1024M流量,后50分钟没有消耗流量的操作,然后你去取流量当你取得是包含了这个桶的后50分钟、40分钟、后30分钟等等,而这样取得时候都会取总值的比例5/6、4/6等等。都会造成严重的误差,因此怎么解决这个问题?
首先解决这个问题的办法就是减小桶的体积,你如果把桶的体积由1小时变成1分钟,这样每分钟都会把之前消耗的记录下来,当你取得是15分钟的时候就会把对应的消耗值取出来,所以这样解决办法是对的,为什么Settings的流量使用情况是对的,因为流量使用情况它的范围是1天,包括了一个小时,这样就没有从桶里取得一说了,Settings相当于是所有桶都拿走,而我们15分钟取一次相当于是从桶里拿,这样的区别造成了误差。
因此想解决问题,改变桶的大小是关键,通过上边的代码了解到,桶的大小,可以通过NetWorkStatsHistory的构造方法来传入,记录流量的时候是属于装桶,而获取流量使用情况属于取桶,因此我们想让桶变小,所以要在装桶的时候把这个桶大小的值改变,于是我就代码跟踪,发现了有一个类如下图:
由图可知,看上边对这个类的解释可知:它是一个系统service,用于收集、存留详细的网络,提供这流量情况给另外的系统服务。经过多个类的扭转发现了存储这个桶大小的代码:
这个Config里面的bucketDuration是用来存储和获取的。看看原来是通过这个方法获取的:
从这代码可以看出,是通过Setting.Global.getlong方法获取这个路径下的\android\frameworks\base\core\res\res\values文件中的默认值,如果没有这个值,就取这个默认值-HOUR_IN_MILLIS(60分钟的意思,即桶的大小),当然你看到那个截图是已经被我改了(/60是经过我改过的)。这样我们再测试一下看一下:
由上图,我们这次做的测试是:开始时间:15分钟之前的时间,结束时间:当前时间。根据消耗,从89.33~103Mb,总共消耗了13M,13:21的时候开始消耗,13:27结束消耗,因此消耗流量大概是22~26消耗的。因此下面获取接口,首次13.62Mb和预期的差不多,一直 测,发现13:38开始减少,因为我们之前消耗的流量被减少了一部分(即13:21~13:23也有消耗,现在不算在内了),13:41时减小到为400+kB这种可以忽略为0了,而13:41对应的就是13:26,正好和我们当时结束使用网络时间所吻合(内心是激动地,终于解决了,oh my god)。
就这样,这个bug,拖延了如此之久,从开始的现象去猜测怎么回事?到看源码是什么逻辑?我学到了,真的一些问题不能偷懒,没有解决不了的bug,只要你塌下心来,认真的去看它真正的实现,你就能解决他了,盲目的猜肯定不能解决问题。