Android效能優化之電量篇

胡凱發表於2015-09-16

Google近期在Udacity上釋出了Android效能優化的線上課程,分別從渲染,運算與記憶體,電量幾個方面介紹瞭如何去優化效能,這些課程是Google之前在Youtube上釋出的Android效能優化典範專題課程的細化與補充。

下面是電量篇章的學習筆記,部分內容與前面的效能優化典範有重合,歡迎大家一起學習交流!

1)Understanding Battery Drain

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

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

當裝置處於待機狀態時消耗的電量是極少的,以N5為例,開啟飛航模式,可以待機接近1個月。可是點亮螢幕,硬體各個模組就需要開始工作,這會需要消耗很多電量。

使用WakeLock或者JobScheduler喚醒裝置處理定時的任務之後,一定要及時讓裝置回到初始狀態。每次喚醒蜂窩訊號進行資料傳遞,都會消耗很多電量,它比WiFi等操作更加的耗電。

2)Battery Historian

Battery Historian是Android 5.0開始引入的新API。通過下面的指令,可以得到裝置上的電量消耗資訊:

$ 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生成的列表資料,這裡的資料資訊量很大,這裡就不展開了。


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分鐘喚醒,最好能夠等到那個時候,兩個任務捆綁一起同時進行,這就是非精確定時器的核心工作原理。我們可以定製計劃的任務,可是系統如果檢測到一個更好的時間,它可以推遲你的任務,以節省電量消耗。

這正是JobScheduler API所做的事情。它會根據當前的情況與任務,組合出理想的喚醒時間,例如等到正在充電或者連線到WiFi的時候,或者集中任務一起執行。我們可以通過這個API實現很多免費的排程演算法。
5)Network and Battery Drain

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

通常情況下,使用3G行動網路傳輸資料,電量的消耗有三種狀態:

Full power: 能量最高的狀態,行動網路連線被啟用,允許裝置以最大的傳輸速率進行操作。
Low power: 一種中間狀態,對電量的消耗差不多是Full power狀態下的50%。
Standby: 最低的狀態,沒有資料連線需要傳輸,電量消耗最少。

下圖是一個典型的3G Radio State Machine的圖示

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

通過前面學習到的Battery Historian我們可以得到裝置的電量消耗資料,如果資料中的移動蜂窩網路(Mobile Radio)電量消耗呈現下面的情況,間隔很小,又頻繁斷斷續續的出現,說明電量消耗效能很不好:

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

另外WiFi連線下,網路傳輸的電量消耗要比行動網路少很多,應該儘量減少行動網路下的資料傳輸,多在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);  
        }  
    }  
}

相關文章