1. 程式人生 > >效能優化十六之Wake_Lock喚醒鎖以及JobScheduler使用

效能優化十六之Wake_Lock喚醒鎖以及JobScheduler使用

前言
      上一篇部落格記錄了電量優化中的第一種優化,把一些不需要及時和使用者互動的一些操作,放到當用戶插上電源的時候。根據自己目前的知識瞭解,只知道三種優化方式,第二種和第三種方式接下來進行介紹。

第二種方式:網路型別選擇優化
      目前大部分手機都支援4G網路,殊不知蜂窩移動訊號是在所有的網路型別中是最消耗電量的,很多人在使用手機的過程中,發現如果一直在使用4G行動網路,電量會持續不了多久就沒電了,而相對來說WIFI會比蜂窩移動訊號的電量消耗會小很多,所以我們在開發過程中可以將某些操作放在連線WIFI後進行操作。如何去判讀網路型別的程式碼例子,網上都有。

第三種方式:wake_lock
      wakelock是個什麼東西呢?查了很多資料瞭解到它是一個喚醒鎖,什麼是喚醒鎖?它主要是相對系統的休眠而言的,意思就是我的程式給CPU加了這個鎖那系統就不會休眠了,這樣做的目的是為了全力配合我們程式的執行。有的情況如果不這麼做就會出現一些問題,比如微信等及時通訊的心跳包會在熄屏不久後停止網路訪問等問題。所以微信裡面是有大量使用到了wake_lock鎖(可以利用WLD進行測試)。

使用場景一:保持螢幕常亮
      當Android裝置空閒時,螢幕會變暗,然後關閉螢幕,最後會停止CPU的執行,這樣可以防止電池電量掉的快。在休眠過程中自定義的Timer、Handler、Thread、Service等都會暫停。但有些時候我們需要改變Android系統預設的這種狀態:比如玩遊戲時我們需要保持螢幕常亮,比如一些下載操作不需要螢幕常亮但需要CPU一直執行直到任務完成。

1、保持螢幕常亮,最好的方式是在Activity中使用FLAG_KEEP_SCREEN_ON 的Flag。
public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
    }
}
優點:這個方法的好處是不像喚醒鎖(wake locks),需要一些特定的許可權(permission)。並且能正確管理不同app之間的切換,不用擔心無用資源的釋放問題(喚醒鎖如何使用下面介紹)。

注意:一般不需要人為的去掉flag,WindowManager會管理好程式進入後臺回到前臺的操作。如果確實需要手動清掉常亮的flag,使用
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
1
2、佈局檔案中設定螢幕常亮:
另一個方式是在佈局檔案中使用android:keepScreenOn屬性:

<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:keepScreenOn="true">
< /RelativeLayout>

android:keepScreenOn = ” true “的作用和FLAG_KEEP_SCREEN_ON一樣。但是使用程式碼的好處是你允許你在需要的地方關閉螢幕。

使用場景二:保持CPU執行
      需要使用PowerManager這個系統服務的喚醒鎖(wake locks)特徵來保持CPU處於喚醒狀態。喚醒鎖允許程式控制宿主裝置的電量狀態。建立和持有喚醒鎖對電池的續航有較大的影響,所以,除非是真的需要喚醒鎖完成儘可能短的時間在後臺完成的任務時才使用它。比如在Acitivity中就沒必要用了。如果需要關閉螢幕,使用上述FLAG_KEEP_SCREEN_ON。
      只有一種合理的使用場景,就是在使用後臺服務需要在螢幕關閉情況下hold住CPU完成一些工作。這時就需要使用喚醒鎖,如果不使用喚醒鎖來執行後臺服務,當CPU在未來的某個時刻休眠導致某個時刻任務會停止,這是我們不想看到的。 (有的人可能認為我以前寫的後臺服務執行得挺好的,1.可能是你的任務時間比較短;2.可能CPU被手機裡面很多其他的軟體一直在喚醒狀態。)。下面是很多網友有同樣的問題:


喚醒鎖可劃分為並識別四種使用者喚醒鎖:

標記值                   CPU  螢幕  鍵盤
PARTIAL_WAKE_LOCK       開啟  關閉  關閉
SCREEN_DIM_WAKE_LOCK    開啟  變暗  關閉
SCREEN_BRIGHT_WAKE_LOCK 開啟  變亮  關閉
FULL_WAKE_LOCK          開啟  變亮  變亮
請注意,自 API 等級 17 開始,FULL_WAKE_LOCK 將被棄用,應用應使用FLAG_KEEP_SCREEN_ON
使用方法一:
第一步就是新增喚醒鎖許可權:

< uses-permission android:name="android.permission.WAKE_LOCK" />
1
第二步直接使用喚醒鎖:

PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"MyWakelockTag");
wakeLock.acquire();
注意:在使用該類的時候,必須保證acquire和release是成對出現的,需要在任務執行完成後呼叫release方法。
第三步釋放喚醒鎖:

private void releaseWakeLock() {
        if (mWakelock.isHeld()) {
            mWakelock.release();//記得釋放CPU鎖
            wakelock_text.append("釋放鎖!");
       }
}
使用方法二:
      雖然上面的方式很簡單,但是推薦的方式是使用WakefulBroadcastReceiver:使用廣播和Service(典型的IntentService)結合的方式可以讓你很好地管理後臺服務的生命週期。

      WakefulBroadcastReceiver是BroadcastReceiver的一種特例。它會為你的APP建立和管理一個PARTIAL_WAKE_LOCK 型別的WakeLock。WakefulBroadcastReceiver把工作交接給service(通常是IntentService),並保證交接過程中裝置不會進入休眠狀態。如果不持有WakeLock,裝置很容易在任務未執行完前休眠。最終結果是你的應用不知道會在什麼時候能把工作完成,相信這不是你想要的。

第一步註冊:

< receiver android:name=".MyWakefulReceiver"></receiver>
1
第二步使用startWakefulService()方法來啟動服務,與startService()相比,在啟動服務的同時,並啟用了喚醒鎖。

public class MyWakefulReceiver extends WakefulBroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {        
        // Start the service, keeping the device awake while the service is        
        // launching. This is the Intent to deliver to the service.        
         Intent service = new Intent(context, MyIntentService.class);        
         startWakefulService(context, service);  
    }
}
第三步當後臺服務的任務完成,要呼叫MyWakefulReceiver.completeWakefulIntent()來釋放喚醒鎖。

public class MyIntentService extends IntentService {
    public static final int NOTIFICATION_ID = 1;
    private NotificationManager mNotificationManager;
    NotificationCompat.Builder builder;

    public MyIntentService() {
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Bundle extras = intent.getExtras();        
        // Do the work that requires your app to keep the CPU running.        
        // 喚醒CPU處理工作
        // Release the wake lock provided by the WakefulBroadcastReceiver.        
         MyWakefulReceiver.completeWakefulIntent(intent);    
    }
}
下面是從網上看到的一些不處理CPU喚醒遇到的一些問題:

1.向伺服器輪詢的程式碼不執行。
      曾經做一個應用,利用Timer和TimerTask,來設定對伺服器進行定時的輪詢,但是發現機器在某段時間後,輪詢就不再進行了。查了很久才發 現是休眠造成的。後來解決的辦法是,利用系統的AlarmService來執行輪詢。因為雖然系統讓機器休眠,節省電量,但並不是完全的關機,系統有一部 分優先順序很高的程式還是在執行的,比如鬧鐘,利用AlarmService可以定時啟動自己的程式,讓cpu啟動,執行完畢再休眠。
2.後臺長連線斷開。
      最近遇到的問題。利用Socket長連線實現QQ類似的聊天功能,發現在螢幕熄滅一段時間後,Socket就被斷開。螢幕開啟的時候需進行重連,但 每次看Log的時候又發現網路是連結的,後來才發現是cpu休眠導致連結被斷開,當你插上資料線看log的時候,網路cpu恢復,一看網路確實是連結的, 坑。最後使用了PARTIAL_WAKE_LOCK,保持CPU不休眠。
3.除錯時是不會休眠的。
      讓我非常鬱悶的是,在除錯APP的時候,就發現,有時Socket會斷開,有時不會斷開,後來才搞明白,因為我有時是插著資料線進行除錯,有時拔掉資料線,這 時Android的休眠狀態是不一樣的。而且不同的機器也有不同的表現,比如有的機器,插著資料線就會充電,有的不會,有的機器的設定的充電時螢幕不變暗 等等,把自己都搞暈了。其實搞明白這個休眠機制,一切都好說了。


