性能優化系列(四)電量性能優化

耗電設備

手機各個硬件模塊的耗電量是不一樣的,有些模塊非常耗電,而有些模塊則相對顯得耗電量小很多。

電量消耗的計算與統計是一件麻煩而且矛盾的事情,記錄電量消耗本身也是一個費電量的事情。唯一可行的方案是使用第三方監測電量的設備,這樣才能夠獲取到真實的電量消耗。

屏幕

當設備處於待機狀態時消耗的電量是極少的,以 Nexus 5 爲例,打開飛行模式,可以待機接近 1 個月。可是點亮屏幕,味着系統的各組件要開始進行工作,界面也需要開始執行渲染,這會需要消耗很多電量。

蜂窩網絡

通常情況下,使用移動網絡傳輸數據,電量的消耗有三種狀態:

  • Full Power

能量最高的狀態,移動網絡連接被激活,允許設備以最大的傳輸速率進行操作。

  • Low power

一種中間狀態,對電量的消耗差不多是 Full power 狀態下的 50%。

  • Standby

最低的狀態,沒有數據連接需要傳輸,電量消耗最少。

總之,爲了減少電量的消耗,在蜂窩移動網絡下,最好做到批量執行網絡請求,儘量避免頻繁的間隔網絡請求。

使用 Battery Historian 我們可以得到設備的電量消耗數據,如果數據中的移動蜂窩網絡(Mobile Radio)電量消耗呈現下面的情況,間隔很小,又頻繁斷斷續續的出現,說明電量消耗性能很不好:

在這裏插入圖片描述

經過優化之後,如果呈現下面的圖示,說明電量消耗的性能是良好的:

在這裏插入圖片描述

另外 WiFi 連接下,網絡傳輸的電量消耗要比移動網絡少很多,應該儘量減少移動網絡下的數據傳輸,多在 WiFi 環境下傳輸數據。

在這裏插入圖片描述

那麼如何才能夠把任務緩存起來,做到批量化執行呢?我們可以使用 JobScheduler 來優化。

跟蹤充電狀態

我們可以通過下面的代碼來獲取手機的當前充電狀態:

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!");
}

在上面的例子演示瞭如何立即獲取到手機的充電狀態,得到充電狀態信息之後,我們可以有針對性的對部分代碼做優化。

比如:我們可以判斷只有當前手機爲 AC 充電狀態時 纔去執行一些非常耗電的操作。

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);
}

監聽充電狀態變化

在清單文件中註冊一個 BroadcastReceiver,通過在一個 Intent 過濾器內定義 ACTION_POWER_CONNECTEDACTION_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>

創建監聽充電狀態變化的 PowerConnectionReceiver。

