動手寫Android內的計劃任務定時框架

香脆的大雞排發表於2017-11-10

在我講解框架之前,我們先來看我一天中的計劃需求。

計劃任務:

7:30~8:30 起床
8:40~9:00 去公司的路上
9:10~9:30 早會
10:00~11:00 技術群裡吹水
11:00~11:10 改了XXXActivity的變數命名(高大上的重構。懂嗎?)
11:10~12:00 思考中午吃什麼

13:00~14:00 睡午覺
14:30~18:00 群裡鬥圖 吃零食 撩妹子 喝茶 玩手機 逛淘寶(今晚雙十一呀)
18:00~18:30 要下班了隨便搞了兩下程式碼 順便git commit -m '今天勞資做得最有意義的事情就是刪掉了兩行程式碼 真它娘贊'

19:00~20:00 回家的路上
21:00~22:00 會所X模 大保健
23:00~3:00 刷微博 內X段子 轉發在群裡 然後吹上一句,我tm才是嗨到最晚的男人
4:00~7:30 該睡覺了

這一天天過得,好呀。 好! 這才叫生活,不叫活著。

我:
“別和我講什麼番茄工作法、四相圖,我只知道我的todoList 每天都是這般重複。”

旁白君:

“哥,你們公司還有空崗位不。我也想....。”

我:


目錄

  • 需求分析
  • 設計框架
  • 如何使用
  • API
  • 注意
  • 引入
  • 使用場景

需求分析

在上面的時間軸裡,我們可以把某段時間點,做某件事情當作是一個任務包。這樣如果用程式碼來表示它就像是這樣的。


            OneDayTask morning = new OneDayTask();
            morning.setStarTime(dataOne("2017-11-11 07:30:00"));  
            morning.setEndTime(dataOne("2017-11-11 08:30:00"));  
            morning.msg = "起床啦";


            OneDayTask work = new OneDayTask();
            work.setStarTime(dataOne("2017-11-11 14:30:00")); 
            work.setEndTime(dataOne("2017-11-11 18:00:00")); 
            work.msg = "群裡鬥圖 吃零食 撩妹子 喝茶 玩手機 逛淘寶";

     //為方便展示,省略其它任務複製程式碼

我們用android手機來模擬一下,這些騷操作。那麼也就是說:

我們需要在7:30這個時間點上收到一條通知,叫"起床啦"任務。
下午14:30收到一條通知,為下午的工作任務。

這樣來想問題的話,想必我們需要一個基於觀察者模式的通知,我們想一想如何在指定的時間來觸發傳送操作呢?

假設1:
開啟一個子執行緒,裡面寫上一個死迴圈。不斷的獲取系統當前時間來判斷是否滿足任務的開始時間和結束時間。當滿足條件時就從佇列中取出這個條任務分發出去。
請思考一下,這個有沒有毛病?


//虛擬碼如下
 OneDayTask morning = new OneDayTask();
do{
  if(getNowTime()== morning.getStarTime()){
  //todo 取出任務 分發出去
  }
}while (true);複製程式碼

首先,我想說這個設計是有問題的。

1.cpu在切換程式碼的執行片段時,可能很快,但是也許有那麼一瞬間已經過了那一秒鐘,而if語句還未得到執行。當getNowTime方法真正執行時,就已經過期了。

2.執行緒裡做死迴圈操作,你覺著合適嗎?反正我覺得挺不合適的。

然鵝。wing神-大精告訴我說,底層處理還是逃離不了。

當然我不存在說用系統給我發的每秒鐘一個的廣播去使用,這樣不友好。目前的方案是封裝AlarmMannager定時任務+廣播通知回掉。每解決一個任務塞入下一個任務交給AlarmMannager來處理,當AlarmMannager定時任務結束後會發起廣播。廣播會再次呼叫下一組任務註冊給AlarmMannager,如此迴圈。聽著有點繞啊。但其實就兩個角色,我們可以把它當作類似遞迴呼叫。但是好處是我們不需要寫什麼死迴圈這種東西。因為AlarmMannager支援定時任務。

