耗電量API
Android系統中很早就有耗電量的API,只不過1直都是隱藏的,Android系統的設置-電池功能就是調用的這個API,該API的核心部份是調用了com.android.internal.os.BatteryStatsHelper類,利用PowerProfile類,讀取power_profile.xml文件,我們1起來看看具體如何計算耗電量,首先從最新版本6.0開始看
6.0的API
源碼
BatteryStatsHelper
其中計算耗電量的方法為490行的processAppUsage,下來1步1步來解釋該方法。
App耗電量的計算探究
private void processAppUsage(SparseArrayasUsers) {
方法的參數是1個SparseArray數組,存儲的對象是UserHandle,官方文檔給出的解釋是,代表1個用戶,可以理解為這個類里面存儲了用戶的相干信息.
final boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null);
然后判斷該次計算是不是針對所有用戶,通過UserHandle的USER_ALL值來判斷,該值為⑴,源碼的地址在https://github.com/DoctorQ/platform_frameworks_base/blob/android⑹.0.0_r1/core/java/android/os/UserHandle.java.
mStatsPeriod = mTypeBatteryRealtime;
然后給公共變量int類型的mStatsPeriod賦值,這個值mTypeBatteryRealtime的計算進程又在320行的refreshStats方法中:
mTypeBatteryRealtime = mStats.computeBatteryRealtime(rawRealtimeUs, mStatsType);
這里面用到了BatteryStats(mStats)類中的computeBatteryRealtime方法,該方法計算出此次統計電量的時間間隔。好,歪樓了,回到BatteryStatsHelper中。
BatterySipper osSipper = null; final SparseArray uidStats = mStats.getUidStats(); final int NU = uidStats.size();
首先創建1個BatterySipper對象osSipper,該對象里面可以存儲1些后續我們要計算的值,然后通過BatteryStats類對象mStats來得到1個包括Uid的對象的SparseArray組數,然后計算了1下這個數組的大小,保存在變量NU中。
for (int iu = 0; iu < NU; iu++) { final Uid u = uidStats.valueAt(iu); final BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP, u, 0);
然后for循環計算每一個Uid代表的App的耗電量,由于BatterySipper可計算的類型有3種:利用, 系統服務, 硬件類型,所以這個地方傳入的是DrainType.APP,還有其他可選類型以下:
public enum DrainType {
IDLE,
CELL,
PHONE,
WIFI,
BLUETOOTH,
FLASHLIGHT,
SCREEN,
APP,
USER,
UNACCOUNTED,
OVERCOUNTED,
CAMERA
}
羅列了目前可計算耗電量的模塊。
mCpuPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType) mWakelockPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType) mMobileRadioPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType) mWifiPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType) mBluetoothPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType) mSensorPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType) mCameraPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType) mFlashlightPowerCalculator.calculateApp(app, u, mRawRealtime, mRawUptime, mStatsType)
其中mStatsType的值為BatteryStats.STATS_SINCE_CHARGED,代表了我們的計算規則是從上次充滿電后數據,還有1種規則是STATS_SINCE_UNPLUGGED是拔掉USB線后的數據。而mRawRealtime是當前時間,mRawUptime是運行時間。6.0的對各個模塊的消耗都交給了單獨的類去計算,這些類都繼承于PowerCalculator抽象類:
藍牙耗電:BluetoothPowerCalculator.java
攝像頭耗電:CameraPowerCalculator.java Cpu耗電:CpuPowerCalculator.java
手電筒耗電:FlashlightPowerCalculator.java
無線電耗電:MobileRadioPowerCalculator.java
傳感器耗電:SensorPowerCalculator.java Wakelock耗電:WakelockPowerCalculator.java Wifi耗電:WifiPowerCalculator.java
這1部份我1會單獨拿出來挨個解釋,現在我們還是回到BatteryStatsHelper繼續往下走
final double totalPower = app.sumPower();
BatterySipper#sumPower方法是統計總耗電量,方法詳情以下,其中usagePowerMah這個值有點特殊,其他的上面都講過.
/**
* Sum all the powers and store the value into `value`.
* @return the sum of all the power in this BatterySipper.
*/ public double sumPower() { return totalPowerMah = usagePowerMah + wifiPowerMah + gpsPowerMah + cpuPowerMah +
sensorPowerMah + mobileRadioPowerMah + wakeLockPowerMah + cameraPowerMah +
flashlightPowerMah;
}
然后根據是不是是DEBUG版本打印信息,這個沒啥可說的,然后會把剛才計算的電量值添加到列表中:
if (totalPower != 0 || u.getUid() == 0) { final int uid = app.getUid(); final int userId = UserHandle.getUserId(uid); if (uid == Process.WIFI_UID) {
mWifiSippers.add(app);
} else if (uid == Process.BLUETOOTH_UID) {
mBluetoothSippers.add(app);
} else if (!forAllUsers && asUsers.get(userId) == null && UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) { Listlist = mUserSippers.get(userId); if (list == null) { list = new ArrayList<>();
mUserSippers.put(userId, list);
} list.add(app);
} else {
mUsageList.add(app);
} if (uid == 0) {
osSipper = app;
}
}
首先判斷totalPower的值和當前uid號是不是符合規則,規則為總耗電量不為0或用戶id為0.當uid表明為WIFI或藍牙時,添加到下面對應的列表中,1般情況下正常的利用我們直接保存到下面的mUsageList中就行就行,但是也有1些例外:
/**
* List of apps using power.
*/ private final ListmUsageList = new ArrayList<>(); /**
* List of apps using wifi power.
*/ private final ListmWifiSippers = new ArrayList<>(); /**
* List of apps using bluetooth power.
*/ private final ListmBluetoothSippers = new ArrayList<>();
如果我們的系統是單用戶系統,且當前的userId號不在我們的統計范圍內,且其進程id號是大于Process.FIRST_APPLICATION_UID(10000,系統分配給普通利用的其實id號),我們就要將其寄存到mUserSippers數組中,定義以下:
private final SparseArraymUserSippers = new SparseArray<>();
最后判斷uid為0的話,代表是Android操作系統的耗電量,賦值給osSipper(494行定義)就能夠了,這樣1個app的計算就完成了,遍歷部份就不說了,保存這個osSipper是為了最后1步計算:
if (osSipper != null) { // The device has probably been awake for longer than the screen on // time and application wake lock time would account for. Assign // this remainder to the OS, if possible.
mWakelockPowerCalculator.calculateRemaining(osSipper, mStats, mRawRealtime,
mRawUptime, mStatsType);
osSipper.sumPower();
}
主流程我們已介紹完了,下面來看各個子模塊耗電量的計算
Cpu耗電量
CpuPowerCalculator.java
Cpu的計算要用到PowerProfile類,該類主要是解析power_profile.xml:
<device name="Android"> <item name="none">0item> <item name="screen.on">0.1item> <item name="screen.full">0.1item> <item name="bluetooth.active">0.1item> <item name="bluetooth.on">0.1item> <item name="wifi.on">0.1item> <item name="wifi.active">0.1item> <item name="wifi.scan">0.1item> <item name="dsp.audio">0.1item> <item name="dsp.video">0.1item> <item name="camera.flashlight">0.1item> <item name="camera.avg">0.1item> <item name="radio.active">0.1item> <item name="radio.scanning">0.1item> <item name="gps.on">0.1item> <array name="radio.on"> <value>0.2value> <value>0.1value> array> <array name="cpu.speeds"> <value>400000value> array> <item name="cpu.idle">0.1item> <array name="cpu.active"> <value>0.1value> array> <item name="battery.capacity">1000item> <array name="wifi.batchedscan"> <value>.0002value> <value>.002value> <value>.02value> <value>.2value> <value>2value> array> device>
這個里面存儲了Cpu(cpu.speeds)的主頻等級,和每一個主頻每秒消耗的毫安(cpu.active),好,現在回到CpuPowerCalculator中,先來看構造方法
public CpuPowerCalculator(PowerProfile profile) { final int speedSteps = profile.getNumSpeedSteps();
mPowerCpuNormal = new double[speedSteps];
mSpeedStepTimes = new long[speedSteps]; for (int p = 0; p < speedSteps; p++) { mPowerCpuNormal[p] = profile.getAveragePower(PowerProfile.POWER_CPU_ACTIVE, p); } }
第1步取得Cpu有幾個主頻等級,由于不同等級消耗的電量不1樣,所以要區分對待,根據主頻的個數,然后初始化mPowerCpuNormal和mSpeedStepTimes,前者用來保存不同等級的耗電速度,后者用來保存在不同等級上耗時,然后給mPowerCpuNormal的每一個元素附上值。構造方法就完成了其所有的工作,現在來計算方法calculateApp,
final int speedSteps = mSpeedStepTimes.length;
long totalTimeAtSpeeds = 0; for (int step = 0; step < speedSteps; step++) {
mSpeedStepTimes[step] = u.getTimeAtCpuSpeed(step, statsType);
totalTimeAtSpeeds += mSpeedStepTimes[step];
}
totalTimeAtSpeeds = Math.max(totalTimeAtSpeeds, 1);
首先得到Cpu主頻等級個數,然后BatteryStats.Uid得到不同主頻上履行時間,計算Cpu總耗時保存在totalTimeAtSpeeds中,
app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000
Cpu的履行時間分很多部份,但是我們關注User和Kernal部份,也就是上面的UserCpuTime和SystemCpuTime。
double cpuPowerMaMs = 0; for (int step = 0; step < speedSteps; step++) {
final double ratio = (double) mSpeedStepTimes[step] / totalTimeAtSpeeds;
final double cpuSpeedStepPower = ratio * app.cpuTimeMs * mPowerCpuNormal[step]; if (DEBUG && ratio != 0) { Log.d(TAG, "UID " + u.getUid() + ": CPU step #" + step + " ratio=" + BatteryStatsHelper.makemAh(ratio) + " power=" + BatteryStatsHelper.makemAh(cpuSpeedStepPower / (60 * 60 * 1000)));
}
cpuPowerMaMs += cpuSpeedStepPower;
}
上面的代碼就是將不同主頻的消耗累加到1起,但是其中值得注意的是,他其實不是用各個主頻的消耗時間*主頻單位時間內消耗的電量,而是用1個radio變量來計算得到各個主頻段履行時間占總時間的百分比,然后用cpuTimeMs來換算成各個主頻的Cpu實際消耗時間,這比5.0的API多了這么1步,我估計是發現了計算的不嚴謹性,這也是Android遲遲不放出統計電量方式的緣由,其實google自己對這塊也沒有掌控,所以才會造成不同API計算方式的差異。好,計算完我們的總消耗后,是否是就算完事了?如果你只需要得到1個App的耗電總量,上面的講授已足夠了,但是6.0的API計算了每一個App的不同進程的耗電量,這個我們就只當看看就行,暫時沒甚么實際意義。
double highestDrain = 0;
app.cpuFgTimeMs = 0; final ArrayMap processStats = u.getProcessStats(); final int processStatsCount = processStats.size(); for (int i = 0; i < processStatsCount; i++) { final BatteryStats.Uid.Proc ps = processStats.valueAt(i); final String processName = processStats.keyAt(i);
app.cpuFgTimeMs += ps.getForegroundTime(statsType); final long costValue = ps.getUserTime(statsType) + ps.getSystemTime(statsType)
+ ps.getForegroundTime(statsType); if (app.packageWithHighestDrain == null ||
app.packageWithHighestDrain.startsWith("*")) {
highestDrain = costValue;
app.packageWithHighestDrain = processName;
} else if (highestDrain < costValue && !processName.startsWith("*")) {
highestDrain = costValue;
app.packageWithHighestDrain = processName;
}
} if (app.cpuFgTimeMs > app.cpuTimeMs) { if (DEBUG && app.cpuFgTimeMs > app.cpuTimeMs + 10000) {
Log.d(TAG, "WARNING! Cputime is more than 10 seconds behind Foreground time");
} app.cpuTimeMs = app.cpuFgTimeMs;
}
上面統計同1App下不同的進程的耗電量,得到消耗最大的進程名,保存到BatterySipper對象中,然后得出App的Cpu的foreground消耗時間,將foreground時間與之前計算得到的cpuTimeMs進行比較,如果foreground時間比cpuTimeMs還要大,那末就將cpuTimeMs的時間改變成foreground的值,但是這個值的變化對之前耗電總量的計算沒有絲毫影響。
// Convert the CPU power to mAh
app.cpuPowerMah = cpuPowerMaMs / (60 * 60 * 1000);
最后的最后,將耗電量用mAh單位來表示,所以在毫秒的基礎上除以60*60*1000。
總結:Cpu耗電量的計算是要辨別不同主頻的,頻率不同,單位時間內消耗的電量是有辨別的,這1點要明白。還有1點就是不同主頻上的履行時間不是通過BatteryStats.Uid#getTimeAtCpuSpeed方法得到的,210是通過百分比和BatteryStats.Uid#getUserCpuTimeUs和getSystemCpuTimeUs計算得到cpuTimeMs乘積得到的。最后1點就是,cpuTimeMs時間是會在計算終了落后行比較,比較的對象是CPU的foreground時間。
WakeLock耗電量的計算
WakelockPowerCalculator.java
從構造方法開始,
public WakelockPowerCalculator(PowerProfile profile) {
mPowerWakelock = profile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
}
首先得到power_profile.xml中cpu.awake表示的值,保存在mPowerWakelock變量中。構造方法只做了這么點事,下面進入calculateApp方法。
@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
long wakeLockTimeUs = 0 final ArrayMap.Uid.Wakelock> wakelockStats =
u.getWakelockStats() final int wakelockStatsCount = wakelockStats.size() for (int i = 0 final BatteryStats.Uid.Wakelock wakelock = wakelockStats.valueAt(i) // Only care about partial wake locks since full wake locks
// are canceled when the user turns the screen off.
BatteryStats.Timer timer = wakelock.getWakeTime(BatteryStats.WAKE_TYPE_PARTIAL) if (timer != null) {
wakeLockTimeUs += timer.getTotalTimeLocked(rawRealtimeUs, statsType) }
}
app.wakeLockTimeMs = wakeLockTimeUs / 1000 mTotalAppWakelockTimeMs += app.wakeLockTimeMs // Add cost of holding a wake lock.
app.wakeLockPowerMah = (app.wakeLockTimeMs * mPowerWakelock) / (1000*60*60) if (DEBUG && app.wakeLockPowerMah != 0) {
Log.d(TAG, "UID " + u.getUid() + ": wake " + app.wakeLockTimeMs + " power=" + BatteryStatsHelper.makemAh(app.wakeLockPowerMah)) }
}
首先取得Wakelock的數量,然后逐一遍歷得到每一個Wakelock對象,得到該對象后,得到BatteryStats.WAKE_TYPE_PARTIAL的喚醒時間,然后累加,其實wakelock有4種,為何只取partial的時間,具體代碼google也沒解釋的很清楚,只是用1句注釋打發了我們。得到總時間后,就能夠與構造方法中的單位時間waklock消耗電量相乘得到Wakelock消耗的總電量。
Wifi耗電量的計算
首先來看構造方法,來了解1下WIFI的耗電量計算用到了power_profile.xml中的哪些屬性:
public WifiPowerCalculator(PowerProfile profile) {
mIdleCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_IDLE) mTxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_TX) mRxCurrentMa = profile.getAveragePower(PowerProfile.POWER_WIFI_CONTROLLER_RX) }
我們去PowerProfile.java找到上面3個常量代表的屬性:
public static final String POWER_WIFI_CONTROLLER_IDLE = "wifi.controller.idle"; public static final String POWER_WIFI_CONTROLLER_RX = "wifi.controller.rx"; public static final String POWER_WIFI_CONTROLLER_TX = "wifi.controller.tx";
知道對應的xml的屬性后我們直接看calculateApp方法:
@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
long rawUptimeUs, int statsType) {
final long idleTime = u.getWifiControllerActivity(BatteryStats.CONTROLLER_IDLE_TIME,
statsType) final long txTime = u.getWifiControllerActivity(BatteryStats.CONTROLLER_TX_TIME, statsType) final long rxTime = u.getWifiControllerActivity(BatteryStats.CONTROLLER_RX_TIME, statsType) app.wifiRunningTimeMs = idleTime + rxTime + txTime app.wifiPowerMah =
((idleTime * mIdleCurrentMa) + (txTime * mTxCurrentMa) + (rxTime * mRxCurrentMa))
/ (1000*60*60) mTotalAppPowerDrain += app.wifiPowerMah app.wifiRxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_RX_DATA,
statsType) app.wifiTxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_WIFI_TX_DATA,
statsType) app.wifiRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_RX_DATA,
statsType) app.wifiTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_WIFI_TX_DATA,
statsType) if (DEBUG && app.wifiPowerMah != 0) {
Log.d(TAG, "UID " + u.getUid() + ": idle=" + idleTime + "ms rx=" + rxTime + "ms tx=" +
txTime + "ms power=" + BatteryStatsHelper.makemAh(app.wifiPowerMah)) }
}
這里的計算方式也是差不多,先根據Uid得到時間,然后乘以構造方法里對應的wifi類型單位時間內消耗電量值,沒甚么難點,就不逐一分析,需要注意的是,這里面還計算了wifi傳輸的數據包的數量和字節數。
藍牙耗電量的計算
藍牙關注的power_profile.xml中的屬性以下:
public static final String POWER_BLUETOOTH_CONTROLLER_IDLE = "bluetooth.controller.idle"; public static final String POWER_BLUETOOTH_CONTROLLER_RX = "bluetooth.controller.rx"; public static final String POWER_BLUETOOTH_CONTROLLER_TX = "bluetooth.controller.tx";
但是還沒有單獨為App計算耗電量的,所以這個地方是空的。
@Override public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs, long rawUptimeUs, int statsType) { }
攝像頭耗電量的計算
CameraPowerCalculator.java
攝像頭的耗電量關注的是power_profile.xml中camera.avg屬性代表的值,保存到mCameraPowerOnAvg,
public static final String POWER_CAMERA = "camera.avg";
計算方式以下:
@Override public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs, long rawUptimeUs, int statsType) { final BatteryStats.Timer timer = u.getCameraTurnedOnTimer(); if (timer != null) { final long totalTime = timer.getTotalTimeLocked(rawRealtimeUs, statsType) / 1000;
app.cameraTimeMs = totalTime;
app.cameraPowerMah = (totalTime * mCameraPowerOnAvg) / (1000*60*60);
} else {
app.cameraTimeMs = 0;
app.cameraPowerMah = 0;
}
}
先計算攝像頭打開的時間totalTime,然后根據這個值乘以mCameraPowerOnAvg得到攝像頭的耗電量。
手電筒耗電量的計算
FlashlightPowerCalculator.java
public static final String POWER_FLASHLIGHT = "camera.flashlight";
跟攝像頭類似,也是先得到時間,然后乘積,不想說了,沒意思。
無線電耗電量的計算
MobileRadioPowerCalculator.java
關注的是power_profile.xml中以下3個屬性:
/**
* Power consumption when screen is on, not including the backlight power.
*/ public static final String POWER_SCREEN_ON = "screen.on"; /**
* Power consumption when cell radio is on but not on a call.
*/ public static final String POWER_RADIO_ON = "radio.on"; /**
* Power consumption when cell radio is hunting for a signal.
*/ public static final String POWER_RADIO_SCANNING = "radio.scanning";
當無窮量連接上時,根據信號強度不同,耗電量的計算是有區分的,所以在構造方法,當無線電的狀態為on時,是要特殊處理的,其他兩個狀態(active和scan)就正常取值就能夠了。
/**
* Power consumption when screen is on, not including the backlight power.
*/ public static final String POWER_SCREEN_ON = "screen.on"; /**
* Power consumption when cell radio is on but not on a call.
*/ public static final String POWER_RADIO_ON = "radio.on"; /**
* Power consumption when cell radio is hunting for a signal.
*/ public static final String POWER_RADIO_SCANNING = "radio.scanning";
計算的方式分兩種,以無線電處于active狀態的次數為辨別,當active大于0,我們用途于active狀態的時間來乘以它的單位耗時。另外一種情況就要根據網絡轉化的數據包來計算耗電量了。
傳感器耗電量的計算
SensorPowerCalculator.java
只關注1個屬性:
public static final String POWER_GPS_ON = "gps.on";
計算方式以下:
@Override public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs, long rawUptimeUs, int statsType) { final SparseArray sensorStats = u.getSensorStats(); final int NSE = sensorStats.size(); for (int ise = 0; ise < NSE; ise++) { final BatteryStats.Uid.Sensor sensor = sensorStats.valueAt(ise); final int sensorHandle = sensorStats.keyAt(ise); final BatteryStats.Timer timer = sensor.getSensorTime(); final long sensorTime = timer.getTotalTimeLocked(rawRealtimeUs, statsType) / 1000; switch (sensorHandle) { case BatteryStats.Uid.Sensor.GPS:
app.gpsTimeMs = sensorTime;
app.gpsPowerMah = (app.gpsTimeMs * mGpsPowerOn) / (1000*60*60); break; default: final int sensorsCount = mSensors.size(); for (int i = 0; i < sensorsCount; i++) { final Sensor s = mSensors.get(i); if (s.getHandle() == sensorHandle) {
app.sensorPowerMah += (sensorTime * s.getPower()) / (1000*60*60); break;
}
} break;
}
}
}
當傳感器的類型為GPS時,我們計算每一個傳感器的時間然后乘以耗電量,和所有的耗電量計算都是1樣,不同的是,當傳感器不是GPS時,這個時候計算就根據SensorManager得到所有傳感器類型,這個里面保存有不同傳感器的單位耗電量,這樣就可以計算不同傳感器的耗電量。
總結
至此我已把App耗電量的計算講完了(還有硬件),前后花費3天時間,好痛苦(此處1萬只草泥馬),不過好在自己也算對這個耗電量的理解有了1定的認識。google官方對耗電量的統計給出的解釋都是不能代表真實數據,只能作為參考值,由于受power_profile.xml的干擾太大,如果手機廠商沒有嚴格設置這個文件,那可想而知出來的值多是不公道的。
提示
騰訊的GT團隊頭幾天推出了耗電量的計算APK,原理是1樣的,大家可以試用下GT