定時喚醒解決方法:利用Android自帶的定時器AlarmManager實現:

Intent intent = new Intent(mContext, ServiceTest.class);
PendingIntent pi = PendingIntent.getService(mContext, 1, intent, 0);
AlarmManager alarm = (AlarmManager) getSystemService(Service.ALARM_SERVICE);
if(alarm != null)
{
    alarm.cancel(pi);
    // 鬧鐘在系統睡眠狀態下會喚醒系統並執行提示功能
    alarm.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + 1000, 2000, pi);// 確切的時間鬧鐘//alarm.setExact(…);
    //alarm.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), pi);
}

      該定時器可以啟動Service服務、傳送廣播、跳轉Activity,並且會在系統睡眠狀態下喚醒系統。所以該方法不用獲取電源鎖和釋放電源鎖。
      注意:在19以上版本,setRepeating中設定的頻繁只是建議值(6.0 的原始碼中最小值是60s),如果要精確一些的用setWindow或者setExact。

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進入休眠狀態,就會成為待機電池殺手。比如微信內部就大量使用了wakelock喚醒鎖,理所當然的成了耗電排行第一。

      那麼Wake Lock API有啥用呢?比如心跳包從請求到應答,比如斷線重連重新登陸這些關鍵邏輯的執行過程,就需要Wake Lock來保護。而一旦一個關鍵邏輯執行成功,就應該立即釋放掉Wake Lock了。兩次心跳請求間隔5到10分鐘,基本不會怎麼耗電。除非網路不穩定,頻繁斷線重連,那種情況不多。

      AlarmManager 是Android 系統封裝的用於管理 RTC 的模組,RTC (Real Time Clock) 是一個獨立的硬體時鐘,可以在 CPU 休眠時正常執行,在預設的時間到達時,通過中斷喚醒 CPU。(極光推送就是利用這個來做的。)

總結:
1. 關鍵邏輯的執行過程,就需要Wake Lock來保護。如斷線重連重新登陸
2. 休眠的情況下如何喚醒來執行任務?用AlarmManager。如推送訊息傳送心跳包,獲取資訊
注意:如果請求網路很差,會要很長的時間,一般我們谷歌建議一定要設定請求超時時間。

最重要的注意事項:
      Android開發中,可能會出現AlarmManager在手機休眠時無法喚醒Service的問題。
      問題的提出:
      一個app,需要後臺保持傳送心跳包。由於鎖屏後CPU休眠,導致心跳包執行緒被掛起,所以嘗試使用alarmManager定時喚醒Service傳送心跳包,當傳入的時間引數很短的時候,例如2500ms,很長時間才會去喚醒一次,而且間隔時間是不固定的。
      原因:
      首先2.5s一次喚醒對於手機電池的消耗是多麼的恐怖,做後臺應用的時候需要考慮下是否會影響手機的正常休眠(深睡眠),各個手機廠家為了對付頻繁喚醒的app,都開發了心跳對齊,對於超過指定的頻率就會被廠商給遮蔽或者被心跳對齊了。

JobScheduler引入:
      當遇到大量高頻次的CPU喚醒及操作,我們該如何去優化,頻繁的喚醒肯定導致電量消耗很大。
      這裡我們可以通過谷歌的JobScheduler來實現,將一些頻繁喚醒的任務集中到一起,這樣只需要喚醒一次cpu,就可以完成所有操作。
      使用方法:
      MyJobService.java:

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

MainActivity.java:

    private ComponentName serviceComponent = new ComponentName(this,MyJobService.class);
        //優化
        JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
        for (int i = 0; i < 500; i++) {
            JobInfo jobInfo = new JobInfo.Builder(i,serviceComponent)
                    .setMinimumLatency(5000)//5秒 最小延時、
                    .setOverrideDeadline(60000)//maximum最多執行時間
             //JobInfo.NETWORK_TYPE_UNMETERED//免費的網路---wifi 藍芽 USB
                    .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)//任意網路---wifi
                    .build();
            jobScheduler.schedule(jobInfo);
        }

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