沒忍住去翻了下系統鬧鐘的定時實現原始碼。

接下來我們就要考慮下面的問題。

1.AlarmMannager在不同的碎片化機型的處理。
2.如果使用AlarmMannager作為核型就必須把佇列中的任務按起始時間進行排序。
3.如果使用到了廣播,在多組定時任務時,aciton不能重複。否則廣播會紊亂。
4.廣播最好不要用靜態的,要用動態的,因為做成開源輪子,使用者如果使用了類似360的外掛化框架,將導致靜態廣播無效的問題。

設計框架

如果不進行封裝裸裸的呼叫定時任務+廣播的話,整個程式碼會非常散亂,毫無設計可言。也無法複用。那麼我們索性花點時間給寫好一點的。

先來一張UML圖。這是整個框架的設計。非常簡潔只有兩個類和一個介面。其中要處理的任務做了泛型。我把這個框架叫TimeTask。

首先來看Task類。

//  get set 省略
public class Task {
  long  starTime;
  long  endTime;
 }複製程式碼

這裡的Task我們可以把它看作是一個任務,他僅僅只有兩個欄位。一個開始時間,一個結束時間。後續我們自定義的任務都必須繼承Task。(這裡有點類似Recyclerview.ViewHolder的設計。)

TimeHandler

public interface TimeHandler<T extends Task> {
    void exeTask(T mTask);//馬上要執行
    void overdueTask(T mTask);//已過期
    void futureTask(T mTask);//未來會執行
}複製程式碼

TimeHandler是一個接收器,也可以理解為觀察者模式裡的監聽器。它主要接受馬上要執行的&已經過期的&未來會執行的任務。

TimeTask

public class TimeTask<T extends Task> {

    private List<TimeHandler> mTimeHandlers = new ArrayList<TimeHandler>();
    private static PendingIntent mPendingIntent;
    private List<T> mTasks= new ArrayList<T>();
    private  List<T> mTempTasks;
    String mActionName;
    private  boolean isSpotsTaskIng = false;
    private  int cursor = 0;
    private Context mContext;
    private TimeTaskReceiver receiver;

    /**
     *
     * @param mContext
     * @param actionName action不要重複
     */
    public TimeTask(Context mContext,@NonNull String actionName) {
       this.mContext=mContext;
       this.mActionName=actionName;
        initBreceiver(mContext);
    }

    private void initBreceiver(Context mContext) {
        receiver = new TimeTaskReceiver();
        IntentFilter filter = new IntentFilter();
        filter.addAction(mActionName);
        mContext.registerReceiver(receiver, filter);
    }


    public void setTasks(List<T> mES) {
        cursorInit();
        if (mTempTasks !=null){
            mTempTasks = mES;
        }else {
            this.mTasks = mES;
        }
    }

    /**
     * 任務計數歸零
     */
    private void cursorInit() {
        cursor = 0;
    }

    /**
     * 新增任務監聽
     * @param mTH
     * @return
     */
    public TimeTask addHandler(TimeHandler<T> mTH) {
        mTimeHandlers.add(mTH);
        return this;
    }

