Android性能優化之電量篇

Google近期在Udacity上發佈了Android性能優化的在線課程,分別從渲染,運算與內存,電量幾個方面介紹瞭如何去優化性能,這些課程是Google以前在Youtube上發佈的Android性能優化典範專題課程的細化與補充。html

 

下面是電量篇章的學習筆記,部份內容與前面的性能優化典範有重合,歡迎你們一塊兒學習交流!java

(1). Understanding Battery Drain

手機各個硬件模塊的耗電量是不同的,有些模塊很是耗電,而有些模塊則相對顯得耗電量小不少。python

                                        android_perf_battery_drain

電量消耗的計算與統計是一件麻煩並且矛盾的事情,記錄電量消耗自己也是一個費電量的事情。惟一可行的方案是使用第三方監測電量的設備,這樣纔可以獲取到真實的電量消耗。android

當設備處於待機狀態時消耗的電量是極少的,以N5爲例,打開飛行模式,能夠待機接近1個月。但是點亮屏幕,硬件各個模塊就須要開始工做,這會須要消耗不少電量。git

使用WakeLock或者JobScheduler喚醒設備處理定時的任務以後,必定要及時讓設備回到初始狀態。每次喚醒蜂窩信號進行數據傳遞,都會消耗不少電量,它比WiFi等操做更加的耗電。github

                  battery_drain_radio

(2). Battery Historian

Battery Historian是Android 5.0開始引入的新API。經過下面的指令,能夠獲得設備上的電量消耗信息:web

$ adb shell dumpsys batterystats > xxx.txt  //獲得整個設備的電量消耗信息
$ adb shell dumpsys batterystats > com.package.name > xxx.txt //獲得指定app相關的電量消耗信息

獲得了原始的電量消耗數據以後,咱們須要經過Google編寫的一個python腳本把數據信息轉換成可讀性更好的html文件:算法

$ python historian.py xxx.txt > xxx.html

打開這個轉換事後的html文件,能夠看到相似TraceView生成的列表數據,這裏的數據信息量很大,這裏就不展開了。shell

             android_perf_battery_historian

(3). Track Battery Status & Battery Manager

咱們能夠經過下面的代碼來獲取手機的當前充電狀態:緩存

// It is very easy to subscribe to changes to the battery state, but you can get the current
// state by simply passing null in as your receiver.  Nifty, isn't that?
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充電狀態時 纔去執行一些很是耗電的操做。

/**
 * This method checks for power by comparing the current battery state against all possible
 * plugged in states. In this case, a device may be considered plugged in either by USB, AC, or
 * wireless charge. (Wireless charge was introduced in API Level 17.)
 */
private boolean checkForPower() {
    // It is very easy to subscribe to changes to the battery state, but you can get the current
    // state by simply passing null in as your receiver.  Nifty, isn't that?
    IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    Intent batteryStatus = this.registerReceiver(null, filter);

    // There are currently three ways a device can be plugged in. We should check them all.
    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);
}

(4). Wakelock and Battery Drain

高效的保留更多的電量與不斷促使用戶使用你的App會消耗電量,這是矛盾的選擇題。不過咱們可使用一些更好的辦法來平衡二者。

假設你的手機裏面裝了大量的社交類應用,即便手機處於待機狀態,也會常常被這些應用喚醒用來檢查同步新的數據信息。Android會不斷關閉各類硬件來延長手機的待機時間,首先屏幕會逐漸變暗直相當閉,而後CPU進入睡眠,這一切操做都是爲了節約寶貴的電量資源。可是即便在這種睡眠狀態下,大多數應用仍是會嘗試進行工做,他們將不斷的喚醒手機。一個最簡單的喚醒手機的方法是使用PowerManager.WakeLock的API來保持CPU工做並防止屏幕變暗關閉。這使得手機能夠被喚醒,執行工做,而後回到睡眠狀態。知道如何獲取WakeLock是簡單的,但是及時釋放WakeLock也是很是重要的,不恰當的使用WakeLock會致使嚴重錯誤。例如網絡請求的數據返回時間不肯定,致使原本只須要10s的事情一直等待了1個小時,這樣會使得電量白白浪費了。這也是爲什麼使用帶超時參數的wakelock.acquice()方法是很關鍵的。

可是僅僅設置超時並不足夠解決問題,例如設置多長的超時比較合適?何時進行重試等等?解決上面的問題,正確的方式多是使用非精準定時器。一般狀況下,咱們會設定一個時間進行某個操做,可是動態修改這個時間也許會更好。例如,若是有另一個程序須要比你設定的時間晚5分鐘喚醒,最好可以等到那個時候,兩個任務捆綁一塊兒同時進行,這就是非精肯定時器的核心工做原理。咱們能夠定製計劃的任務,但是系統若是檢測到一個更好的時間,它能夠推遲你的任務,以節省電量消耗。

                                 alarmmanager_inexact_wakelock

這正是JobScheduler API所作的事情。它會根據當前的狀況與任務,組合出理想的喚醒時間,例如等到正在充電或者鏈接到WiFi的時候,或者集中任務一塊兒執行。咱們能夠經過這個API實現不少免費的調度算法。