public class PowerConnectionReceiver extends BroadcastReceiver {
  @Override
  public 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 = "未知道狀態";
        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,這時當充電狀態發生變化時 PowerConnectionReceiver 就會收到通知。

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
this.registerReceiver(new PowerConnectionReceiver(), intentFilter);

監聽電池電量變化

在清單文件中註冊一個 BroadcastReceiver,通過偵聽 ACTION_BATTERY_LOWACTION_BATTERY_OKAY,每當設備電池電量不足或退出不足狀態時,便會觸發該接收器。

<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>

創建監聽電池電量變化的 BatteryLevelReceiver。

public class BatteryLevelReceiver extends BroadcastReceiver {
  @Override
  public void onReceive(Context context, Intent intent) {
    // 當前剩餘電量
    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,這時當電池電量發生變化時 BatteryLevelReceiver 就會收到通知。

IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
this.registerReceiver(new BatteryLevelReceiver(), intentFilter);

通常,如果設備連接了交流充電器,您應該最大限度提高後臺更新的頻率;而如果設備是通過 USB 充電,則應降低更新頻率,如果電池正在放電,則應進一步降低更新頻率;在電池電量極低時停用所有後臺更新。

WakeLock

WakeLock 是一種鎖的機制,只要有應用拿着這個鎖,CPU 就無法進入休眠狀態,一直處於工作狀態。

比如,手機屏幕在屏幕關閉的時候,有些應用依然可以喚醒屏幕提示用戶消息,這裏就是用到了 Wakelock 鎖機制,雖然手機屏幕關閉了,但是這些應用依然在運行着。

手機耗電的問題,大部分是開發人員沒有正確使用這個鎖,成爲「待機殺手」。

Android 手機有兩個處理器,一個叫 Application Processor(AP),一個叫 Baseband Processor(BP)。

AP 是 ARM 架構的處理器,用於運行 Linux + Android 系統;BP 用於運行實時操作系統(RTOS),通訊協議棧運行於 BP 的 RTOS 之上。非通話時間,BP 的能耗基本上在 5mA 左右,而 AP 只要處於非休眠狀態,能耗至少在 50mA 以上,執行圖形運算時會更高。另外 LCD 工作時功耗在 100mA 左右,WiFi 也在 100mA 左右。

一般手機待機時,AP、LCD、WIFI 均進入休眠狀態,這時 Android 中應用程序的代碼也會停止執行。

Android 爲了確保應用程序中關鍵代碼的正確執行,提供了 Wake Lock 的 API,使得應用程序有權限通過代碼阻止 AP 進入休眠狀態。但如果不領會 Android 設計者的意圖而濫用 Wake Lock API,爲了自身程序在後臺的正常工作而長時間阻止 AP 進入休眠狀態,就會成爲待機電池殺手。

那麼 Wake Lock API 具體有啥用呢?心跳包從請求到應答,斷線重連重新登陸等關鍵邏輯的執行過程,就需要 Wake Lock 來保護。而一旦一個關鍵邏輯執行成功,就應該立即釋放掉 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 是一個標誌位,標誌位是用來控制獲取的 WakeLock 對象的類型,主要控制 CPU 工作時屏幕是否需要亮着以及鍵盤燈需要亮着,標誌位說明如下:

levelAndFlags CPU是否運行 屏幕是否亮着 鍵盤燈是否亮着
PARTIAL_WAKE_LOCK
SCREEN_DIM_WAKE_LOCK 低亮度
SCREEN_BRIGHT_WAKE_LOCK 高亮度
FULL_WAKE_LOCK

特殊說明:自 API 等級 17 開始,FULL_WAKE_LOCK 將被棄用。應用應使用 FLAG_KEEP_SCREEN_ON

WakeLock 類可以用來控制設備的工作狀態。使用該類中的 acquire 可以使 CPU 一直處於工作的狀態,如果不需要使 CPU 處於工作狀態就調用 release 來關閉。

  • 自動 release

如果我們調用的是 acquire(long timeout),那麼就無需我們自己手動調用 release() 來釋放鎖,系統會幫助我們在 timeout 時間後釋放。

  • 手動 release

如果我們調用的是 acquire() 那麼就需要我們自己手動調用 release() 來釋放鎖。

最後使用 WakeLock 類記得加上如下權限:

<uses-permission android:name="android.permission.WAKE_LOCK" />   

注意:在使用該類的時候,必須保證 acquire 和 release 是成對出現的。

屏幕保持常亮

當設備從休眠狀態中,被應用程序喚醒一瞬間會耗電過多,我們可以保持屏幕常亮來節省電量,代碼聲明:

// 屏幕保持常亮
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 允許同時執行多個任務,執行某些指定的任務時不需要考慮時機控制引起的電池消耗。

使用 Job Scheduler,應用需要做的事情就是判斷哪些任務是不緊急的,可以交給 Job Scheduler 來處理,Job Scheduler 集中處理收到的任務,選擇合適的時間,合適的網絡,再一起進行執行。

下面是使用 Job Scheduler 的一段簡要示例,需要先有一個 JobService:

public class MyJobService extends JobService {

  @Override
  public boolean onStartJob(JobParameters params) {
    Log.i("MyJobService", "Totally and completely working on job " 
          + params.getJobId());
    // 檢查網絡狀態
    if (isNetworkConnected()) {
      new SimpleDownloadTask() .execute(params);
      // 返回 true,表示該工作耗時,
      // 同時工作處理完成後需要調用 onStopJob 銷燬(jobFinished)
      return true;
    } else {
      Log.i("MyJobService", "No connection on job " + params.getJobId() 
            + "; sad face");
    }
    // 返回 false,任務運行不需要很長時間,到 return 時已完成任務處理
    return false;
  }

  @Override
  public boolean onStopJob(JobParameters params) {
    Log.i("MyJobService", "Something changed, so I'm calling it on job " 
          + params.getJobId());
    // 有且僅有 onStartJob 返回值爲 true 時,纔會調用 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;

    @Override
    protected 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 sec
        conn.setConnectTimeout(15000); // 15 sec
        conn.setRequestMethod("GET");
        //Starts the query
        conn.connect();
        int response = conn.getResponseCode();
        Log.d(LOG_TAG, "The response is: " + response);
        is = conn.getInputStream();

        // Convert the input stream to a string
        Reader 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.";
      }
    }

    @Override
    protected void onPostExecute(String result) {
      // 當任務完成時,需要調用 jobFinished() 讓系統知道完成了哪項任務
      jobFinished(mJobParam, false);
      Log.i("SimpleDownloadTask", result);
    }
  }
}

定義了 JobService 的子類後,然後需要在 AndroidManifest.xml 中進行聲明:

<service android:name="pkgName.JobSchedulerService"
    android:permission="android.permission.BIND_JOB_SERVICE" />

最後模擬通過點擊 Button 觸發 N 個任務,交給 JobService 來處理:

public class FreeTheWakelockActivity extends ActionBarActivity {
  public static final String LOG_TAG = "FreeTheWakelockActivity";