    /**
     * 開始任務
     */
    public void startLooperTask() {

        if (isSpotsTaskIng&&mTasks.size() == cursor){ //恢復普通任務
            recoveryTask();
            return;
        }

        if (mTasks.size() > cursor){
            T mTask = mTasks.get(cursor);
            long mNowtime = System.currentTimeMillis();
            //在當前區間內立即執行
            if (mTask.getStarTime() < mNowtime && mTask.getEndTime() > mNowtime) {
                for (TimeHandler mTimeHandler : mTimeHandlers) {
                    mTimeHandler.exeTask(mTask);
                }
                Log.d("TimeTask","推送cursor:" + cursor + "時間:" + new Date(mTask.getStarTime()));
            }
            //還未到來的訊息 加入到定時任務
            if (mTask.getStarTime() > mNowtime && mTask.getEndTime() > mNowtime) {
                for (TimeHandler mTimeHandler : mTimeHandlers) {
                    mTimeHandler.futureTask(mTask);
                }
                Log.d("TimeTask","預約cursor:" + cursor + "時間:" + new Date(mTask.getStarTime()));
                configureAlarmManager(mTask.getStarTime());
                return;
            }
            //訊息已過期
            if (mTask.getStarTime() < mNowtime && mTask.getEndTime() < mNowtime) {
                for (TimeHandler mTimeHandler : mTimeHandlers) {
                    mTimeHandler.overdueTask(mTask);
                }
                Log.d("TimeTask","過期cursor:" + cursor + "時間:" + new Date(mTask.getStarTime()));
            }
            cursor++;
            if (isSpotsTaskIng&&mTasks.size() == cursor){ //恢復普通任務
                configureAlarmManager(mTask.getEndTime());
                return;
            }
            startLooperTask();
        }
    }


    /**
     * 停止任務
     */
    public void stopLooper() {
        cancelAlarmManager();
    }

    /**
     * 裝在定時任務
     * @param Time
     */
    private void configureAlarmManager(long Time) {
        AlarmManager manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE);
        PendingIntent pendIntent = getPendingIntent();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, Time, pendIntent);
        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            manager.setExact(AlarmManager.RTC_WAKEUP, Time, pendIntent);
        } else {
            manager.set(AlarmManager.RTC_WAKEUP, Time, pendIntent);
        }
    }

    /**
     *  取消定時器
     */
    private void cancelAlarmManager() {
        AlarmManager manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE);
        manager.cancel(getPendingIntent());
    }

    private PendingIntent getPendingIntent() {
        if (mPendingIntent == null) {
            int requestCode = 0;
            Intent intent = new Intent();
            intent.setAction(mActionName);
            mPendingIntent = PendingIntent.getBroadcast(mContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        }
        return mPendingIntent;
    }

    /**
     * 插播任務
     */
    public void spotsTask(List<T> mSpotsTask) {
        // 2017/10/16 暫停 任務分發
        isSpotsTaskIng = true;
        synchronized (mTasks) {
            if (mTempTasks == null&&mTasks!=null) {//沒有發生過插播
                mTempTasks = new ArrayList<T>();
                for (T mTask : mTasks) {
                    mTempTasks.add(mTask);
                }
            }
            mTasks = mSpotsTask;
            //  2017/10/16 恢復 任務分發
            cancelAlarmManager();
            cursorInit();
            startLooperTask();
        }
    }

    /**
     * 恢復普通任務
     */
    private void recoveryTask() {
        synchronized (mTasks) {
            isSpotsTaskIng = false;
            if (mTempTasks != null) {//有發生過插播
                mTasks = mTempTasks;
                mTempTasks = null;
                cancelAlarmManager();
                cursorInit();
                startLooperTask();
            }
        }
    }

    public void onColse(){
        mContext.unregisterReceiver(receiver);
        mContext=null;
    }

    public  class TimeTaskReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            TimeTask.this.startLooperTask(); //預約下一個
        }
    }
}複製程式碼

這段程式碼略長了點,聽我拆開了給大家慢慢道來。
1.首先TimeTask泛型指定了任務必須強制繼承Task。在構造方法中。我們呼叫了initBreceiver註冊了一個廣播。這裡就是我們前面提到的AlarmManager發通知給他的。

2.我們再看addHandler方法接受一個TimeHandler,這裡可以多次註冊。也就是說內部通過List裝了監聽器。到時候分發的時候也會多處可收到訊息。

3.startLooperTask也就是開啟任務執行的方法。內部主要做三件事。恢復插播任務、分發任務、預約任務。

4.上面提到了預約任務,實際預約任務就是利用AlarmManager定時指定時間傳送廣播通知我們到時間了該做事了。而廣播內的onReceive方法回再次回掉startLooperTask方法。這樣下來任務會被分發出去。同時會預約一下組任務。