(5). Network and Battery Drain

下面內容來自官方Training文檔中高效下載章節關於手機(Radio)蜂窩信號對電量消耗的介紹。

一般狀況下,使用3G移動網絡傳輸數據,電量的消耗有三種狀態:

  • Full power: 能量最高的狀態,移動網絡鏈接被激活,容許設備以最大的傳輸速率進行操做。
  • Low power: 一種中間狀態,對電量的消耗差很少是Full power狀態下的50%。
  • Standby: 最低的狀態,沒有數據鏈接須要傳輸,電量消耗最少。

下圖是一個典型的3G Radio State Machine的圖示(來自AT&T,詳情請點擊這裏):

              mobile_radio_state_machine.png

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

經過前面學習到的Battery Historian咱們能夠獲得設備的電量消耗數據,若是數據中的移動蜂窩網絡(Mobile Radio)電量消耗呈現下面的狀況,間隔很小,又頻繁斷斷續續的出現,說明電量消耗性能很很差:

             android_perf_battery_bad.png

通過優化以後,若是呈現下面的圖示,說明電量消耗的性能是良好的:

              android_perf_battery_good

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

             android_perf_battery_wifi

那麼如何纔可以把任務緩存起來,作到批量化執行呢?下面就輪到Job Scheduler出場了。

(6). Using Job Scheduler

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

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

public class MyJobService extends JobService {
    private static final String LOG_TAG = "MyJobService";

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(LOG_TAG, "MyJobService created");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i(LOG_TAG, "MyJobService destroyed");
    }

    @Override
    public boolean onStartJob(JobParameters params) {
        // This is where you would implement all of the logic for your job. Note that this runs
        // on the main thread, so you will want to use a separate thread for asynchronous work
        // (as we demonstrate below to establish a network connection).
        // If you use a separate thread, return true to indicate that you need a "reschedule" to
        // return to the job at some point in the future to finish processing the work. Otherwise,
        // return false when finished.
        Log.i(LOG_TAG, "Totally and completely working on job " + params.getJobId());
        // First, check the network, and then attempt to connect.
        if (isNetworkConnected()) {
            new SimpleDownloadTask() .execute(params);
            return true;
        } else {
            Log.i(LOG_TAG, "No connection on job " + params.getJobId() + "; sad face");
        }
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        // Called if the job must be stopped before jobFinished() has been called. This may
        // happen if the requirements are no longer being met, such as the user no longer
        // connecting to WiFi, or the device no longer being idle. Use this callback to resolve
        // anything that may cause your application to misbehave from the job being halted.
        // Return true if the job should be rescheduled based on the retry criteria specified
        // when the job was created or return false to drop the job. Regardless of the value
        // returned, your job must stop executing.
        Log.i(LOG_TAG, "Whelp, something changed, so I'm calling it on job " + params.getJobId());
        return false;
    }

    /**
     * Determines if the device is currently online.
     */
    private boolean isNetworkConnected() {
        ConnectivityManager connectivityManager =
                (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
        return (networkInfo != null && networkInfo.isConnected());
    }

    /**
     *  Uses AsyncTask to create a task away from the main UI thread. This task creates a
     *  HTTPUrlConnection, and then downloads the contents of the webpage as an InputStream.
     *  The InputStream is then converted to a String, which is logged by the
     *  onPostExecute() method.
     */
    private class SimpleDownloadTask extends AsyncTask<JobParameters, Void, String> {

        protected JobParameters mJobParam;

        @Override
        protected String doInBackground(JobParameters... params) {
            // cache system provided job requirements
            mJobParam = params[0];
            try {
                InputStream is = null;
                // Only display the first 50 characters of the retrieved web page content.
                int len = 50;

                URL url = new URL("https://www.google.com");
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setReadTimeout(10000); //10sec
                conn.setConnectTimeout(15000); //15sec
                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(mJobParam, false);
            Log.i(LOG_TAG, result);
        }
    }
}

而後模擬經過點擊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();
            }
        });
    }

    /**
     * This method polls the server via the JobScheduler API. By scheduling the job with this API,
     * your app can be confident it will execute, but without the need for a wake lock. Rather, the
     * API will take your network jobs and execute them in batch to best take advantage of the
     * initial network connection cost.
     *
     * The JobScheduler API works through a background service. In this sample, we have
     * a simple service in MyJobService to get you started. The job is scheduled here in
     * the activity, but the job itself is executed in MyJobService in the startJob() method. For
     * example, to poll your server, you would create the network connection, send your GET
     * request, and then process the response all in MyJobService. This allows the JobScheduler API
     * to invoke your logic without needed to restart your activity.
     *
     * For brevity in the sample, we are scheduling the same job several times in quick succession,
     * but again, try to consider similar tasks occurring over time in your application that can
     * afford to wait and may benefit from batching.
     */
    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 (for brevity in the sample)
                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // WiFi or data connections
                    .build();

            mWakeLockMsg.append("Scheduling job " + i + "!\n");
            scheduler.schedule(jobInfo);
        }
    }
}
相關文章
相關標籤/搜索