安卓電量優化之AlarmManager使用全部解析


版權聲明:本文出自汪磊的博客,轉載請務必注明出處。

一、AlarmManager概述

AlarmManager是安卓系統中一種系統級別的提示服務,可以在我們設定時間或者周期性的執行一個intent,這個intent可以是啟動Service服務、發送廣播、跳轉Activity,看到這里是不是會想這不就是定時器Timer嗎,Timer確實是一般定時需求的最便捷實現方式,但是試想一下手機空閑狀態下,屏幕會變暗,最后CPU會停止運行,這樣可以防止電池電量掉的快。在長時間休眠情況下自定義的Timer、Handler、Thread、Service等都會暫停,因為它們沒有喚醒CPU的能力,但是AlarmManager可以喚醒CPU,到達規定的時間就會大吼一聲:"小U,別裝睡了,起來干活",CPU就會乖乖起來干活了。AlarmManager最重要的特性就是能在手機休眠情況下喚醒CPU來工作。

二、AlarmManager重點API講解

使用AlarmManager我們首先獲取AlarmManager系統服務,如下:

1 am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

很簡單,沒有什么特殊需要說明。

API 19之前AlarmManager的常用方法:

(1)set(int type,long startTime,PendingIntent pi)//該方法用於設置一次性定時器,到達時間執行完就完蛋了。

(2)setRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//該方法用於設置可重復執行的定時器。

(3)setInexactRepeating(int type,long startTime,long intervalTime,PendingIntent pi)//該方法用於設置可重復執行的定時器。與setRepeating相比,這個方法更加考慮系統電量,比如系統在低電量情況下可能不會嚴格按照設定的間隔時間執行鬧鍾,因為系統可以調整報警的交付時間,使其同時觸發,避免超過必要的喚醒設備。

參數說明:

int type:鬧鍾類型,常用有五個類型,說明如下:

AlarmManager.ELAPSED_REALTIME 表示鬧鍾在手機睡眠狀態下不可用,就是睡眠狀態下不具備喚醒CPU的能力(跟普通Timer差不多了),該狀態下鬧鍾使用相對時間,相對於系統啟動開始。
AlarmManager.ELAPSED_REALTIME_WAKEUP 表示鬧鍾在睡眠狀態下會喚醒系統並執行提示功能,該狀態下鬧鍾也使用相對時間
AlarmManager.RTC 表示鬧鍾在睡眠狀態下不可用,該狀態下鬧鍾使用絕對時間,即當前系統時間
AlarmManager.RTC_WAKEUP 表示鬧鍾在睡眠狀態下會喚醒系統並執行提示功能,該狀態下鬧鍾使用絕對時間
AlarmManager.POWER_OFF_WAKEUP 表示鬧鍾在手機關機狀態下也能正常進行提示功能,5個狀態中用的最多的狀態之一,該狀態下鬧鍾也是用絕對時間

long startTime:鬧鍾的第一次執行時間,以毫秒為單位。需要注意的是,本屬性與第一個屬性(type)密切相關,如果第一個參數對應的鬧鍾使用的是相對時間(ELAPSED_REALTIME和ELAPSED_REALTIME_WAKEUP),那么本屬性就得使用相對時間,比如當前時間就表示為:SystemClock.elapsedRealtime();如果第一個參數對應的鬧鍾使用的是絕對時間 (RTC、RTC_WAKEUP、POWER_OFF_WAKEUP),那么本屬性就得使用絕對時間,當前時間就表示 為:System.currentTimeMillis()。

long intervalTime:表示兩次鬧鍾執行的間隔時間,也是以毫秒為單位。

PendingIntent pi:到時間后執行的意圖。PendingIntent是Intent的封裝類。需要注意的是,如果是通過啟動服務來實現鬧鍾提 示的話,PendingIntent對象的獲取就應該采用Pending.getService(Context c,int i,Intent intent,int j)方法;如果是通過廣播來實現鬧鍾提示的話,PendingIntent對象的獲取就應該采用 PendingIntent.getBroadcast(Context c,int i,Intent intent,int j)方法;如果是采用Activity的方式來實現鬧鍾提示的話,PendingIntent對象的獲取就應該采用 PendingIntent.getActivity(Context c,int i,Intent intent,int j)方法。關於PendingInten不是本文重點,請自行查閱使用方法。

使用舉例:需求,定義一個在CPU休眠情況下也能執行的鬧鍾,每隔5秒發送一次廣播,代碼如下:

1         Intent intent = new Intent("WANG_LEI"); 2         intent.putExtra("msg","起床了啊"); 3         PendingIntent pi = PendingIntent.getBroadcast(this,0,intent,0); 4 
5         AlarmManager am = (AlarmManager)getSystemService(ALARM_SERVICE); 6         // 每隔5秒后通過PendingIntent pi對象發送廣播 
7         am.setRepeating(AlarmManager.RTC_WAKEUP,System.currentTimeMillis(),5*1000,pi); 

三、AlarmManager的版本適配

以上講解在API<19的情況下能正常運行,但是在API>=19和API<=23手機上運行會發現尼瑪怎么不好使了,比如我們設置1分鍾執行一次,真正運行起來卻變成3分鍾執行一次,這不是坑爹嗎。這是為什么呢?查閱谷歌文檔會發現,關於4.4版本有如下描述:

看到了吧,4.4及以上版本谷歌進行了優化,怎么優化的呢?這樣說吧之前版本比如手機上裝了兩個應用A,B均使用了AlarmManager,A應用設定5秒喚醒一次CPU執行任務,B應用設定7秒喚醒一次CPU執行任務,在API<19手機上這樣運行沒問題的,5秒一次,7秒一次輪着喚醒CPU干活,但是到了4.4及以上版本這樣就不行了,谷歌一想老子出的這功能都被你們玩壞了,照這樣下去小劉5秒一次,小徐6秒一次,小江7秒一次CPU不停地被喚醒這用戶電量都被消耗沒了(喚醒CPU是很耗電的),好,老子直接優化一下,針對這種情況老子統一進行批處理了,你們都給我7秒喚醒一次CPU,這一次你們三個活都干了。大體優化邏輯就是這樣子。

BUT,凡是都有但是啊,你要想在API>=19和API<=23手機上照樣能正常運行咋辦,谷歌還是很貼心的提供額外API,使用setExact(int type, long triggerAtMillis, PendingIntent operation)就可以了。(具體使用代碼文章下面會有,別急)

滿心歡舞的我們修改完后繼續運行了。

BUT,在6.0及以上手機又出問題了,手機在進入休眠狀態一段時間后AlarmManager不工作了,真是服了,繼續找問題吧,發現6.0中谷歌對

低電耗模式和應用待機模式(6.0開始引入)進行了優化,描述如下(原文鏈接:https://developer.android.google.cn/training/monitoring-device-state/doze-standby.html):

看到了吧,描述很清楚了,仔細讀一下就明白了,但是同樣為我們提供了對應API解決:setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)。到此AlarmManager的版本適配就完了,但是還有一個問題setExact(int type, long triggerAtMillis, PendingIntent operation)以及setExactAndAllowWhileIdle(int type, long triggerAtMillis, PendingIntent operation)方法都沒有重復提醒的設置,沒有setRepeating類似API,都是一次性的鬧鍾,我們怎么實現每隔一段時間執行一次任務的需求呢?很簡單,重復注冊就可以了,這么說不明白的話請繼續看下文,Demo會講到。

四、AlarmManager實例Demo講解(包含版本適配以及高版本設置重復鬧鍾)

好了經過上面講解,我相信你是似懂非懂的,因為沒看到具體代碼啊,簡單,一個小Demo就全都明白了。

實現功能:在CPU休眠情況下依然可以每隔五秒發送一次廣播,在廣播接收者中執行相應邏輯(Demo中只是打印Log),適配各個版本。

先看一下最核心的AlarmManagerUtils類:

 1 public class AlarmManagerUtils {  2 
 3     private static final long TIME_INTERVAL = 5 * 1000;//鬧鍾執行任務的時間間隔  4     private Context context;  5     public static AlarmManager am;  6     public static PendingIntent pendingIntent;  7     //  8     private AlarmManagerUtils(Context aContext) {  9         this.context = aContext; 10  } 11 
12     //餓漢式單例設計模式
13     private static AlarmManagerUtils instance = null; 14 
15     public static AlarmManagerUtils getInstance(Context aContext) { 16         if (instance == null) { 17             synchronized (AlarmManagerUtils.class) { 18                 if (instance == null) { 19                     instance = new AlarmManagerUtils(aContext); 20  } 21  } 22  } 23         return instance; 24  } 25 
26     public void createGetUpAlarmManager() { 27         am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 28         Intent intent = new Intent("WANG_LEI"); 29         intent.putExtra("msg", "趕緊起床"); 30         pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);//每隔5秒發送一次廣播 31  } 32 
33     @SuppressLint("NewApi") 34     public void getUpAlarmManagerStartWork() { 35         //版本適配 36         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
37  am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, 38  System.currentTimeMillis(), pendingIntent); 39         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
40  am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), 41  pendingIntent); 42         } else { 43  am.setRepeating(AlarmManager.RTC_WAKEUP, 44  System.currentTimeMillis(), TIME_INTERVAL, pendingIntent); 45  } 46  } 47 
48     @SuppressLint("NewApi") 49     public void getUpAlarmManagerWorkOnReceiver() { 50         //高版本重復設置鬧鍾達到低版本中setRepeating相同效果 51         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// 6.0及以上
52  am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, 53                     System.currentTimeMillis() + TIME_INTERVAL, pendingIntent); 54         } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {// 4.4及以上
55  am.setExact(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() 56                     + TIME_INTERVAL, pendingIntent); 57  } 58  } 59 }