5.需求分析的時候我們提到了AlarmMannager適配實際上就是針對M和KITKAT進行特殊的API處理。

如何使用

1.定義一個Task為你的任務物件,注意基類Task物件已經包含了任務的啟動時間和結束時間

    class  MyTask extends Task {
        //// TODO: 這裡可以放置你自己的資源,務必繼承Task物件
        String name;
    }複製程式碼

2.定義一個任務接收器

   TimeHandler<MyTask> timeHandler = new TimeHandler<MyTask>() {
        @Override
        public void exeTask(MyTask mTask) {
               //準時執行
              // 一般來說,在exeTask方法中處理你的邏輯就好可以,過期和未來的都不需要關注 
        }

        @Override
        public void overdueTask(MyTask mTask) {
                 ///已過期的任務
        }

        @Override
        public void futureTask(MyTask mTask) {
              //未來將要執行的任務
        }
    };複製程式碼

3.定義一個任務分發器,並新增接收器


        TimeTask<MyTask> myTaskTimeTask = new TimeTask<>(MainActivity.this,ACTION); // 建立一個任務處理器
        myTaskTimeTask.addHandler(timeHandler); //新增時間回掉複製程式碼

4.配置你的任務時間間隔,(啟動時間,結束時間)

    private List<MyTask> creatTasks() {
        return  new ArrayList<MyTask>() {{
            MyTask BobTask = new MyTask();
                        //******測試demo請務必修改時間******
                      BobTask.setStarTime(dataOne("2017-11-08 21:57:00"));   //當前時間
                      BobTask.setEndTime(dataOne("2017-11-08 21:57:05"));  //5秒後結束
                      BobTask.name="Bob";
                      add(BobTask);

                      MyTask benTask = new MyTask();
                      benTask.setStarTime(dataOne("2017-11-08 21:57:10")); //10秒開始
                      benTask.setEndTime(dataOne("2017-11-08 21:57:15")); //15秒後結束
                      benTask.name="Ben";
                      add(benTask);
        }};
    }複製程式碼

5.新增你的任務佇列,跑起來.


        myTaskTimeTask.setTasks(creatTasks());//建立時間任務資源 把資源放進去處理
        myTaskTimeTask.startLooperTask();//  啟動複製程式碼

這樣下來,當呼叫 myTaskTimeTask.startLooperTask()後,會先分發給timeHandler名稱為Bob的任務。
隨後10秒分發Ben名稱的任務。 任務處理器會根據我們配置的啟動時間和結束時間進行分發工作。

Api

TimeTask

  • TimeTask(Context mContext,String actionName);//初始化
  • setTasks(List mES);//設定任務列表
  • addHandler(TimeHandler mTH);//新增任務監聽器
  • startLooperTask();//啟動任務
  • stopLooper();//停止任務
  • spotsTask(List mSpotsTask);//插播任務
  • onColse();//關閉 防止記憶體洩漏

程式碼中已有詳細註釋,程式碼不是很複雜看原理讀最好了。

注意:

  • 1.務必確保你的任務佇列中的任務時已經按照時間排序的。
  • 2.務必使用泛型繼承Task任務。
  • 3.如果你需要用到多組TimeTask,要保證actionName不要重複,就是自己給取一個名字。

引入

根gradle上新增

    repositories {
            ...
            maven { url 'https://jitpack.io' }
        }複製程式碼
dependencies {
    compile 'com.github.BolexLiu:TimeTask:1.1'
}複製程式碼

github: github.com/BolexLiu/Ti…

使用場景

簡單來說滿足以下應用場景:

  • 1.當你需要為任務定時啟動和結束
  • 2.你有多組任務,時間線上可能存在重疊的情況

目前線上正式環境的使用情況:

  • 1.電視機頂盒媒體分發
  • 2.android大螢幕廣告機任務輪播

如何下次找到我?

本文首發香脆的大雞排 原創文章轉載請先取得聯絡。

相關文章