封裝一個阻塞佇列,輕鬆實現排隊執行任務功能!

L_Xian發表於2019-01-03

前言

個人覺得佇列的使用在專案開發中挺多地方可以用到的,所以將如何封裝一個佇列的過程記錄下來,總體來說難度並不大,但畢竟能力有限,如果各位有好的建議或意見歡迎提出來,如果本文能幫到你的話,記得點贊哦。

需求背景

在專案開發中,會經常遇到一些需要排隊執行的功能,比如發動態時上傳多張圖片,需要一張一張的上傳,比如直播間動畫連需傳送或者收到訊息需要展示時,需要一個一個動畫去展示等等場景,這時候會容易想到用佇列去實現,但是我想不少小夥伴會直接弄一個 list,存著要執行的任務,然後通過遞迴的方式去遍歷列表實現,類似下面程式碼:

//存著一些執行任務的列表
List<String> list = new ArrayList<>();
//執行任務
private void doTask() {
    if (list.size() > 0) {
        String task = list.get(0);
        doSoming(task);
        list.remove(0);
        doTask();
    }
}
複製程式碼

首先這種方式實現是可以完成所需要的功能的,面對一些簡單的場景來說比較容易想到而且實現也簡單。但是面對複雜一點的場景卻有著不少缺點,特別是在一些祖傳專案裡面,誰也說不準程式碼有多亂,到時候維護是很困難的(這個本人已在實際專案中深有體會)。所以封裝一個佇列去完成這種功能,是一個比較實用而且必須的手段。

首先看看要封裝的佇列需要有什麼功能:

  1. 在實際中,我們執行的任務大概可以分兩種,一個是有明確的執行時間的,比如,要連續顯示10個動畫,每個展示5秒這種。一個是沒明確的執行時間的,比如連續上傳10張圖片,每個上傳任務的完成時間是需要等到上傳成功回撥回來才知道的這種。所以佇列第一個功能是每個任務都可以相容這兩種情況,而且當然是一個執行完再執行下一個,排隊執行。
  2. 既然要排隊執行,當然會有優先順序之分,所以每個任務都能設定優先順序,佇列可以根據優先順序去排隊執行任務。

至於佇列選哪一個,我這裡選擇的是 PriorityBlockingQueue(阻塞優先順序佇列),這個佇列的特點是儲存的物件必須是實現Comparable介面,而且它是阻塞佇列,其他特點或者不瞭解的同學可以自行去了解,接下來都是對它的封裝。

1. 定義一個列舉類TaskPriority,定義任務的優先順序。

public enum TaskPriority {
    LOW, //低
    DEFAULT,//普通
    HIGH, //高
}
複製程式碼

優先順序分為3種,如註釋所示,他們的關係:LOW<DEFAULT<HIGH

2.佇列任務執行時間確定和不確定兩種情況的實現策略

  1. 針對任務執行時間確定的這種情況,比較簡單,可以給這個任務設定一個時間 duration,等任務開始執行時,便開始阻塞等待,等到時間到達時,才放開執行下一個任務。
  2. 針對任務執行時間不確定的情況,我們則需要在任務執行完成的回撥裡面手動放開,所以這裡打算再用一個PriorityBlockingQueue佇列,因為它有這樣的一個特點:如果佇列是空的,呼叫它的 take()方法時,它就會一直阻塞在那裡,當列表不為空時,這個方法就不會阻塞。 所以當任務開始執行時,呼叫take()方法,等到我們的完成回撥來時,再手動給它add一個值,阻塞就放開,再執行下一個任務。

3.確定了任務兩種情況的實現策略後,接下來定義一個介面,定義一下每個任務需要做什麼事情

public interface ITask extends Comparable<ITask> {
    //將該任務插入佇列
    void enqueue();

    //執行具體任務的方法
    void doTask();

    //任務執行完成後的回撥方法
    void finishTask();

    //設定任務優先順序
    ITask setPriority(TaskPriority mTaskPriority);

    //獲取任務優先順序
    TaskPriority getPriority();

    //當優先順序相同 按照插入順序 先入先出 該方法用來標記插入順序
    void setSequence(int mSequence);

    //獲取入隊次序
    int getSequence();

    //每個任務的狀態,就是標記完成和未完成
    boolean getStatus();
    
    //設定每個任務的執行時間,該方法用於任務執行時間確定的情況
    ITask setDuration(int duration);
    
