了解这一块也是因为有需求要获取指定应用所消耗的流量。在Android中,流量消耗主要分为手机卡和WIFI,在Settings中,也有统计流量的使用情况。经了解,Settings中是通过Loader去加载,Loader的原理就不多说了,很多大神的分析都很赞,这里仅介绍下该方法是怎么获取到流量的,其中会附带介绍Settings关于流量统计的源码分析思路。
Settings中有关流量统计的功能是在com.android.settings.datausage包路径下,其中DataUsageSummary是流量使用情况的入口。其中有两个方法:
/**
* 添加Mobile流量统计
* @param subId
*/
private void addMobileSection(int subId) {
TemplatePreferenceCategory category = (TemplatePreferenceCategory)
inflatePreferences(R.xml.data_usage_cellular);
category.setTemplate(getNetworkTemplate(subId), subId, services);
category.pushTemplates(services);
}
/**
* 添加Wifi流量统计
*/
private void addWifiSection() {
TemplatePreferenceCategory category = (TemplatePreferenceCategory)
inflatePreferences(R.xml.data_usage_wifi);
category.setTemplate(NetworkTemplate.buildTemplateWifiWildcard(), 0, services);
}
这两个方法分别是添加Mobile和Wifi的流量统计,顺着两份xml文件查找下去,可以发现两个流量统计的入口都是指向DataUsageList。而此处setTemplate方法传入的两个Template会在后面区分是Mobile还是Wifi时用到。
//Mobile
NetworkTemplate.buildTemplateMobileAll(
services.mTelephonyManager.getSubscriberId(subscriptionId));
//Wifi
NetworkTemplate.buildTemplateWifiWildcard()
找到具体的入口后,便可以分析具体的功能实现。由于我们只关心流量数据的变化,在抛去繁琐的UI初始化以后,我们可以发现跟数据变化有关的两个方法:updateDetailData()和bindStats()。下面利用注释分析下这两个方法的主要功能:
/**
* Update details based on {@link #mChart} inspection range depending on
* current mode. Updates {@link #mAdapter} with sorted list
* of applications data usage.
*/
private void updateDetailData() {
if (LOGD) Log.d(TAG, "updateDetailData()");
//该参数主要用于指定查询的时间范围
final long start;
final long end;
...
...//此处省略了start,和end处理逻辑。
...
final long now = System.currentTimeMillis();
final Context context = getActivity();
NetworkStatsHistory.Entry entry = null;
if (mChartData != null) {
entry = mChartData.network.getValues(start, end, now, null);
}
//初始化Loader
// kick off loader for detailed stats
getLoaderManager().restartLoader(LOADER_SUMMARY,
SummaryForAllUidLoader.buildArgs(mTemplate, start, end), mSummaryCallbacks);
//显示总的消耗量
final long totalBytes = entry != null ? entry.rxBytes + entry.txBytes : 0;
final String totalPhrase = Formatter.formatFileSize(context, totalBytes);
mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase));
if (mShowDataUsage) {
final int summaryRes = R.string.data_usage_total_during_range;
final String rangePhrase = Utils.formatDateRange(context, mSelectLeft, mSelectRight);
mUsageSummary.setSummary(getString(summaryRes, totalPhrase, rangePhrase));
}
}
Loader终于出现了,通过LoaderManager进行管理,其中restartLoader需要三个参数,其中一个参数由SummaryForAllUidLoader生成。该类位于com.android.settingslib.net路径下,SummaryForAllUidLoader是继承自AsyncTaskLoader实现异步加载。该类的buildArgs方法将之前提到的Template、start、end分装成Bundle格式。第二个参数LoaderCallbacks
private final LoaderCallbacks mSummaryCallbacks = new LoaderCallbacks<
NetworkStats>() {
@Override
public Loader onCreateLoader(int id, Bundle args) {
//创建异步加载的Loader,这里的mStatsSession是指的是INetworkStatsSession,是获取流量的主要入口
return new SummaryForAllUidLoader(getActivity(), mStatsSession, args);
}
@Override
public void onLoadFinished(Loader loader, NetworkStats data) { //加载结束后会调用该方法,用于更新数据源
final int[] restrictedUids = services.mPolicyManager.getUidsWithPolicy(
POLICY_REJECT_METERED_BACKGROUND);
bindStats(data, restrictedUids);
updateEmptyVisible();
}
@Override
public void onLoaderReset(Loader loader) {
//当restartLoader时,会调用该方法,用于重置数据源
bindStats(null, new int[0]);
updateEmptyVisible();
}
};
查看SummaryForAllUidLoader,可以发现,获取流量的主要方法还是通过INetworkStatsSession.getSummaryForAllUid()这个方法来获取的,此处我们可以展开联想直接操作INetworkStatsSession是否就可以获取到流量统计结果了?
onLoadFinished中的NetworkStats就是我们要获取的流量数据,而且是所有应用的流量数据。在加载结束后会调用bindStats()来处理数据,该方法不做详细介绍,我们只要获取到NetworkStats就可以用自己的逻辑来处理流量数据。接下来介绍下本人封装的方法,用于获取全部应用的流量数据:
private static final int LOADER_SUMMARY = 3;
INetworkStatsSession mSession ;
PackageManager pm = null;
NetworkStats mStats = null;
public Map forLoaderManager(NetworkTemplate template, long start, long end){
//用于获取INetworkStatsSession
INetworkStatsService mService
= INetworkStatsService.Stub.asInterface(ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
try {
mSession = mService.openSession();
if (mSession == null) {
Log.e(TAG, "getDataUsageWifi: the session is null" );
return null;
}
mStats = null;
((Activity)mContext).getLoaderManager().restartLoader(LOADER_SUMMARY,
SummaryForAllUidLoader.buildArgs(template, start, end), mSummaryCallbacks);
//下面是我自己实现的数据处理逻辑,由于需要实现同步返回,所以利用while去控制,如有更好的方法,欢迎指教
int time = 50;
while(true){
if(mStats != null)
break;
if(time-- < 0)
break;
Thread.sleep(50);
}
if(mStats == null){
Log.e(TAG, "getDataUsageWifi: get the netword data usage failed" );
return null;
}
pm = mContext.getPackageManager();
Map map = new HashMap<>();
int size = mStats.size();
NetworkStats.Entry entity = null;
for(int i=0; i
由于需要提供Service方式的SDK,而Loader是Activity和Fragment中才有的,所以上述的方法无法实现。通过上述的分析知道数据源其实是从Context.NETWORK_STATS_SERVICE这个服务获取的。那是否可以直接通过操作服务来获取到数据呢。所以就有了另外一种直接的获取方式来实现我的功能:
public Map forNetworkStatsManager(int type, long start, long end){
Log.d(TAG, "getAllDataUsage: start="+ start+ ", end="+ end);
//用于获取应用UID
PackageManager pm = (PackageManager) mContext.getPackageManager();
if(pm == null){
Log.e(TAG, "getAllDataUsage: get the telephone manager failed" );
return null;
}
//用于获取subscriberId
TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
if(pm == null){
Log.e(TAG, "getAllDataUsage: get the telephone manager failed" );
return null;
}
//用于获取流量数据
NetworkStatsManager nsm = (NetworkStatsManager) mContext.getSystemService(Context.NETWORK_STATS_SERVICE);
if(nsm == null){
Log.e(TAG, "getAllDataUsage: get the network stats manager failed");
return null;
}
List list = pm.getInstalledApplications(PackageManager.GET_ACTIVITIES);
try {
Map result = getDataUsageByUid(nsm, tm, type, start, end);
if(result == null)return null;
for (ApplicationInfo info: list) {
DataUsageWifi data = result.get(info.uid);
if(data == null)
continue;
data.packageName = info.packageName;
}
return result;
} catch (RemoteException e) {
e.printStackTrace();
return null;
}
}
private Map getDataUsageByUid(NetworkStatsManager nsm, TelephonyManager tm,
int type, long start, long end) throws RemoteException {
Log.d(TAG, "listDataUsageByUid: executed");
// 获取subscriberId
String subId = tm.getSubscriberId();
if((type==ConnectivityManager.TYPE_MOBILE) && (subId==null)){
Log.e(TAG, "listDataUsageByUid: get sub id fail" );
return null;
}
android.app.usage.NetworkStats summaryStats;
android.app.usage.NetworkStats.Bucket summaryBucket
= new android.app.usage.NetworkStats.Bucket();
Map map = new HashMap<>();
//此处的type用于决定是获取Mobile通道的流量数据还是获取Wifi通道的流量数据
//ConnectivityManager.TYPE_MOBILE
//ConnectivityManager.TYPE_WIFI
summaryStats = nsm.querySummary(type, subId, start, end);
do {
summaryStats.getNextBucket(summaryBucket);
int summaryUid = summaryBucket.getUid();
long summaryRx = summaryBucket.getRxBytes();
long summaryTx = summaryBucket.getTxBytes();
Log.i(TAG, "uid:" + summaryBucket.getUid() + " rx:" + summaryBucket.getRxBytes() +
" tx:" + summaryBucket.getTxBytes());
DataUsageWifi info = new DataUsageWifi(summaryUid, "", summaryRx, summaryTx);
map.put(summaryUid, info);
} while (summaryStats.hasNextBucket());
return map;
}
NetworkStatsManager还有其他几个方法,可以通过UID去获取指定应用的流量,实现起来更加方便。至于性能方面有没有差异,还没有仔细去研究,下一步将会去探讨这些,以及Context.NETWORK_STATS_SERVICE中的这个服务到底都做了哪些活。
刚开始学习总结工作内容,想到什么写什么,感觉很乱咯。稍后会将Demo的源码上传,有需要的朋友可以下载运行看看,同样需要系统签名,具体操作可以操考应用耗电量那一篇的方式进行操作。