在在线网站http://androidxref.com/上对Android版本6.0.1_r10源码进行分析

官方手机的应用耗电排行具体实现位置在:/packages/apps/Settings/src/com/android/settings/fuelgauge/PowerUsageSummary.java

PowerUsageSummary类的作用是筛选耗电量最多的前十个应用并且展示

PowerUsageSummary`类继承自 `PowerUsageBase

开始的一部分的UI界面的创建和一些常量的定义,比如:

  • USE_FAKE_DATA,定义是否要使用假数据;
  • private BatteryHistoryPreference mHistPref;BatteryHistoryPreference类获取耗电量历史数据(读取sp文件)

sp文件数据来自power_usage_summary.xml文件

  • PreferenceGroup类:统计所有APP耗电量

主要目光放在refreshStats方法里

super.refreshStats();

跟进父类方法

protected void refreshStats() {
mStatsHelper.refreshStats(BatteryStats.STATS_SINCE_CHARGED, mUm.getUserProfiles());
}

BatteryStats.STATS_SINCE_CHARGED传入的是我们的计算规则

  • STATS_SINCE_CHARGED 上次充满电后数据
  • STATS_SINCE_UNPLUGGED 拔掉USB线后的数据

mUm.getUserProfiles() 是传入的多用户

mUm = (UserManager) activity.getSystemService(Context.USER_SERVICE);

这也是由Android的安全机制导致的,即多用户下的多应用

mStatsHelper.refreshStats方法现在我们只要知道是刷新当前的电量统计的就行

然后是一些UI的刷新,该部分略过

final PowerProfile powerProfile = mStatsHelper.getPowerProfile();
final BatteryStats stats = mStatsHelper.getStats();
final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);

可以看到mStatsHelper无处不在,实际上电量统计的核心实现就是该部分实现的

mStatsHelper.getPowerProfile()获取电源的配置信息,浅跟进一下

public PowerProfile getPowerProfile() {
return mPowerProfile;
}

初始化是在这里

public void create(BatteryStats stats) {
mPowerProfile = new PowerProfile(mContext);
mStats = stats;
}

持续跟进

public PowerProfile(Context context) {
// Read the XML file for the given profile (normally only one per
// device)
if (sPowerMap.size() == 0) {
readPowerValuesFromXml(context);
}
initCpuClusters();
}