    //獲取每個任務執行的時間
    int getDuration();

    //阻塞任務執行,該方法用於任務執行時間不確定的情況
    void blockTask() throws Exception;

    //解除阻塞任務,該方法用於任務執行時間不確定的情況
    void unLockBlock();
}
複製程式碼

介面基本定義了一些基本需要的方法,因為PriorityBlockingQueue的特點,所以介面繼承了Comparable,用於實現優先順序排隊功能。具體方法功能請看註釋。

4.封裝一下PriorityBlockingQueue的基本功能

public class BlockTaskQueue {
    private String TAG = "BlockTaskQueue";
    private AtomicInteger mAtomicInteger = new AtomicInteger();
    //阻塞佇列
    private final BlockingQueue<ITask> mTaskQueue = new PriorityBlockingQueue<>();

    private BlockTaskQueue() {
    }
    //單例模式
    private static class BlockTaskQueueHolder {
        private final static BlockTaskQueue INSTANCE = new BlockTaskQueue();
    }

    public static BlockTaskQueue getInstance() {
        return BlockTaskQueueHolder.INSTANCE;
    }

    /**
     * 插入時 因為每一個Task都實現了comparable介面 所以佇列會按照Task複寫的compare()方法定義的優先順序次序進行插入
     * 當優先順序相同時,使用AtomicInteger原子類自增 來為每一個task 設定sequence,
     * sequence的作用是標記兩個相同優先順序的任務入隊的次序
     */
    public <T extends ITask> int add(T task) {
        if (!mTaskQueue.contains(task)) {
            task.setSequence(mAtomicInteger.incrementAndGet());
            mTaskQueue.add(task);
            Log.d(TAG, "\n add task " + task.toString());
        }
        return mTaskQueue.size();
    }

    public <T extends ITask> void remove(T task) {
        if (mTaskQueue.contains(task)) {
            Log.d(TAG, "\n" + "task has been finished. remove it from task queue");
            mTaskQueue.remove(task);
        }
        if (mTaskQueue.size() == 0) {
            mAtomicInteger.set(0);
        }
    }

    public ITask poll() {
        return mTaskQueue.poll();
    }

    public ITask take() throws InterruptedException {
        return mTaskQueue.take();
    }

    public void clear() {
        mTaskQueue.clear();
    }

    public int size() {
        return mTaskQueue.size();
    }
}
複製程式碼

這裡是一個簡單的封裝,具體解釋請看註釋。

5. 寫一個類記錄下當前執行的任務資訊,方便獲取時使用

public class CurrentRunningTask {
    private static ITask sCurrentShowingTask;

    public static void setCurrentShowingTask(ITask task) {
        sCurrentShowingTask = task;
    }

    public static void removeCurrentShowingTask() {
        sCurrentShowingTask = null;
    }

    public static ITask getCurrentShowingTask() {
        return sCurrentShowingTask;
    }

    public static boolean getCurrentShowingStatus() {
        return sCurrentShowingTask != null && sCurrentShowingTask.getStatus();
    }
}
複製程式碼

有時候要獲取一些正在執行的任務的資訊,所以這裡弄類一個類來將正在執行的任務儲存起來。

6. 基礎需要的東西都寫好後,下面開始封裝一個基礎的任務類了

