耗電的背景知識
電池的關鍵指標:
- 電池容量。
- 充電時間。
- 壽命。隨著使用時間的延長,電池容量會降低。例如蘋果官方資料,500 次充電迴圈 iPhone 電池的剩餘容量為原來的 80%。
- 安全性。三星 Note 7 爆炸。
耗電模組
電能 = 電壓 * 電流 * 時間
power_profiler.xml 檔案定義了不同模組的電流消耗值以及該模組在一段時間內大概消耗的電流。可以參考官方文件《Android 電源配置檔案》。電流的大小和當前模組的狀態也有關係。
螢幕
adb shell dumpsys batterystats > battery.txt
// 各個 Uid 的總耗電量,而且是粗略的電量計算估計。
Estimated power use (mAh):
Capacity: 3450, Computed drain: 501, actual drain: 552-587
...
Idle: 41.8
Uid 0: 135 ( cpu=103 wake=31.5 wifi=0.346 )
Uid u0a208: 17.8 ( cpu=17.7 wake=0.00460 wifi=0.0901 )
Uid u0a65: 17.5 ( cpu=12.7 wake=4.11 wifi=0.436 gps=0.309 )
...
// reset 電量統計
adb shell dumpsys batterystats --reset
複製程式碼
BatteryStatsService 是對外的電量統計服務,具體實現邏輯在 BatteryStatsImpl 中。BatteryStatsImpl 內部使用的就是 PowerProfile,為每個應用建立一個 UID 例項來監控應用系統資源的使用情況。
優化案例:《大眾點評 App 的短視訊耗電量優化實戰》。
反饋耗電問題時,bug report 和 Battery Historian 是最好的排查方法。
//7.0 和 7.0 以後
$ adb bugreport bugreport.zip
//6.0 和 6.0 之前:
$ adb bugreport > bugreport.txt
// 通過 historian 圖形化展示結果
python historian.py -a bugreport.txt > battery.html
複製程式碼
Android 每年都在為了電量優化做努力,在整個 Android 的發展過程中,分幾個階段:
- 野蠻生長:Pre Android 5.0。
- 逐步收緊:Android 5.0~8.0。5.0 專門開啟一個 Volta 專案,改善電池的續航。
- 最嚴限制:Android 9.0。9.0 開始的電源管理,引入了更加嚴格的限制。
耗電優化
耗電優化的優先順序:
- 優化應用的後臺耗電,符合使用者心理預期。
- 符合系統規則,讓系統認為你的耗電正常,避免彈出“高耗電警告”。
耗電優化的難點:
- 缺乏現場,無法復現。
- 資訊不全,難以定位。統計無法定位到堆疊,無法直接定位到具體原因。
- 無法評估結果。
耗電的主要原因:
- 需求導向,保活。例如:推送、後臺任務。
- 程式碼的 Bug。GPS 沒有關閉、WakeLock 沒有釋放。
從主要問題出發,整理優化思路:
- 找到需求場景的替代方案。使用廠商通道、foreground service 或者引導使用者加白名單。後臺任務的優化思想是:減少、延遲和合並。參考:《Android 後臺排程任務與省電》。
- 符合 Android 規則。參考微信的《監控電池電量和充電狀態》
- 異常情況監控。系統會允許部分合規的後臺邏輯,參考:Android P 的電量管理、電量管理。
Android Vitals 在過年其實根本無法使用。
參考:
華為的耗電紅線
監控耗電的方法:
// 代理 PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);
@Override
public void beforeInvoke(Method method, Object[] args) {
// 申請 Wakelock
if (method.getName().equals("acquireWakeLock")) {
if (isAppBackground()) {
// 應用後臺邏輯,獲取應用堆疊等等
} else {
// 應用前臺邏輯,獲取應用堆疊等等
}
// 釋放 Wakelock
} else if (method.getName().equals("releaseWakeLock")) {
// 釋放的邏輯
}
}
複製程式碼
// 代理 AlarmManagerService
new ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);
public void beforeInvoke(Method method, Object[] args) {
// 設定 Alarm
if (method.getName().equals("set")) {
// 不同版本引數型別的適配,獲取應用堆疊等等
// 清除 Alarm
} else if (method.getName().equals("remove")) {
// 清除的邏輯
}
}
複製程式碼
- 插樁。解決 Android P 之後,很多 Hook 的手段不支援的問題。參考 Facebook 的耗電監控開源庫 Battery-Metrics
public class WakelockMetrics {
// Wakelock 申請
public void acquire(PowerManager.WakeLock wakelock) {
wakeLock.acquire();
// 在這裡增加 Wakelock 申請監控邏輯
}
// Wakelock 釋放
public void release(PowerManager.WakeLock wakelock, int flags) {
wakelock.release();
// 在這裡增加 Wakelock 釋放監控邏輯
}
}
複製程式碼
步驟
實現
Hook
實現了IAlarmManagerHook、IPowerManagerHook、ILocationManagerHook這三那個代理,分別實現對AlarmManager、PowerManager、LocationManager的監聽。
public class BatteryHookManager {
/**
* 初始化
* @param context
*/
public void initHook(Context context){
//初始化日誌
BatteryLogUtil.init(context);
//初始化Hook
new IAlarmManagerHook(context).onInstall();
new IPowerManagerHook(context).onInstall();
new ILocationManagerHook(context).onInstall();
}
private BatteryHookManager(){}
private static class BatteryHookManagerHolder{
private static final BatteryHookManager INSTANCE= new BatteryHookManager();
}
public static BatteryHookManager getImp(){
return BatteryHookManagerHolder.INSTANCE;
}
}
複製程式碼
在專案的application裡面初始化BatteryHook
public class BatteryApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
BatteryHookManager.getImp().initHook(this);
}
}
複製程式碼
檢測錯誤的UI繪製重新整理導致的耗電
排除由於錯誤的繪製方法,導致CPU佔用過高,進而導致耗電量高
檢測方式參考大眾點評App的短視訊耗電量優化實戰
首先開啟開發者選項,開啟GPU檢視更新的開關,然後看看應用內部有哪些不必要的UI重新整理,
場景一(自定義TextView)
首頁快速組隊頁面,列表中檢視更多在一直重新整理,直接看程式碼,
這裡有一個自定義View,繼續看TextViewWideContent的程式碼
分析程式碼發現在onDraw()裡面呼叫了setPadding(),繼續看setPadding()的原始碼,
可以看到這裡面呼叫了頁面invalidate(),這就導致了onDraw()方法的迴圈呼叫,所以頁面會持續重新整理。結論就是在自定義View的onDraw()方法裡面,呼叫setPadding(),會導致頁面重複繪製 解決辦法也很簡單把setPadding放到onLayout()裡面去。
場景二(CoordinatorLayout+AppBarLayout錯誤的依賴關係)
在CoordinatorLayout+AppBarLayout頁面結構中,錯誤的將底部依賴於頭部控制元件,導致底部一直在重新整理,去掉 app:layout_anchor="@id/user_appbar_layout" 就好了。
Android電量優化的相關建議
①在需要網路連線的程式中,首先檢查網路連線是否正常,如果沒有網路連線,那麼就不需要執行相應的程式;
②判斷網路型別,針對特定的資料在特定的網路下請求.例如:大量資料傳輸的時候在wifi下請求;wifi下下載資料耗電量只有2、3、4G的1/3.
③使用效率高的資料格式和解析方法,推薦使用JSON和Protobuf;
④在進行大資料量下載時,儘量使用GZIP方式下載;
⑤使用推送,代替迴圈請求
⑥其它:
儘量不要使用浮點運算;
回收java物件,特別是較大的java對像,使用reset方法;
主動回收java物件,特別是較大的,例如bitmap。減少GC的工作頻率;
避免記憶體抖動,記憶體抖動是因為大量的物件被建立又在短時間內馬上被釋放;
避免在for迴圈、onDraw方法中建立物件;無法避免的可以建立物件池,然後在不使用的時候釋放;
對定位要求不是太高的話儘量不要使用GPS定位,可以使用wifi和行動網路cell定位即可;
獲取螢幕尺寸等資訊可以使用快取技術,不需要進行多次請求;
使用AlarmManager來定時啟動服務替代使用sleep方式的定時任務;