可以看到这里有一段注释: Read the XML file for the given profile (normally only one perdevice

跟进readPowerValuesFromXml方法,其实这个方法就是用来解析power_profile.xml文件的,该文件在源码中的位置为 /frameworks/base/core/res/res/xml/power_profile.xmlpower_profile.xml是一个可配置的功耗数据文件

private void readPowerValuesFromXml(Context context) {
int id = com.android.internal.R.xml.power_profile;
final Resources resources = context.getResources();
XmlResourceParser parser = resources.getXml(id);
boolean parsingArray = false;
ArrayList<Double> array = new ArrayList<Double>();
String arrayName = null; try {
// ....

在这里需要提一下Android中对于应用和硬件的耗电量计算方式:

有一张“价格表”,记录每种硬件1秒钟耗多少电。有一张“购物清单”,记录apk使用了哪几种硬件,每种硬件用了多长时间。假设某个应用累计使用了60秒的cpu,cpu1秒钟耗1mAh,那这个应用就消耗了60mAh的电

这里的价格表就是我们找到的power_profile.xml文件,手机的硬件是各不相同的,所以每一款手机都会有一张自己的"价格表",这张表的准确性由手机厂商负责。

这也是为什么我们碰到读取xml文件的时候注释里面会有normally only one perdevice

如果我们想要看自己手机的power_profile.xml文件咋办,它会存储在手机的/system/framework/framework-res.apk路径中,我们可以将它pull出来,通过反编译的手法获得power_profile.xml文件

mStatsHelper.getStats()返回BatteryStats对象,跟进可以发现实际上返回的是BatteryStatsImpl,它描述了所有与电量消耗有关的信息

final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);

见名知意,获取设备的平均耗电量,用于与阈值进行对比

这部分看上去是界面和主题的显示

TypedValue value = new TypedValue();
getContext().getTheme().resolveAttribute(android.R.attr.colorControlNormal, value, true);
int colorControl = getContext().getColor(value.resourceId);

检查消耗的电量是否大于阈值,以及是否使用假数据,否则不显示应用耗电量

if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {

根据UID进行合并分组

final List<BatterySipper> usageList = getCoalescedUsageList(USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList());

其中getCoalescedUsageList方法对UID进行分组

getFakeStats()方法返回一堆假数据

private static List<BatterySipper> getFakeStats() {
ArrayList<BatterySipper> stats = new ArrayList<>();
float use = 5;
for (DrainType type : DrainType.values()) {
if (type == DrainType.APP) {
continue;
}
stats.add(new BatterySipper(type, null, use));
use += 5;
}
stats.add(new BatterySipper(DrainType.APP,
new FakeUid(Process.FIRST_APPLICATION_UID), use));
stats.add(new BatterySipper(DrainType.APP,
new FakeUid(0), use)); // Simulate dex2oat process.
BatterySipper sipper = new BatterySipper(DrainType.APP,
new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID)), 10.0f);
sipper.packageWithHighestDrain = "dex2oat";
stats.add(sipper); sipper = new BatterySipper(DrainType.APP,
new FakeUid(UserHandle.getSharedAppGid(Process.FIRST_APPLICATION_UID + 1)), 10.0f);
sipper.packageWithHighestDrain = "dex2oat";
stats.add(sipper); sipper = new BatterySipper(DrainType.APP,
new FakeUid(UserHandle.getSharedAppGid(Process.LOG_UID)), 9.0f);
stats.add(sipper); return stats;
}

mStatsHelper.getUsageList()返回BatterySipper数组,每个BatterySipper代表一个应用(uid)的消耗的电量信息

BatteryStatsHelper.java中的refreshStats方法中对mUsageList进行了赋值,这部分的具体操作在分析BatteryStatsHelper.java的时候再提

final int dischargeAmount = USE_FAKE_DATA ? 5000
: stats != null ? stats.getDischargeAmount(mStatsType) : 0;

这里的mStatsType值为

private int mStatsType = BatteryStats.STATS_SINCE_CHARGED;

这里我们前面提过,含义是

  • STATS_SINCE_CHARGED 上次充满电后数据
  • STATS_SINCE_UNPLUGGED 拔掉USB线后的数据

所以这段的含义是获取上次充满电之后的电量消耗

stats.getDischargeAmount(mStatsType)

接下来遍历BatterySipper,对每一个UID代表的APP的耗电量进行过滤

final int numSippers = usageList.size();
for (int i = 0; i < numSippers; i++) {
final BatterySipper sipper = usageList.get(i);
if ((sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) {
continue;
}
double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower();
final double percentOfTotal =
((sipper.totalPowerMah / totalPower) * dischargeAmount);
if (((int) (percentOfTotal + .5)) < 1) {
continue;
}

如果耗电功率小于阈值则不进行显示

if ((sipper.totalPowerMah * SECONDS_IN_HOUR) < MIN_POWER_THRESHOLD_MILLI_AMP) {

获取设备总耗电量

double totalPower = USE_FAKE_DATA ? 4000 : mStatsHelper.getTotalPower();

计算占用总耗电量的百分比

final double percentOfTotal =
((sipper.totalPowerMah / totalPower) * dischargeAmount);

如果比例小于0.5,则不进行下一步操作

if (((int) (percentOfTotal + .5)) < 1) {
continue;
}

对某些情况进行过滤

if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) {
// Don't show over-counted unless it is at least 2/3 the size of
// the largest real entry, and its percent of total is more significant
if (sipper.totalPowerMah < ((mStatsHelper.getMaxRealPower()*2)/3)) {
continue;
}
if (percentOfTotal < 10) {
continue;
}
if ("user".equals(Build.TYPE)) {
continue;
}
}
if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) {
// Don't show over-counted unless it is at least 1/2 the size of
// the largest real entry, and its percent of total is more significant
if (sipper.totalPowerMah < (mStatsHelper.getMaxRealPower()/2)) {
continue;
}
if (percentOfTotal < 5) {
continue;
}
if ("user".equals(Build.TYPE)) {
continue;
}
}

进行UI界面的更新,其中也包含了获取应用的icon图标

final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid()));
final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper);
final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(),
userHandle);
final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(),
userHandle);
final PowerGaugePreference pref = new PowerGaugePreference(getActivity(),
badgedIcon, contentDescription, entry);

获取当前应用的最大百分比,以及占总数的百分比

final double percentOfMax = (sipper.totalPowerMah * 100)
/ mStatsHelper.getMaxPower();
sipper.percent = percentOfTotal;

UI更新

pref.setTitle(entry.getLabel());
pref.setOrder(i + 1);
pref.setPercent(percentOfMax, percentOfTotal);
if (sipper.uidObj != null) {
pref.setKey(Integer.toString(sipper.uidObj.getUid()));
}
if ((sipper.drainType != DrainType.APP || sipper.uidObj.getUid() == 0)
&& sipper.drainType != DrainType.USER) {
pref.setTint(colorControl);
}
addedSome = true;
mAppListGroup.addPreference(pref);
if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST + 1)) {
break;
}

其中这里对显示的数量进行了限制

if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST + 1)) {
break;
}

MAX_ITEMS_TO_LIST的赋值

private static final int MAX_ITEMS_TO_LIST = USE_FAKE_DATA ? 30 : 10;

循环外有对addedSome的判断

if (!addedSome) {
addNotAvailableMessage();
}

实际上就是判断是不是有符合要求的耗电应用,如果没有的话,就显示一条提示信息

private void addNotAvailableMessage() {
Preference notAvailable = new Preference(getActivity());
notAvailable.setTitle(R.string.power_usage_not_available);
mAppListGroup.addPreference(notAvailable);
}

这部分就是PowerUsageSummary.java文件获取Settings电池中显示的应用耗电量信息,根据我们上面的分析,实际上控制上面的continue就能获取全部已安装应用的耗电量。在Android的不同API版本中,会有一些适配的工作量

关于申请权限,普通应用是没有办法获取到应用耗电量信息的,系统会抛出异常

java.lang.SecurityException: uid 10089 does not have android.permission.BATTERY_STATS.

如果想要进行相关API的调用,首先应用需要配置android.uid.system成为系统应用,并且进行系统签名,才能够拥有相关权限,本地编译的话需要调用Android的internal接口,我使用的是替换本地android.jar才可以正常打包出apk文件

本地编写了一个获取Android应用耗电量的demo,运行截图如下

END

建了一个微信的安全交流群,欢迎添加我微信备注进群,一起来聊天吹水哇,以及一个会发布安全相关内容的公众号,欢迎关注


PowerUsageSummary.java源码分析的更多相关文章

  1. Java源码分析 | CharSequence

    本文基于 OracleJDK 11, HotSpot 虚拟机. CharSequence 定义 CharSequence 是 java.lang 包下的一个接口,是 char 值的可读序列, 即其本身 ...

  2. Java源码分析:关于 HashMap 1.8 的重大更新(转载)

    http://blog.csdn.net/carson_ho/article/details/79373134 前言 HashMap 在 Java 和 Android 开发中非常常见 而HashMap ...

  3. JAVA源码分析-HashMap源码分析(二)

    本文继续分析HashMap的源码.本文的重点是resize()方法和HashMap中其他的一些方法,希望各位提出宝贵的意见. 话不多说,咱们上源码. final Node<K,V>[] r ...

  4. Java源码分析之LinkedList

    LinkedList与ArrayList正好相对,同样是List的实现类,都有增删改查等方法,但是实现方法跟后者有很大的区别. 先归纳一下LinkedList包含的API 1.构造函数: ①Linke ...

  5. Java源码分析:Guava之不可变集合ImmutableMap的源码分析

    一.案例场景 遇到过这样的场景,在定义一个static修饰的Map时,使用了大量的put()方法赋值,就类似这样-- public static final Map<String,String& ...

  6. JAVA源码分析-HashMap源码分析(一)

    一直以来,HashMap就是Java面试过程中的常客,不管是刚毕业的,还是工作了好多年的同学,在Java面试过程中,经常会被问到HashMap相关的一些问题,而且每次面试都被问到一些自己平时没有注意的 ...

  7. 【转】【java源码分析】Map中的hash算法分析

    全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.Conc ...

  8. 【Java源码分析】LinkedList类

    LinkedList<E> 源码解读 继承AbstractSequentialList<E> 实现List<E>, Deque<E>, Cloneabl ...

  9. 一致性哈希Java源码分析

    首次接触一致性哈希是在学习memcached的时候,为了解决分布式服务器的负载均衡或者说选路的问题,一致性哈希算法不仅能够使memcached服务器被选中的概率(数据分布)更加均匀,而且使得服务器的增 ...

  10. JAVA源码分析------锁(1)

    http://870604904.iteye.com/blog/2258604 第一次写博客,也就是记录一些自己对于JAVA的一些理解,不足之处,请大家指出,一起探讨. 这篇博文我打算说一下JAVA中 ...

随机推荐

  1. kali配置

    IP设置 配置文件:/etc/networking/interface 临时IP ifconfig eth0 192.168.31.111/24 永久IP auto eth0 #iface eth0 ...

  2. LOJ2324「清华集训 2017」小Y和二叉树

    题目链接 瞎jb贪一发就过了.首先度数<=2且编号最小的点一定是中序遍历最靠前的点,我们从这个点开始dfs一遍算出子树中度数<=2且编号最小的点记为\(f(i)\),然后从这个点开始一步一 ...

  3. SpringBoot框架SpEL表达式注入漏洞复现与原理分析

    前言 这是2016年的一个洞,利用条件是至少知道一个触发 springboot 默认错误页面的接口及参数名. 影响版本:1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升级版本 ...

  4. VScode将代码提交到远程服务器、同时解决每次提交都要输入密码的问题(这里以gitee为例子)

    文章目录 1.第一种情况.项目直接从gitee上拉取下来 2.第二种情况.将新建的项目提交到远程服务器 3.解决将代码提交到远程每次都要输入用户名和密码 4.个人遇到的奇葩问题 1.第一种情况.项目直 ...

  5. pta第一次博客

    目录 pta第一次博客 1.前言 2.设计与分析 第二次作业第二题 第三次作业第一题 第三次作业第二题 第三次作业第三题 3.踩坑心得: 4.改进建议 5.总结 pta第一次博客 1.前言 这三次pt ...

  6. Vue ref 和 v-for 结合(ref 源码解析)

    前言 Vue 中组件的使用很方便,而且直接取组件实例的属性方法等也很方便,其中通过 ref 是最普遍的. 平时使用中主要是对一个组件进行单独设置 ref ,但是有些场景下可能是通过给定数据渲染的,这时 ...

  7. cordon节点,drain驱逐节点,delete 节点

    目录 一.系统环境 二.前言 三.cordon节点 3.1 cordon节点概览 3.2 cordon节点 3.3 uncordon节点 四.drain节点 4.1 drain节点概览 4.2 dra ...

  8. java学习之爬虫

    0x00前言 对比与Python的爬虫机制和java的爬虫机制来详解一下java的爬虫,对于一般性的需求无论java还是python都可以胜任. 如需要模拟登陆.对抗防采集选择python更方便些,如 ...

  9. OpenCvSharp的安装和使用

    OpencvSharp是opencv的C#版本,使用习惯了opencv的人学起OpenCvSharp会很容易上手,看了网上很多的安装方式,最后我感觉还是自己去下载安装包的方式最简单,通过Nuget的方 ...

  10. K8s 生产最佳实践-限制 NameSpace 资源用量

    前言 想象一下这个场景:多个系统运行在同一套 K8s 集群上,有重要系统,也有不太重要的系统.但是某一天,某个不重要的系统突然占用了该 K8s 集群的所有资源,导致该集群上的其他系统的正常运行受到影响 ...