AlarmManagerUtils就是將與AlarmManager有關的操作都封裝起來了,方便解耦。很簡單,主要就是版本適配了,上面已經講解夠仔細了,這里就是判斷不同版本調用不同API了。

MainActivity代碼:

 1 public class MainActivity extends Activity {  2 
 3     private AlarmManagerUtils alarmManagerUtils;  4 
 5  @Override  6     protected void onCreate(Bundle savedInstanceState) {  7         super.onCreate(savedInstanceState);  8  setContentView(R.layout.activity_main);  9         // 10         alarmManagerUtils = AlarmManagerUtils.getInstance(this); 11  alarmManagerUtils.createGetUpAlarmManager(); 12         // 13         findViewById(R.id.am).setOnClickListener(new OnClickListener() { 14 
15             @SuppressLint("NewApi") 16  @Override 17             public void onClick(View v) { 18                 // 19  alarmManagerUtils.getUpAlarmManagerStartWork(); 20  } 21  }); 22  } 23 }

MainActivity中就是調用AlarmManagerUtils中已經封裝好的代碼進行初始化以及點擊Button的時候調用getUpAlarmManagerStartWork方法完成第一次觸發AlarmManager

最后看下廣播接收者中具體做了什么。

MyBroadcastReceiver類:

 1 public class MyBroadcastReceiver extends BroadcastReceiver {  2 
 3     private static final String TAG = "MyBroadcastReceiver";  4 
 5     @SuppressLint("NewApi")  6  @Override  7     public void onReceive(Context context, Intent intent) {  8         //高版本重復設置鬧鍾達到低版本中setRepeating相同效果
 9  AlarmManagerUtils.getInstance(context).getUpAlarmManagerWorkOnReceiver(); 10         // 11         String extra = intent.getStringExtra("msg"); 12         Log.i(TAG, "extra = " + extra); 13  } 14 }

onReceive方法中再次注冊一下AlarmManager達到低版本中setRepeating相同效果。

好了,Demo中核心就是AlarmManagerUtils類,看懂了就全懂了,還需要自己去慢慢研究明白。

四、AlarmManager疑難問題總結

1:進程被殺死,AlarmManager停止工作

在Demo運行的過程中發現我們主動殺死進程AlarmManager也就停止運行了,Log停止打印。我們只能在應用打開或者應用中存在服務在服務重啟的時候重新注冊一下AlarmManager,我還沒有發現什么其余好的辦法,如果你有好的解決辦法,請留言給與解答,向您請教。

2:手機重啟AlarmManager停止工作

其實這個問題和上面進程被殺死情況差不多,這種情況我們可以注冊一個監聽手機重啟的廣播,在收到廣播的時候重新注冊一下AlarmManager就可以了。

3:各廠商的“心跳對齊”

小米,華為等手機廠商,都有“心跳對齊”機制,比如我們開發一個APP,在后台2s就要喚醒一次CPU執行任務,

上面說過喚醒CPU是耗電的(2s就喚醒一次,我只能說產品經理有問題,實現者需求的程序更有問題)。

各大廠商檢測到你APP這么頻繁喚醒CPU對用戶來說是很耗電的,所以系統會將你應用強制"心跳對齊",使你的APP不那么頻繁喚醒CPU。比如你APP設定

的是2s執行一次,但是實際在各大廠商運行起來是10s一次。

五、總結

好了,本文到此就該結束了,相信經過以上講述你對AlarmManager有了更進一步全面了解,在我們使用的時候請不要濫用,考慮一下用戶電量,盡量優化自己APP。

 聲明:文章將會陸續搬遷到個人公眾號,以后文章也會第一時間發布到個人公眾號,及時獲取文章內容請關注公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM