性能优化系列(四)电量性能优化
文章首發(fā)「Android波斯灣」公眾號,更新地址:https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode
耗電設(shè)備
手機各個硬件模塊的耗電量是不一樣的,有些模塊非常耗電,而有些模塊則相對顯得耗電量小很多。
電量消耗的計算與統(tǒng)計是一件麻煩而且矛盾的事情,記錄電量消耗本身也是一個費電量的事情。唯一可行的方案是使用第三方監(jiān)測電量的設(shè)備,這樣才能夠獲取到真實的電量消耗。
屏幕
當(dāng)設(shè)備處于待機狀態(tài)時消耗的電量是極少的,以 Nexus 5 為例,打開飛行模式,可以待機接近 1 個月。可是點亮屏幕,味著系統(tǒng)的各組件要開始進行工作,界面也需要開始執(zhí)行渲染,這會需要消耗很多電量。
蜂窩網(wǎng)絡(luò)
通常情況下,使用移動網(wǎng)絡(luò)傳輸數(shù)據(jù),電量的消耗有三種狀態(tài):
- Full Power
能量最高的狀態(tài),移動網(wǎng)絡(luò)連接被激活,允許設(shè)備以最大的傳輸速率進行操作。
- Low power
一種中間狀態(tài),對電量的消耗差不多是 Full power 狀態(tài)下的 50%。
- Standby
最低的狀態(tài),沒有數(shù)據(jù)連接需要傳輸,電量消耗最少。
總之,為了減少電量的消耗,在蜂窩移動網(wǎng)絡(luò)下,最好做到批量執(zhí)行網(wǎng)絡(luò)請求,盡量避免頻繁的間隔網(wǎng)絡(luò)請求。
使用 Battery Historian 我們可以得到設(shè)備的電量消耗數(shù)據(jù),如果數(shù)據(jù)中的移動蜂窩網(wǎng)絡(luò)(Mobile Radio)電量消耗呈現(xiàn)下面的情況,間隔很小,又頻繁斷斷續(xù)續(xù)的出現(xiàn),說明電量消耗性能很不好:
經(jīng)過優(yōu)化之后,如果呈現(xiàn)下面的圖示,說明電量消耗的性能是良好的:
另外 WiFi 連接下,網(wǎng)絡(luò)傳輸?shù)碾娏肯囊纫苿泳W(wǎng)絡(luò)少很多,應(yīng)該盡量減少移動網(wǎng)絡(luò)下的數(shù)據(jù)傳輸,多在 WiFi 環(huán)境下傳輸數(shù)據(jù)。
那么如何才能夠把任務(wù)緩存起來,做到批量化執(zhí)行呢?我們可以使用 JobScheduler 來優(yōu)化。
跟蹤充電狀態(tài)
我們可以通過下面的代碼來獲取手機的當(dāng)前充電狀態(tài):
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); Intent batteryStatus = this.registerReceiver(null, filter); int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC); if (acCharge) {Log.v(LOG_TAG, "The phone is charging!"); }在上面的例子演示了如何立即獲取到手機的充電狀態(tài),得到充電狀態(tài)信息之后,我們可以有針對性的對部分代碼做優(yōu)化。
比如:我們可以判斷只有當(dāng)前手機為 AC 充電狀態(tài)時 才去執(zhí)行一些非常耗電的操作。
private boolean checkForPower() {IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);Intent batteryStatus = this.registerReceiver(null, filter);int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);boolean usbCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_USB);boolean acCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_AC);boolean wirelessCharge = false;if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {wirelessCharge = (chargePlug == BatteryManager.BATTERY_PLUGGED_WIRELESS);}return (usbCharge || acCharge || wirelessCharge); }監(jiān)聽充電狀態(tài)變化
在清單文件中注冊一個 BroadcastReceiver,通過在一個 Intent 過濾器內(nèi)定義 ACTION_POWER_CONNECTED 和 ACTION_POWER_DISCONNECTED 來同時偵聽這兩種事件。
<receiver android:name=".PowerConnectionReceiver"><intent-filter><action android:name="android.intent.action.ACTION_POWER_CONNECTED"/><action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/></intent-filter> </receiver>創(chuàng)建監(jiān)聽充電狀態(tài)變化的 PowerConnectionReceiver。
public class PowerConnectionReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS,BatteryManager.BATTERY_STATUS_UNKNOWN);String batteryStatus = "";switch (status) {case BatteryManager.BATTERY_STATUS_CHARGING:batteryStatus = "正在充電";break;case BatteryManager.BATTERY_STATUS_DISCHARGING:batteryStatus = "正在放電";break;case BatteryManager.BATTERY_STATUS_NOT_CHARGING:batteryStatus = "未充電";break;case BatteryManager.BATTERY_STATUS_FULL:batteryStatus = "充滿電";break;case BatteryManager.BATTERY_STATUS_UNKNOWN:batteryStatus = "未知道狀態(tài)";break;}Toast.makeText(context, "batteryStatus = " + batteryStatus, Toast.LENGTH_LONG).show();int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED,BatteryManager.BATTERY_PLUGGED_AC);String chargePlug = "";switch (plugged) {case BatteryManager.BATTERY_PLUGGED_AC:chargePlug = "AC充電";break;case BatteryManager.BATTERY_PLUGGED_USB:chargePlug = "USB充電";break;case BatteryManager.BATTERY_PLUGGED_WIRELESS:chargePlug = "無線充電";break;}Toast.makeText(context, "chargePlug=" + chargePlug, Toast.LENGTH_LONG).show();} }最后注冊 PowerConnectionReceiver,這時當(dāng)充電狀態(tài)發(fā)生變化時 PowerConnectionReceiver 就會收到通知。
IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); this.registerReceiver(new PowerConnectionReceiver(), intentFilter);監(jiān)聽電池電量變化
在清單文件中注冊一個 BroadcastReceiver,通過偵聽 ACTION_BATTERY_LOW 和 ACTION_BATTERY_OKAY,每當(dāng)設(shè)備電池電量不足或退出不足狀態(tài)時,便會觸發(fā)該接收器。
<receiver android:name=".BatteryLevelReceiver"><intent-filter><action android:name="android.intent.action.ACTION_BATTERY_LOW"/><action android:name="android.intent.action.ACTION_BATTERY_OKAY"/></intent-filter> </receiver>創(chuàng)建監(jiān)聽電池電量變化的 BatteryLevelReceiver。
public class BatteryLevelReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {// 當(dāng)前剩余電量int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);// 電量最大值int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);// 電量百分比float batteryPct = level / (float) scale;Log.d("BatteryLevelReceiver", "batteryPct = " + batteryPct);Toast.makeText(context, "batteryPct = " + batteryPct, Toast.LENGTH_LONG).show();} }最后注冊 BatteryLevelReceiver,這時當(dāng)電池電量發(fā)生變化時 BatteryLevelReceiver 就會收到通知。
IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED); this.registerReceiver(new BatteryLevelReceiver(), intentFilter);通常,如果設(shè)備連接了交流充電器,您應(yīng)該最大限度提高后臺更新的頻率;而如果設(shè)備是通過 USB 充電,則應(yīng)降低更新頻率,如果電池正在放電,則應(yīng)進一步降低更新頻率;在電池電量極低時停用所有后臺更新。
WakeLock
WakeLock 是一種鎖的機制,只要有應(yīng)用拿著這個鎖,CPU 就無法進入休眠狀態(tài),一直處于工作狀態(tài)。
比如,手機屏幕在屏幕關(guān)閉的時候,有些應(yīng)用依然可以喚醒屏幕提示用戶消息,這里就是用到了 Wakelock 鎖機制,雖然手機屏幕關(guān)閉了,但是這些應(yīng)用依然在運行著。
手機耗電的問題,大部分是開發(fā)人員沒有正確使用這個鎖,成為「待機殺手」。
Android 手機有兩個處理器,一個叫 Application Processor(AP),一個叫 Baseband Processor(BP)。
AP 是 ARM 架構(gòu)的處理器,用于運行 Linux + Android 系統(tǒng);BP 用于運行實時操作系統(tǒng)(RTOS),通訊協(xié)議棧運行于 BP 的 RTOS 之上。非通話時間,BP 的能耗基本上在 5mA 左右,而 AP 只要處于非休眠狀態(tài),能耗至少在 50mA 以上,執(zhí)行圖形運算時會更高。另外 LCD 工作時功耗在 100mA 左右,WiFi 也在 100mA 左右。
一般手機待機時,AP、LCD、WIFI 均進入休眠狀態(tài),這時 Android 中應(yīng)用程序的代碼也會停止執(zhí)行。
Android 為了確保應(yīng)用程序中關(guān)鍵代碼的正確執(zhí)行,提供了 Wake Lock 的 API,使得應(yīng)用程序有權(quán)限通過代碼阻止 AP 進入休眠狀態(tài)。但如果不領(lǐng)會 Android 設(shè)計者的意圖而濫用 Wake Lock API,為了自身程序在后臺的正常工作而長時間阻止 AP 進入休眠狀態(tài),就會成為待機電池殺手。
那么 Wake Lock API 具體有啥用呢?心跳包從請求到應(yīng)答,斷線重連重新登陸等關(guān)鍵邏輯的執(zhí)行過程,就需要 Wake Lock 來保護。而一旦一個關(guān)鍵邏輯執(zhí)行成功,就應(yīng)該立即釋放掉 Wake Lock 了。兩次心跳請求間隔 5 到 10 分鐘,基本不會怎么耗電。
WakeLock 使用
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); WakeLock wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakelockTag");newWakeLock(int levelAndFlags, String tag) 中 PowerManager.PARTIIAL_WAKE_LOCK 是一個標(biāo)志位,標(biāo)志位是用來控制獲取的 WakeLock 對象的類型,主要控制 CPU 工作時屏幕是否需要亮著以及鍵盤燈需要亮著,標(biāo)志位說明如下:
| PARTIAL_WAKE_LOCK | 是 | 否 | 否 |
| SCREEN_DIM_WAKE_LOCK | 是 | 低亮度 | 否 |
| SCREEN_BRIGHT_WAKE_LOCK | 是 | 高亮度 | 否 |
| FULL_WAKE_LOCK | 是 | 是 | 是 |
特殊說明:自 API 等級 17 開始,FULL_WAKE_LOCK 將被棄用。應(yīng)用應(yīng)使用 FLAG_KEEP_SCREEN_ON。
WakeLock 類可以用來控制設(shè)備的工作狀態(tài)。使用該類中的 acquire 可以使 CPU 一直處于工作的狀態(tài),如果不需要使 CPU 處于工作狀態(tài)就調(diào)用 release 來關(guān)閉。
- 自動 release
如果我們調(diào)用的是 acquire(long timeout),那么就無需我們自己手動調(diào)用 release() 來釋放鎖,系統(tǒng)會幫助我們在 timeout 時間后釋放。
- 手動 release
如果我們調(diào)用的是 acquire() 那么就需要我們自己手動調(diào)用 release() 來釋放鎖。
最后使用 WakeLock 類記得加上如下權(quán)限:
<uses-permission android:name="android.permission.WAKE_LOCK" />注意:在使用該類的時候,必須保證 acquire 和 release 是成對出現(xiàn)的。
屏幕保持常亮
當(dāng)設(shè)備從休眠狀態(tài)中,被應(yīng)用程序喚醒一瞬間會耗電過多,我們可以保持屏幕常亮來節(jié)省電量,代碼聲明:
// 屏幕保持常亮 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); // 一般不需要人為的去掉 FLAG_KEEP_SCREEN_ON 的 flag, // windowManager 會管理好程序進入后臺回到前臺的的操作 //getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);或者,直接在布局中加上 keepScreenOn = true :
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:keepScreenOn="true"tools:context="com.jeanboy.app.batterysample.MainActivity"> </android.support.constraint.ConstraintLayout>JobScheduler
在 API 21,Google 提供了一個新叫做 Job Scheduler API 的組件來處理這樣的場景。Job Scheduler API 允許同時執(zhí)行多個任務(wù),執(zhí)行某些指定的任務(wù)時不需要考慮時機控制引起的電池消耗。
使用 Job Scheduler,應(yīng)用需要做的事情就是判斷哪些任務(wù)是不緊急的,可以交給 Job Scheduler 來處理,Job Scheduler 集中處理收到的任務(wù),選擇合適的時間,合適的網(wǎng)絡(luò),再一起進行執(zhí)行。
下面是使用 Job Scheduler 的一段簡要示例,需要先有一個 JobService:
public class MyJobService extends JobService {@Overridepublic boolean onStartJob(JobParameters params) {Log.i("MyJobService", "Totally and completely working on job " + params.getJobId());// 檢查網(wǎng)絡(luò)狀態(tài)if (isNetworkConnected()) {new SimpleDownloadTask() .execute(params);// 返回 true,表示該工作耗時,// 同時工作處理完成后需要調(diào)用 onStopJob 銷毀(jobFinished)return true;} else {Log.i("MyJobService", "No connection on job " + params.getJobId() + "; sad face");}// 返回 false,任務(wù)運行不需要很長時間,到 return 時已完成任務(wù)處理return false;}@Overridepublic boolean onStopJob(JobParameters params) {Log.i("MyJobService", "Something changed, so I'm calling it on job " + params.getJobId());// 有且僅有 onStartJob 返回值為 true 時,才會調(diào)用 onStopJob 來銷毀 job// 返回 false 來銷毀這個工作return false;}private boolean isNetworkConnected() {ConnectivityManager connectivityManager =(ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();return (networkInfo != null && networkInfo.isConnected());}private class SimpleDownloadTask extends AsyncTask<JobParameters,Void, String> {protected JobParameters mJobParam;@Overrideprotected String doInBackground(JobParameters... params) {mJobParam = params[0];try {InputStream is = null;int len = 50;URL url = new URL("https://www.google.com");HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setReadTimeout(10000); // 10 secconn.setConnectTimeout(15000); // 15 secconn.setRequestMethod("GET");//Starts the queryconn.connect();int response = conn.getResponseCode();Log.d(LOG_TAG, "The response is: " + response);is = conn.getInputStream();// Convert the input stream to a stringReader reader = null;reader = new InputStreamReader(is, "UTF-8");char[] buffer = new char[len];reader.read(buffer);return new String(buffer);} catch (IOException e) {return "Unable to retrieve web page.";}}@Overrideprotected void onPostExecute(String result) {// 當(dāng)任務(wù)完成時,需要調(diào)用 jobFinished() 讓系統(tǒng)知道完成了哪項任務(wù)jobFinished(mJobParam, false);Log.i("SimpleDownloadTask", result);}} }定義了 JobService 的子類后,然后需要在 AndroidManifest.xml 中進行聲明:
<service android:name="pkgName.JobSchedulerService"android:permission="android.permission.BIND_JOB_SERVICE" />最后模擬通過點擊 Button 觸發(fā) N 個任務(wù),交給 JobService 來處理:
public class FreeTheWakelockActivity extends ActionBarActivity {public static final String LOG_TAG = "FreeTheWakelockActivity";TextView mWakeLockMsg;ComponentName mServiceComponent;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_wakelock);mWakeLockMsg = (TextView) findViewById(R.id.wakelock_txt);mServiceComponent = new ComponentName(this, MyJobService.class);Intent startServiceIntent = new Intent(this, MyJobService.class);startService(startServiceIntent);Button theButtonThatWakelocks = (Button) findViewById(R.id.wakelock_poll);theButtonThatWakelocks.setText(R.string.poll_server_button);theButtonThatWakelocks.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {pollServer();}});}public void pollServer() {JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);for (int i = 0; i < 10; i++) {JobInfo jobInfo = new JobInfo.Builder(i, mServiceComponent).setMinimumLatency(5000) // 5 seconds.setOverrideDeadline(60000) // 60 seconds.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // WiFi or data connections.build();mWakeLockMsg.append("Scheduling job " + i + "!\n");scheduler.schedule(jobInfo);}} }官方 demo 地址:https://github.com/googlesamples/android-JobScheduler
Energy Profiler
Energy Profiler 是 Android Profiler 中的一個組件,可幫助開發(fā)者找到應(yīng)用程序能量消耗的位置。
Energy Profiler 通過監(jiān)控 CPU、網(wǎng)絡(luò)和 GPS 傳感器的使用情況,并以圖形化顯示每個組件使用多少能量。Energy Profiler 還會顯示可能影響能耗的系統(tǒng)事件(WakeLock、Alarms、Jobs 和 Location),Energy Profiler 不直接測量能耗,相反,它使用一種模型來估算設(shè)備上每種資源的能耗。
可以在 View > Tool Windows > Android Profiler 中打開 Energy Profiler 界面。
Energy Profiler 的具體使用可查看 Android 開發(fā)文檔 - 使用 Energy Profiler 檢查能源使用情況。
Energy Profiler 支持 Android 8.0 (API 26) 及以上的系統(tǒng),Android 8.0 (API 26) 以下請使用 Battery Historian。
Battery Historian
Battery Historian 是一款由 Google 提供的 Android 系統(tǒng)電量分析工具,能夠以網(wǎng)頁形式展示手機的電量消耗過程。
GitHub 地址:https://github.com/google/battery-historian
本文以 macOS 環(huán)境為例,介紹 Battery Historian 的使用。
Windows 環(huán)境請參考:Battery Historian 2.0 for windows 環(huán)境搭建。
安裝 Docker
手動下載 Docker 安裝包,下載鏈接:https://download.docker.com/mac/stable/Docker.dmg。
安裝好之后點擊圖標(biāo)運行,在頂部菜單欄可以看到一個鯨魚圖標(biāo),說明 Docker 正在運行。
然后在控制臺輸入:
$ docker --version
看到如下內(nèi)容,說明 Docker 可以正常使用:
Docker version 19.03.1, build 74b1e89安裝 Battery Historian
通過下面命令安裝 Battery Historian:
$ docker run -d -p 9999:9999 bhaavan/battery-historian
上面的步驟都完成之后就可以啟動 Battery Historian 了,默認端口是 9999。
之后在瀏覽器中輸入 http://localhost:9999 就可以看到效果,然后上傳 bugreport 文件進行分析了。
獲取 bugreport
根據(jù)系統(tǒng)版本不同 bugreport 的獲取方式略有差別:
如果 是Android 7.0 及以上版本,通過下面命令來獲取 bugreport:
$ adb bugreport bugreport.zip
如果是 Android 6.0 及以下版本,通過下面命令來獲取 bugreport:
$ adb bugreport > bugreport.txt
獲取到 bugreport 文件之后,我們就可以將其上傳到 Battery Historian 上進行分析,下面是它的輸出結(jié)果。
分析結(jié)果
在頁面的下方我們可以查看這段時間內(nèi)系統(tǒng)的狀態(tài) system stats,也可以選擇某個應(yīng)用查看應(yīng)用的狀態(tài) app stats。
其中我們可以看到 Device estimated power use 中顯示了估算的應(yīng)用耗電量值為 0.18%。
Battery Historian 還有個比較功能,在首頁選擇 Switch to Bugreport Comparisor,然后就可以上傳兩個不同的 bugreport 文件,submit 之后就可以看到它們的對比結(jié)果了,這個功能用來分析同一個應(yīng)用的兩個不同版本前后的耗電量非常有用。
需要注意的是,一般開始統(tǒng)計數(shù)據(jù)之前需要使用下面的命令將以前的累積數(shù)據(jù)清空:
$ adb shell dumpsys batterystats --enable full-wake-history
$ adb shell dumpsys batterystats --reset
上面的操作相當(dāng)于初始化操作,如果不這么做會有一大堆的干擾的數(shù)據(jù),看起來會比較痛苦。
關(guān)于 bugreport 相關(guān)的知識推薦閱讀 Android adb bugreport 工具分析和使用 這篇文章,作者簡單地從源碼角度分析了 adb bugreport 命令的運行原理,結(jié)論是 bugreport 其實是啟動了 dumpstate 服務(wù)來輸出數(shù)據(jù),其中數(shù)據(jù)來源包括:
- 系統(tǒng)屬性
- /proc 和 /sys 節(jié)點文件
- 執(zhí)行 shell 命令獲得相關(guān)輸出
- logcat 輸出
- Android Framework Services 信息基本使用 dumpsys 命令通過 binder 調(diào)用服務(wù)中的 dump 函數(shù)獲得信息
結(jié)果分析參考:https://testerhome.com/topics/3733
我的 GitHub
github.com/jeanboydev
技術(shù)交流群
歡迎加入技術(shù)交流群,來一起交流學(xué)習(xí)。
我的公眾號
歡迎關(guān)注我的公眾號,分享各種技術(shù)干貨,各種學(xué)習(xí)資料,職業(yè)發(fā)展和行業(yè)動態(tài)。
參考資料
- YouTube - Android 性能優(yōu)化典范第 2 季
- Udacity 學(xué)院 - Android 性能優(yōu)化
- 胡凱 - Android 性能優(yōu)化之電量篇
- Android 電量優(yōu)化
- Android 開發(fā)文檔 - Optimize for battery life
- Android 開發(fā)文檔 - 使用 Energy Profiler 檢查能源使用情況
- Android adb bugreport 工具分析和使用
- Battery Historian 2.0 for windows 環(huán)境搭建
總結(jié)
以上是生活随笔為你收集整理的性能优化系列(四)电量性能优化的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python分布式框架_高性能分布式执行
- 下一篇: CSS3笔记之定位篇(一)relativ