在 Android 應用開發中,咱們須要考慮的是如何優化電量使用,讓咱們的 App 不會由於電量消耗太高被用戶排斥,或者被其餘安全應用報告,以此確保用戶黏性。html
開發中一直鏈接手機,不知道電量消耗有多快。java
咱們沒有辦法拿到每個用戶手機的組件能耗,其中不一樣的硬件模塊使用了不一樣的參數,而後使用了不一樣的算法來進行估算。可是,具體的參數值根據手機所使用的硬件來講是不同的。python
如今通常手機的電池容量會佔用內部組件將近一半的空間。android
P=UI(電功率=電壓 * 電流)git
一般使用充電循環次數衡量。github
嚴格控制電池容量,例如 VOOC 就使用了各類安全檢測技術。web
分批有效地收集和傳遞傳感器事件。算法
批處理在合理的類似時間內的全部應用的鬧鈴,以便系統僅喚醒一次。docker
對於電量的統計有一個公式,以下所示:shell
模塊電量(mAh) = 模塊電流(mA)* 模塊耗時(h)
複製代碼
Android 系統要求 ROM 廠商必須在 /frameworks/base/core/res/res/xml/power_profile.xml 提供組件的電源配置文件。而 Android 系統的電量計算 PowerProfile 正是經過讀取 power_profile.xml 的數據。
獲取電池電量、充電狀態、電池狀態等信息。
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_BATTERY_CHANGED); Intent intent = registerReceiver(null, filter); LogUtils.i("battery " + intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)); 複製代碼
batterystats 是 Android 5.0 提供的工具,它能夠獲取各個 App 的 WakeLock、CPU 時間佔用等信息,同時增長了一個 Estimated power use(mAh)功能,預估耗電量。
將電量測量轉化爲功能模塊的使用時間或者次數。
adb shell dumpsys batterystats > battery.txt
複製代碼
在 battery.txt 搜索 ‘Estimated power use’ 關鍵字,下面粗略統計了各個 Uid 的總耗電量。
Estimated power use (mAh): Capacity: 3350, Computed drain: 2767, actual drain: 3752-3853 Uid 1000: 1014 ( cpu=999 wake=1.36 radio=11.4 wifi=1.24 gps=0.435 sensor=0.808 ) Excluded from smearing Unaccounted: 985 ( ) Including smearing: 0 ( ) Excluded from smearing Uid 0: 416 ( cpu=157 wake=210 radio=38.8 wifi=9.51 ) Excluded from smearing ... 複製代碼
batterystats 所記錄的電量統計數據源自於 BatteryStatsService-電量統計服務,其實現類爲 BatteryStatsImpl,內部正是使用的 PowerProfile 。
BatteryStatsImpl 爲每個應用建立與之對應的 UID 來監控器系統資源的使用狀況,其統計了 12 大模塊的電量消耗,以下所示:
若是打不開,可使用備用網站 bathist
在 Add Metrics 中咱們能夠增長更多的測量項。
若是一直處於 running,則代表電量消耗比較高。
選中 Job Scheduler 的某一個工做時間片,咱們能夠查看具體的 發生的時間、耗時以及次數,最重要的是它統計出來了是哪個進程在使用這個 JobScheduler。
選擇多個文件進行上傳對比。
在 App 開發中,常常會因爲某個需求場景或 代碼 bug 而致使大量耗電。
對於耗電優化中,咱們最經常使用的就是 JobScheduler,下面👇,咱們來實戰一下。
/** * 開啓 JobScheduler */ private void startJobScheduler() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE); JobInfo.Builder builder = new JobInfo.Builder(1, new ComponentName(getPackageName(), JobSchedulerService.class.getName())); // 設置僅在 充電和WIFI 下才使用 JobScheduler 進行批量任務處理 builder.setRequiresCharging(true) .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); jobScheduler.schedule(builder.build()); } } 複製代碼
其中,「JobSchedulerService 就是用於進行批量任務處理的服務」,示例代碼以下所示:
/** * 用於進行批量任務處理的 JobSchedulerService */ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class JobSchedulerService extends JobService { @Override public boolean onStartJob(JobParameters params) { // 此處執行在主線程 // 模擬一些處理:批量網絡請求,APM日誌上報 return false; } @Override public boolean onStopJob(JobParameters params) { return false; } } 複製代碼
符合 Android 規則,手機在充電狀態纔去作耗電工做。示例代碼以下所示:
IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
Intent batteryStatus = context.registerReceiver(null, ifilter); //獲取用戶是否在充電的狀態或者已經充滿電了 int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1); boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL; 複製代碼
避免後臺長時間獲取 WakeLock、WIFI 和藍牙的掃描等。
Android P 使用了 Android Vitals 監控後臺耗電,其規則以下所示:
「Android 手機保護 AP 和 BP 兩個 CPU。AP 即 Application Processor,全部的用戶界面以及 App 都是運行在 AP 上的。BP 即 Baseband Processor,手機射頻都是運行在這個 CPU 上的。而通常咱們所說的耗電,PowerProfile 文件裏面的 CPU,指的是 AP」。
CPU 耗電一般有兩種狀況:
經常使用優化 CPU 時間片的方式有:
一般狀況下,使用 WIFI 鏈接網絡時的功耗要低於使用移動網絡的功耗。而使用移動網絡傳輸數據,電量的消耗有如下3種狀態:
所以,爲了不網絡鏈接所帶來的電量消耗,咱們能夠採用以下幾種方案:
WakeLock 經常使用於後臺播放音視頻、錄製音視頻、下載文件的狀況。若是沒有合理使用 WakeLock,則會形成嚴重的耗電問題,爲了不該問題,「咱們應該按期針對使用了 WakeLock 的模塊進行重點排查」。
咱們可使用 adb shell dumpsys power
命令查看系統當前的耗電信息,其中咱們能夠看到 WakeLock 列表,它一般會以 「」mLocks.size「 或者 」Wake Locks:size「」 開頭。關於 WakeLock 的使用咱們要着重注意如下幾點:
「浮點運算比整數運算更消耗 CPU 時間片,所以耗電也會增長」。避開浮點運算的優化方法以下所示:
「咱們能夠監聽滅屏以及亮屏的廣播,在滅屏的時候中止 surfaceView 的動畫繪製。在亮屏的時候,恢復動畫的繪製」。
之後臺耗電監控爲主,必須監控的模塊有:
「必須監控的現場信息有」 :
最後,咱們須要 「提煉規則,將監控內容 => 抽象成規則」。
咱們能夠經過代理對應的 Service 實現,完成收集 Wakelock、Alarm、GPS 的申請堆棧、釋放信息、手機充電狀態等等。
這裏咱們就以 WakeLock 的監控爲例,切面代碼以下所示:
public static long sStartTime = 0;
@Insert(value = "acquire") @TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF) public static void acquire(Context context){ trace = Log.getStackTraceString(new Throwable()); sStartTime = System.currentTimeMillis(); Origin.callVoid(); new Handler().postDelayed(new Runnable() { @Override public void run() { WakeLockUtils.release(); } },1000); } @Insert(value = "release") @TargetClass(value = "com.optimize.performance.wakelock.WakeLockUtils",scope = Scope.SELF) public static void release(){ LogUtils.i("PowerManager "+(System.currentTimeMillis() - sStartTime)+"/n"+trace); 複製代碼
此外,咱們也能夠利用 epic 來監控每一個線程的執行時間,超過閾值則警告,示例代碼以下所示:
public static long runTime = 0;
@Insert(value = "run") @TargetClass(value = "java.lang.Runnable",scope = Scope.ALL) public void run(){ runTime = System.currentTimeMillis(); Origin.callVoid(); LogUtils.i("runTime "+(System.currentTimeMillis() - runTime)); } 複製代碼
「寫一個基礎類,而後在統一的調用接口中添加監控邏輯」。這裏咱們能夠參考 Facebook Battery-Metrics
獲取、監控數據的方式。其代碼以下所示:
public class WakelockMetrics {
/** * 獲取 WakeLock * * @param wakeLock WakeLock * @param timeout 超時時間 */ public static void acquire(PowerManager.WakeLock wakeLock, long timeout) { wakeLock.acquire(timeout); // 監控 wakelock 相關信息 Log.e("HOOOOOOOOK", "--acquireWakeLock--"); Log.e("HOOOOOOOOK", Utils.getStackTrace()); // 使用 Battery-Metrics 庫統計其它維度的電量信息 } /** * 釋放 WakeLock * * @param wakeLock WakeLock */ public static void release(PowerManager.WakeLock wakeLock) { wakeLock.release(); Log.e("HOOOOOOOOK", "--releaseWakeLock--"); Log.e("HOOOOOOOOK", Utils.getStackTrace()); // 使用 Battery-Metrics 庫統計其它維度的電量信息 } } 複製代碼
Gradle 耗電量統計插件中 BatteryCreateMethodVisitor 的核心實現代碼以下所示:
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { // 監控 Wakelock String monitorClass = "com/ss/android/ugc/bytex/example/battery_monitor/WakelockMetrics"; if (!monitorClass.equals(className) && "android/os/PowerManager$WakeLock".equals(owner) && opcode == Opcodes.INVOKEVIRTUAL && "acquire".equals(name)) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, monitorClass, name, "(Landroid/os/PowerManager$WakeLock;J)V", isInterface ); return; } if (!monitorClass.equals(className) && "android/os/PowerManager$WakeLock".equals(owner) && opcode == Opcodes.INVOKEVIRTUAL && "release".equals(name)) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, monitorClass, name, "(Landroid/os/PowerManager$WakeLock;)V", isInterface ); return; } // 監控 Gps monitorClass = "com/ss/android/ugc/bytex/example/battery_monitor/GpsMetrics"; if (!monitorClass.equals(className) && "android/location/LocationManager".equals(owner) && opcode == Opcodes.INVOKEVIRTUAL && "requestLocationUpdates".equals(name)) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, monitorClass, name, "(Landroid/location/LocationManager;Ljava/lang/String;JFLandroid/location/LocationListener;)V", isInterface ); return; } if (!monitorClass.equals(className) && "android/location/LocationManager".equals(owner) && opcode == Opcodes.INVOKEVIRTUAL && "removeUpdates".equals(name)) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, monitorClass, name, "(Landroid/location/LocationManager;Landroid/location/LocationListener;)V", isInterface ); return; } // 監控 Alarm Service monitorClass = "com/ss/android/ugc/bytex/example/battery_monitor/AlarmMetrics"; if (!monitorClass.equals(className) && "android/app/AlarmManager".equals(owner) && opcode == Opcodes.INVOKEVIRTUAL && "set".equals(name)) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, monitorClass, name, "(Landroid/app/AlarmManager;IJLandroid/app/PendingIntent;)V", isInterface ); return; } if (!monitorClass.equals(className) && "android/app/AlarmManager".equals(owner) && opcode == Opcodes.INVOKEVIRTUAL && "cancel".equals(name)) { mv.visitMethodInsn( Opcodes.INVOKESTATIC, monitorClass, name, "(Landroid/app/AlarmManager;Landroid/app/PendingIntent;)V", isInterface ); return; } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } 複製代碼
系統的代碼插樁方案沒法替換。
電量相關的測試相對來講難度較大,由於 App 在具體手機上的耗電量沒法準確統計,每個手機所使用的硬件不同,那麼它相應的功耗就不同。並且這個功耗值咱們只能在線下經過導出手機的 power_profile.xml 文件拿到。
因爲咱們沒法獲取準確的耗電量,因此咱們只能增長多個維度來輔助判斷 App 是否耗電。
最後,咱們能夠分場景各個突破。
關於電量測試,咱們能夠針對各個功能場景進行鍼對性的專項測試。操做一段時間後,咱們能夠在手機設置—電量消耗裏面,利用其數據做爲判斷依據。這樣雖然直觀,但精確度不行。
介紹 Battery Historian:
由於咱們不能在線上統計出 App 的電量消耗,所以須要在儘可能保證 App 在正常使用下的耗電。對此咱們採起了一系列的電量優化措施:
根據場景謹慎地選擇傳感器使用的模式,好比說在使用 GPS 的時候通常要避免使用高精度的模式,或者是儘可能複用上一次的定位結果。
咱們在實際項目中使用 WakeLock 有幾個注意事項,第一,acquire、release 要成對地釋放,第二,儘可能使用 acquire 的超時方法來設置超時時間,避免由於異常狀況從而致使 WakeLock 而沒法釋放的狀況,第三,關於 WakeLock 的釋放必定要寫在 try-catch-finally 的 finally 當中,保證 WakeLock 在異常狀況下的釋放。
JobScheduler 能夠容許開發者在符合某些條件下創造執行在後臺的任務,咱們能夠設置執行一些耗電操做的場景,好比說 處於 WIFI 狀態下同時鏈接電源 的狀況下。同時,要注意用戶在離開界面後,要避免耗電的操做,好比說中止播放動畫。經過這些操做,咱們的 App 就不會比以前耗電了。
對於電量優化來講,最重要的就是 「創建監控與自動化報警的一整套體系,只有發現了耗電的問題所在,才能使用針對性的解決措施」。
個人公衆號 JsonChao
開通啦,歡迎關注~
❝歡迎關注個人微信:
❞bcce5360
❝「因爲微信羣人數過多,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。」
❞
❝2千人QQ羣,「Awesome-Android學習交流羣,QQ羣號:959936182」, 歡迎你們加入~
❞