public class BaseTask implements ITask {
    private final String TAG = getClass().getSimpleName();
    private TaskPriority mTaskPriority = TaskPriority.DEFAULT; //預設優先順序
    private int mSequence;// 入隊次序
    private Boolean mTaskStatus = false; // 標誌任務狀態,是否仍在展示
    protected WeakReference<BlockTaskQueue> taskQueue;//阻塞佇列
    protected int duration = 0; //任務執行時間
    //此佇列用來實現任務時間不確定的佇列阻塞功能
    private PriorityBlockingQueue<Integer> blockQueue;
    //建構函式
    public BaseTask() {
        taskQueue = new WeakReference<>(BlockTaskQueue.getInstance());
        blockQueue = new PriorityBlockingQueue<>();
    }
    //入隊實現
    @Override
    public void enqueue() {
        TaskScheduler.getInstance().enqueue(this);
    }
    //執行任務方法,此時標記為設為true,並且將當前任務記錄下來
    @Override
    public void doTask() {
        mTaskStatus = true;
        CurrentRunningTask.setCurrentShowingTask(this);
    }
    //任務執行完成,改變標記位,將任務在佇列中移除,並且把記錄清除
    @Override
    public void finishTask() {
        this.mTaskStatus = false;
        this.taskQueue.get().remove(this);
        CurrentRunningTask.removeCurrentShowingTask();
        Log.d(TAG, taskQueue.get().size() + "");
    }
    //設定任務優先順序實現
    @Override
    public ITask setPriority(TaskPriority mTaskPriority) {
        this.mTaskPriority = mTaskPriority;
        return this;
    }
    //設定任務執行時間
    public ITask setDuration(int duration) {
        this.duration = duration;
        return this;
    }
    //獲取任務優先順序
    @Override
    public TaskPriority getPriority() {
        return mTaskPriority;
    }
    //獲取任務執行時間
    @Override
    public int getDuration() {
        return duration;
    }
    //設定任務次序
    @Override
    public void setSequence(int mSequence) {
        this.mSequence = mSequence;
    }
    //獲取任務次序
    @Override
    public int getSequence() {
        return mSequence;
    }
    獲取任務狀態
    @Override
    public boolean getStatus() {
        return mTaskStatus;
    }
    //阻塞任務執行
    @Override
    public void blockTask() throws Exception {
        blockQueue.take(); //如果佇列裡面沒資料,就會一直阻塞
    }
    //解除阻塞
     @Override
     public void unLockBlock() {
        blockQueue.add(1); //往裡面隨便新增一個資料,阻塞就會解除
     }

    /**
     * 排隊實現
     * 優先順序的標準如下:
     * TaskPriority.LOW < TaskPriority.DEFAULT < TaskPriority.HIGH
     * 當優先順序相同 按照插入次序排隊
     */
    @Override
    public int compareTo(ITask another) {
        final TaskPriority me = this.getPriority();
        final TaskPriority it = another.getPriority();
        return me == it ? this.getSequence() - another.getSequence() :
                it.ordinal() - me.ordinal();
    }
    //輸出一些資訊
    @Override
    public String toString() {
        return "task name : " + getClass().getSimpleName() + " sequence : " + mSequence + " TaskPriority : " + mTaskPriority;
    }
}
複製程式碼

程式碼並不難,看註釋應該都會懂,這裡看下入隊方法,並沒有直接 add 到 taskQueue 裡面,而且通過 TaskScheduler 的 enqueue 方法入隊,TaskScheduler 是一個任務排程類,裡面封裝了入隊以及對佇列操作的一些功能,下面看看它是如何實現的。

7. TaskScheduler

public class TaskScheduler {
    private final String TAG = "TaskScheduler";
    private BlockTaskQueue mTaskQueue = BlockTaskQueue.getInstance();
    private ShowTaskExecutor mExecutor;

    private static class ShowDurationHolder {
        private final static TaskScheduler INSTANCE = new TaskScheduler();
    }

    private TaskScheduler() {
        initExecutor();
    }

    private void initExecutor() {
        mExecutor = new ShowTaskExecutor(mTaskQueue);
        mExecutor.start();
    }

    public static TaskScheduler getInstance() {
        return ShowDurationHolder.INSTANCE;
    }

     public void enqueue(ITask task) {
        //因為TaskScheduler這裡寫成單例,如果isRunning改成false的話,不判斷一下,就會一直都是false
        if (!mExecutor.isRunning()) {
            mExecutor.startRunning();
        }
        //按照優先順序插入佇列 依次播放
        mTaskQueue.add(task);
    }

    public void resetExecutor() {
        mExecutor.resetExecutor();
    }

    public void clearExecutor() {
        mExecutor.clearExecutor();
    }
}
複製程式碼

ShowTaskExecutor是一個任務排隊執行器,裡面主要是一個死迴圈,不斷的在佇列裡面取出任務,並且執行任務,下面看看它的實現。

8.ShowTaskExecutor

public class ShowTaskExecutor {
    private final String TAG = "ShowTaskExecutor";
    private BlockTaskQueue taskQueue;
    private TaskHandler mTaskHandler;
    private boolean isRunning = true;
    private static final int MSG_EVENT_DO = 0;
    private static final int MSG_EVENT_FINISH = 1;