  TextView mWakeLockMsg;
  ComponentName mServiceComponent;

  @Override
  protected 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() {
      @Override
      public 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 中的一個組件,可幫助開發者找到應用程序能量消耗的位置。

Energy Profiler 通過監控 CPU、網絡和 GPS 傳感器的使用情況,並以圖形化顯示每個組件使用多少能量。Energy Profiler 還會顯示可能影響能耗的系統事件(WakeLock、Alarms、Jobs 和 Location),Energy Profiler 不直接測量能耗,相反,它使用一種模型來估算設備上每種資源的能耗。

可以在 View > Tool Windows > Android Profiler 中打開 Energy Profiler 界面。

在這裏插入圖片描述

Energy Profiler 的具體使用可查看 Android 開發文檔 - 使用 Energy Profiler 檢查能源使用情況

Energy Profiler 支持 Android 8.0 (API 26) 及以上的系統,Android 8.0 (API 26) 以下請使用 Battery Historian。

Battery Historian

Battery Historian 是一款由 Google 提供的 Android 系統電量分析工具,能夠以網頁形式展示手機的電量消耗過程。

GitHub 地址:https://github.com/google/battery-historian

本文以 macOS 環境爲例,介紹 Battery Historian 的使用。

Windows 環境請參考:Battery Historian 2.0 for windows 環境搭建

安裝 Docker

手動下載 Docker 安裝包,下載鏈接:https://download.docker.com/mac/stable/Docker.dmg

安裝好之後點擊圖標運行,在頂部菜單欄可以看到一個鯨魚圖標,說明 Docker 正在運行。

然後在控制檯輸入:

$ docker --version

看到如下內容,說明 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

根據系統版本不同 bugreport 的獲取方式略有差別:

如果 是Android 7.0 及以上版本,通過下面命令來獲取 bugreport:

$ adb bugreport bugreport.zip

如果是 Android 6.0 及以下版本,通過下面命令來獲取 bugreport:

$ adb bugreport > bugreport.txt

獲取到 bugreport 文件之後,我們就可以將其上傳到 Battery Historian 上進行分析,下面是它的輸出結果。

在這裏插入圖片描述

分析結果

在頁面的下方我們可以查看這段時間內系統的狀態 system stats,也可以選擇某個應用查看應用的狀態 app stats。

在這裏插入圖片描述

其中我們可以看到 Device estimated power use 中顯示了估算的應用耗電量值爲 0.18%

Battery Historian 還有個比較功能,在首頁選擇 Switch to Bugreport Comparisor,然後就可以上傳兩個不同的 bugreport 文件,submit 之後就可以看到它們的對比結果了,這個功能用來分析同一個應用的兩個不同版本前後的耗電量非常有用。

在這裏插入圖片描述
需要注意的是,一般開始統計數據之前需要使用下面的命令將以前的累積數據清空:

$ adb shell dumpsys batterystats --enable full-wake-history

$ adb shell dumpsys batterystats --reset

上面的操作相當於初始化操作,如果不這麼做會有一大堆的干擾的數據,看起來會比較痛苦。

關於 bugreport 相關的知識推薦閱讀 Android adb bugreport 工具分析和使用 這篇文章,作者簡單地從源碼角度分析了 adb bugreport 命令的運行原理,結論是 bugreport 其實是啓動了 dumpstate 服務來輸出數據,其中數據來源包括:

  • 系統屬性
  • /proc/sys 節點文件
  • 執行 shell 命令獲得相關輸出
  • logcat 輸出
  • Android Framework Services 信息基本使用 dumpsys 命令通過 binder 調用服務中的 dump 函數獲得信息

結果分析參考:https://testerhome.com/topics/3733

我的 GitHub

github.com/jeanboydev

技術交流羣

歡迎加入技術交流羣,來一起交流學習。

QQ 技術交流羣
微信技術交流羣

我的公衆號

歡迎關注我的公衆號,分享各種技術乾貨,各種學習資料,職業發展和行業動態。

Android 波斯灣

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章