1. 程式人生 > >Android開發筆記(一百六十)休眠模式下的定時器控制

Android開發筆記(一百六十)休眠模式下的定時器控制

定時器AlarmManager常常用於需要週期性處理的場合,比如鬧鐘提醒、任務輪詢等等。並且定時器來源於系統服務,即使App已經不在運行了,也能收到定時器發出的廣播而被喚醒。似此迴光返照的神技,便遭到開發者的濫用,造成使用者手機充斥著各種殺不光程序,就算通過手機安全工具一再地清理記憶體,只要定時設定的時刻到達,剛殺掉的流氓App就會死灰復燃。長此以往,手機的執行速度越來越慢,記憶體也越來越不夠用了,更糟糕的是,電量消耗地越來越快。

Android手機越用越慢的毛病老大不掉,為此每次系統版本升級,Android都力圖在穩定性、安全性上有所改善。針對定時器AlarmManager的濫用問題,Android從4.4開始,修改了setRepeating方法的執行規則。原本該方法可指定每隔固定時間就傳送定時廣播,但在Android4.4之後,作業系統為了節能省電,將會自動調整定時器喚醒的時間。比如原來呼叫setRepeating方法設定了每隔10秒傳送廣播,但App在實際執行過程中,很可能過了好幾分鐘才傳送一次廣播,這意味著該方法將不再保證每次工作都在開發者設定的時間開始。

正如博文《Android開發筆記(七十五)記憶體洩漏的處理》描述的那樣,當時為了演示定時器發生記憶體洩漏的場景,並沒有直接呼叫setRepeating方法,而是接力呼叫set方法。App每次收到定時廣播之後,還得重新開始下一次的定時任務,如此方可相容Android4.4之後的持續定時功能。下面是將setRepeating方法改為使用set方法實現的程式碼例子:
    private String ALARM_EVENT = "com.example.performance.alarm";
    private static AlarmManager mAlarmManager;
    private static PendingIntent pIntent;
    private static int mDelay = 3000;
    
    // 設定定時任務,注意setRepeating的時間間隔並不可靠,只能呼叫set方法間接實現定時
    private void setAlarm() {
        Intent intent = new Intent(ALARM_EVENT);
        pIntent = PendingIntent.getBroadcast(this, 0, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
        mAlarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
        // 在API 19(即Android4.4)之後,作業系統為了節能省電,會調整alarm喚醒的時間,
        // 所以setRepeating方法不保證每次工作都在指定的時間開始,
        // 此時需要先登出原鬧鐘,再呼叫set方法開啟新鬧鐘。
        // mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
        //          System.currentTimeMillis(), mDelay, pIntent);
        mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis()+mDelay, pIntent);
    }

    // 定義一個定時廣播的接收器
    public static class AlarmReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent != null) {
                if (tv_alarm != null) {
                    mDesc = String.format("%s\n%s 鬧鐘時間到達", mDesc, DateUtil.getNowTime());
                    tv_alarm.setText(mDesc);
                    // 設定下一次的定時任務
                    repeatAlarm();
                }
            }
        }
    }
    
    // 每次時刻到達,都重新設定下一次的定時任務,從而間接實現了持續喚醒的功能
    private static void repeatAlarm() {
        // 取消原有的定時任務
        mAlarmManager.cancel(pIntent);
        // 開啟新的定時任務
        mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis()+mDelay, pIntent);
    }

上面瞞天過海的辦法看似完美規避了Android4.4的執行規則,可惜廣大開發者還沒來得及沾沾自喜,Android6.0又推出了更加嚴格的休眠模式。所謂休眠模式,即是當手機螢幕關閉的時候(又稱熄屏、暗屏),系統就會自動開啟休眠模式,這樣原本正在執行的App將進入掛起模式,不能再進行訪問網路等常用操作。當然為了保證App不被完全掛死,系統也會定期退出休眠模式,好比青蛙從冬眠之中甦醒過來,在甦醒期間,系統允許掛起的App重新恢復執行,繼續先前設定好的任務。可是這個甦醒期是短暫的(通常只有幾秒),一旦甦醒期結束,系統又重新進入休眠模式,於是那些App再次掛起,等待下次甦醒期的到來,如此往復。當然,只要手機恢復亮屏,比如使用者按下電源鍵、使用者給手機插上電源、手機接到來電等等,系統便自動退出休眠模式,所有掛起的App都會恢復正常運轉。

手機在休眠期間,之前通過定時器的set方法設定好的定時任務,即使定時的時刻到達,也要等到甦醒期間才會得到執行。如果一定要在休眠期喚醒鬧鐘,就得呼叫setAndAllowWhileIdle代替set方法,或者呼叫setExactAndAllowWhileIdle代替setExact方法。其中setAndAllowWhileIdle與setExactAndAllowWhileIdle這兩個方法是Android從6.0開始新增的定時方法,字面意思是即使正在休眠、也要執行定時任務。然而休眠模式的本意是掛起包括定時任務在內的App事務,現在卻提供setAndAllowWhileIdle方法留下了後門,為開發者的雞鳴狗盜之事大開方便,如此規定豈不是貽笑大方?

這光景,簡直是活脫脫的一出Android版本的自相矛盾,話說Android設計師當街叫賣Android的安全盾,號稱這面盾很牢固、沒有矛可以刺穿;前來踢館的開發者拿著一把Android的setRepeating矛,說道這把矛可以破了那面盾。設計師眼看不妙,趕忙拿起另一面名叫Android4.4的安全盾,又稱你的setRepeating矛不行了;開發者精明得很,隨身抄著一把Android的set矛,又道這把矛可以破了那面Android4.4的盾。設計師火冒三丈,心想豈能甘拜下風,於是拿出一面Android6.0的休眠盾,聲稱有此盾護身不怕set矛;誰料道高一尺、魔高一丈,開發者奪過一把Android出產的setAndAllowWhileIdle矛,依舊能刺開Android6.0休眠盾。結果Android設計師大汗淋漓,卻不肯認輸,嘴裡碎碎念:“此山是我開,此樹是我栽,要從此路過,留下買路財。罷了罷了,甭管你的矛有多鋒利,反正我規定休眠盾至少能抗住九分鐘。”這裡的九分鐘參見Android官方說明:Neither setAndAllowWhileIdle() nor setExactAndAllowWhileIdle() can fire alarms more than once per 9 minutes, per app,意思是不管是setAndAllowWhileIdle還是setExactAndAllowWhileIdle,在休眠期內每個App每隔9分鐘最多隻能喚醒一次鬧鐘。

一方面要照顧使用者的手機省電需求,另一方面要考慮開發者的業務實現,開發Android的谷歌公司真是煞費苦心,只可惜魚與熊掌不可兼得呀。我們作為開發者,要讓定時器適配Android6.0的休眠模式倒也不難,只需把下面這行的set方法程式碼:
        mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis()+mDelay, pIntent);
改成下面相容6.0的程式碼就好了:
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            mAlarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,
                    System.currentTimeMillis()+mDelay, pIntent);
        } else {
            mAlarmManager.set(AlarmManager.RTC_WAKEUP,
                    System.currentTimeMillis()+mDelay, pIntent);
        }
其實就是判斷當前系統版本,對於Android6.0及以上版本,使用setAndAllowWhileIdle方法替換set方法即可。


點此檢視Android開發筆記的完整目錄