PowerUsageSummary.java原始碼分析

春告鳥發表於2023-01-11

在線上網站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

建了一個微信的安全交流群,歡迎新增我微信備註進群,一起來聊天吹水哇,以及一個會發布安全相關內容的公眾號,歡迎關注 ?

GIF GIF

相關文章