    public ShowTaskExecutor(BlockTaskQueue taskQueue) {
        this.taskQueue = taskQueue;
        mTaskHandler = new TaskHandler();
    }
    //開始遍歷任務佇列
    public void start() {
        AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    while (isRunning) { //死迴圈
                        ITask iTask;
                        iTask = taskQueue.take(); //取任務
                        if (iTask != null) {
                            //執行任務
                            TaskEvent doEvent = new TaskEvent();
                            doEvent.setTask(iTask);
                            doEvent.setEventType(TaskEvent.EventType.DO);
                            mTaskHandler.obtainMessage(MSG_EVENT_DO, doEvent).sendToTarget();
                            //一直阻塞,直到任務執行完
                            if (iTask.getDuration()!=0) {
                                TimeUnit.MICROSECONDS.sleep(iTask.getDuration());
                            }else {
                                iTask.blockTask();
                            }
                            //完成任務
                            TaskEvent finishEvent = new TaskEvent();
                            finishEvent.setTask(iTask);
                            finishEvent.setEventType(TaskEvent.EventType.FINISH);
                            mTaskHandler.obtainMessage(MSG_EVENT_FINISH, finishEvent).sendToTarget();
                        }
                    }
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
    }
    //根據不同的訊息回撥不同的方法。
    private static class TaskHandler extends Handler {
        TaskHandler() {
            super(Looper.getMainLooper());
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            TaskEvent taskEvent = (TaskEvent) msg.obj;
            if (msg.what == MSG_EVENT_DO && taskEvent.getEventType() == TaskEvent.EventType.DO) {
                taskEvent.getTask().doTask();
            }
            if (msg.what == MSG_EVENT_FINISH && taskEvent.getEventType() == TaskEvent.EventType.FINISH) {
                taskEvent.getTask().finishTask();
            }
        }
    }

    public void startRunning() {
        isRunning = true;
    }

    public void pauseRunning() {
        isRunning = false;
    }

    public boolean isRunning() {
        return isRunning;
    }

    public void resetExecutor() {
        isRunning = true;
        taskQueue.clear();
    }

    public void clearExecutor() {
        pauseRunning();
        taskQueue.clear();
    }
}
複製程式碼
public class TaskEvent {
    private WeakReference<ITask> mTask;
    int mEventType;

    public ITask getTask() {
        return mTask.get();
    }

    public void setTask(ITask mTask) {
        this.mTask = new WeakReference<>(mTask);
    }

    public int getEventType() {
        return mEventType;
    }

    public void setEventType(int mEventType) {
        this.mEventType = mEventType;
    }

    public static class EventType {
        public static final int DO = 0X00;
        public static final int FINISH = 0X01;
    }
}
複製程式碼

好了整個佇列已經封裝好了,下面看看如何使用吧:

使用方法

  1. 定義一個Task,繼承 BaseTask,並實現對應的方法
public class LogTask extends BaseTask {
    String name;

    public LogTask(String name) {
        this.name = name;
    }

    //執行任務方法,在這裡實現你的任務具體內容
    @Override
    public void doTask() {
        super.doTask();
        Log.i("LogTask", "--doTask-" + name);
        
        //如果這個Task的執行時間是不確定的,比如上傳圖片,那麼在上傳成功後需要手動呼叫
        //unLockBlock方法解除阻塞,例如:
        uploadImage(new UploadListener{
           void onSuccess(){
                unLockBlock();
            }
        });
    }

    //任務執行完的回撥,在這裡你可以做些釋放資源或者埋點之類的操作
    @Override
    public void finishTask() {
        super.finishTask();
        Log.i("LogTask", "--finishTask-" + name);
    }
}
複製程式碼
  1. 然後依次入隊使用
findViewById(R.id.btn1).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new LogTask("DEFAULT")
                .setDuration(5000) //設定了時間,代表這個任務時間是確定的,如果不確定,則不用設定
                .setPriority(TaskPriority.DEFAULT) //設定優先順序,預設是DEFAULT
                .enqueue(); //入隊
    }
});
findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new LogTask("LOW")
                .setDuration(4000)
                .setPriority(TaskPriority.LOW)
                .enqueue();
    }
});
findViewById(R.id.btn3).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        new LogTask("HIGH")
                .setDuration(3000)
                .setPriority(TaskPriority.HIGH)
                .enqueue();
    }
});
複製程式碼

就這樣就可以了,是不是很簡單,隨便依次點選按鈕多下,你會發現任務都在根據優先順序排隊執行了。

封裝一個阻塞佇列,輕鬆實現排隊執行任務功能!

獲取當前執行任務的方法:

LogTask task = (LogTask) CurrentRunningTask.getCurrentShowingTask();
複製程式碼

相關文章