本文讨论一下异步链式调用的设计与实现。
考虑如下情况:
情况1:
访问网络(或其他耗时的事情)。通常的做法是:
1、显示一个ProgressDialog对话框,提示用户。
2、启动工作线程来执行耗时操作。
3、发送消息到关联到主线程的Handler里面,关闭对话框。
情况2:
从网络下载一个zip文件,下载完成之后,询问用户是否执行解压操作。通常的合理做法:
1、显示一个ProgressDialog对话框,提示用户。
2、启动线程执行下载操作。
3、发送消息到关联到主线程的Handler里面,关闭对话框,然后启动一个询问对话框。
4、用户如果点击[YES],显示一个ProgressDialog对话框。
5、启动用线程执行解压操作。
6、发送消息到关联到主线程的Handler里面,关闭对话框。
通常情况下,在Android我们有两种方式来使用线程,一是Thread + Handler这种标准方式,另外一种是使用AsyncTask类。
实现这两个情况的缺点:
1、定义Handler,发送消息,使得代码变得复杂,不易理解。
2、发送消息是异步处理,在某些情况下可能需要做等待操作。
3、流程执行混乱,不是流水作业。
基于以上情况,我们能不能也像流水线的操作那么调用我们的回调(Callback),使用者只关心第一步干什么,第二步干什么,如果能这样的话,那么在哪步做什么都能明确定义出来,这就是链式调用。
请看下面的链式调用的写法(JavaScript):
Async.go(initialArgument) .next(firstAsyncOperation) .next(secondAsyncOperation) .next(thirdAsyncOperation) .next(function(finalResult) { alert(finalResult); })
用户只需要添加每一步的task到一个队列里面,然后执行,这些task就会按添加的顺序执行,从而实现链式调用。
这种思想还不挺好的,在写代码的时候,我们更加关注实现的逻辑,不需要去考虑发什么消息等。只考虑第一步干什么,第二步干什么等。这样在以后代码维护时也比较好。
我们能不能设计出一个Android版本的异步链式调用的模块呢,请看下面。
Task
我们抽象出每一步要做的事情,定义一个Task类,它是一个抽象类,有如下核心属性和方法:
mRunInBackground
用来指示这个Task是运行在后台线程还是运行在主线程。
onExecuter(TaskOperation)
我们需要实现该方法,在这里面执行我们想要做的事情。
onProgressUpdate(Object)
我们可以重写该方法,来更新我们所做事情的进度,这个方法运行在主线程。
注意:在使用时,你必须指定这个Task是运行在UI线程还是后台线程。
TaskOperation
1)这个类里面包含了task的运行参数,上一个task的输出将会作为下一个task的输入。
2)它可以指示继续或暂停执行下一个task。
3)它里面使用了一个object[]来存储参数。
TaskManager
1)管理task队列,始终从队列第一个开始执行,执行一个task后,这个task将从队列出移除。
2)内部创建了一个带有消息循环的线程。
3)执行task时,判断其运行的线程环境,如果运行在UI线程,发送消息到UI的Handler来执行。
4)内部封装了Handler,用户不用关心是否发送消息。
5)核心方法有:
- next(Task)
- execute()
- execute(TaskOperation)
- cancelCurrentTask()
- removeTasks()
- publishProgress(Object)
这里只是给了一个最基本的设计思路,现在该设计还有完善的地方,具体的实现请参考相关的代码和测试工程。
实现代码
Task.java
/* * System: CoreLib * @version 1.00 * * Copyright (C) 2010, LiHong * */ package com.nj1s.lib.task; import java.util.concurrent.atomic.AtomicBoolean; /** * <p> * This method define the task used to do something. Typically you should override * {@link #onExecute(TaskOperation)} method to do you things, on the other hand, you * also can override the {@link #onProgressUpdate(Object)} method to get the progress of * you things. * </p> * * <p> * NOTE: * There is an very important thing you should pay attention to, you must specify the task * is running on background thread or UI thread, the default flag is true ---- running on * background thread. * </p> * * @author LeeHong * * @date 2012/10/30 */ public abstract class Task { /** * The id of the task, typically you need NOT set it, if will set automatically when you * add this task into {@link TaskManager} class. */ private int mId = 0; /** * The task name. */ private String mName = null; /** * Indicate this task is canceled or not. */ private AtomicBoolean mCancelled = new AtomicBoolean(false); /** * The task status, default value is {@link Status#PENDING}. */ private volatile Status mStatus = Status.PENDING; /** * The running status, default value is {@link RunningStatus#UI_THREAD}. */ private volatile RunningStatus mRunStatus = RunningStatus.UI_THREAD; /** * Indicates the current status of the task. Each status will be set only once * during the lifetime of a task. */ public enum Status { /** * Indicates that the task has not been executed yet. */ PENDING, /** * Indicates that the task is running. */ RUNNING, /** * Indicates that {@link Task#onExecute} has finished. */ FINISHED, } /** * Indicate the task running status. */ public enum RunningStatus { /** * Indicate the task is running in the background thread. */ WORK_THREAD, /** * Indicate the task is running in the UI thread. */ UI_THREAD, } /** * The constructor method. * * @param runInBackground * @param name */ public Task(Task task) { this.mRunStatus = task.mRunStatus; this.mName = task.mName; this.mStatus = task.mStatus; } /** * The constructor method. * * @param status indicate the task is running in background thread or not. */ public Task(RunningStatus status) { this(status, null); } /** * The constructor method. * * @param runInBackground * @param name */ public Task(RunningStatus status, String name) { mRunStatus = status; mName = name; } /** * Override this method to do you works. * * @param operation The operation is passed from previous task. * * @return Typically you should return the {@link #operation}. */ public abstract TaskOperation onExecute(TaskOperation operation); /** * Called when change the progress, this method is running in UI thread. * * @param progresses */ public void onProgressUpdate(Object progresses) { } /** * Cancel the task. */ public void cancel() { mCancelled.set(true); } /** * Indicate the task is canceled or not. * * @return */ public boolean isCancelled() { return mCancelled.get(); } /** * Get the running status. * * @return */ public RunningStatus getRunningStatus() { return mRunStatus; } /** * Set the name of the task. * * @param name The task name. */ public void setTaskName(String name) { mName = name; } /** * Get the task name. * * @return the task name. */ public String getTaskName() { return mName; } /** * Set the status of the task. * * @param status */ public void setStatus(Status status) { mStatus = status; } /** * Get the status of the task. * * @return */ public Status getStatus() { return mStatus; } /** * Set the id of the task. * * @param id */ public void setTaskId(int id) { mId = id; } /** * Get the task id. */ public int getTaskId() { return mId; } /** * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("name = ").append(mName).append(" "); sb.append("id = ").append(mId).append(" "); sb.append(super.toString()); return sb.toString(); } }
TaskOperation.java
/* * System: CoreLib * @version 1.00 * * Copyright (C) 2010, LiHong. */ package com.nj1s.lib.task; import java.util.ArrayList; import com.nj1s.lib.task.TaskManager.TaskManagerState; /** * The task operation, it wraps the task parameter, etc. * * @author LeeHong * * @date 2012/10/30 */ public class TaskOperation { /** * The task parameter. */ private Object[] mNextTaskParams = null; /** * The task manager status. */ private TaskManagerState mTaskManagerStatus = TaskManagerState.CONTINUE; /** * The constructor method. */ public TaskOperation() { } /** * The constructor method. * * @param nextTaskParams */ public TaskOperation(Object[] nextTaskParams) { mNextTaskParams = nextTaskParams; } /** * The constructor method. * * @param operation */ public TaskOperation(TaskOperation operation) { setTaskParams(operation); } /** * Get the task parameter. */ public Object[] getTaskParams() { return mNextTaskParams; } /** * Set the task parameter. * * @param params */ public void setTaskParams(Object[] params) { mNextTaskParams = params; } /** * Set the task parameters. * * @param operation */ public void setTaskParams(TaskOperation operation) { if (operation == this) { throw new IllegalArgumentException("The argument can NOT be self."); } if (null == operation) { return; } Object[] params = operation.getTaskParams(); if (null == params) { return; } ArrayList<Object> paramsList = new ArrayList<Object>(); if (null != mNextTaskParams) { for (Object param : mNextTaskParams) { paramsList.add(param); } } for (Object param : params) { paramsList.add(param); } mNextTaskParams = paramsList.toArray(); } /** * @param status the mTaskManagerStatus to set */ public void setTaskManagerStatus(TaskManagerState status) { mTaskManagerStatus = status; } /** * @return the mTaskManagerStatus */ public TaskManagerState getTaskManagerStatus() { return mTaskManagerStatus; } /** * Append the specified parameter to the end of the parameter list. * * @param param */ public void appendTaskParam(Object param) { appendTaskParams(new Object[] {param}); } /** * Append the specified parameter to the end of the parameter list. * * @param params */ public void appendTaskParams(Object[] params) { if (null != params) { TaskOperation operation = new TaskOperation(params); setTaskParams(operation); } } }
TaskManager.java
/* * System: CoreLib * @version 1.00 * * Copyright (C) 2010, LiHong. * */ package com.nj1s.lib.task; import java.util.HashMap; import java.util.LinkedList; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.text.TextUtils; import android.util.Log; import com.nj1s.lib.task.Task.RunningStatus; import com.nj1s.lib.task.Task.Status; import com.nj1s.lib.thread.ThreadWorker; /** * This class is used to manager the tasks so that you can add many tasks into the task manger * and these tasks will be running one by one. * * <h2>Example:</h2> * <pre class="prettyprint"> * private void showProgressDialog() * { * final ProgressDialog mProgressDialog = null; * final TaskManager taskManager = new TaskManager("ShowProgressDlg"); * * // Set the state change listener. * taskManager.setStateChangeListener(new IStateChangeListener() * { * public void onStateChanged(TaskManager taskManager, State oldState, State newState) * { * Toast.makeText(ShowProgressDlgActivity.this, " onStateChanged state = " + newState, Toast.LENGTH_SHORT).show(); * } * }); * * taskManager * .next(new Task(Task.RunningStatus.UI_THREAD) * { * public TaskOperation onExecute(TaskOperation operation) * { * mProgressDialog = new ProgressDialog(ShowProgressDlgActivity.this); * mProgressDialog.setTitle("Download"); * mProgressDialog.setMessage("Downlonding data from server..."); * mProgressDialog.setCancelable(false); * mProgressDialog.show(); * * return null; * } * }) * .next(new Task(Task.RunningStatus.WORK_THREAD) * { * public TaskOperation onExecute(TaskOperation operation) * { * // Simulate the work thread. * sleep(5000); * * return null; * } * }) * .next(new Task(Task.RunningStatus.UI_THREAD) * { * public TaskOperation onExecute(TaskOperation operation) * { * if (null != mProgressDialog && mProgressDialog.isShowing()) * { * mProgressDialog.dismiss(); * mProgressDialog = null; * } * * return null; * } * }) * .execute(); // Call this method to execute these tasks. * } * </pre> * * <h2>Note:</h2> * <pre> * The {@link Task} class must be specified the task running state, one of the enum {@link Task#RunningStatus}. * </pre> * * @author LeeHong * * @date 2012/10/30 * * @see {@link Task} * @see {@link TaskOperation} */ public class TaskManager { /** * Execute task message. */ private static final int MESSAGE_POST_EXECUTE = 0x01; /** * Update progress message. */ private static final int MESSAGE_POST_PROGRESS = 0x02; /** * The state change listener. */ public interface IStateChangeListener { /** * Called when the task manager's state is changed. This method will be called in * UI thread. * * @param taskManager Which task manager's state changed. * @param oldState The old state. * @param newState The new state. */ public void onStateChanged(TaskManager taskManager, State oldState, State newState); } /** * A representation of a task manager's state. A given thread may only be in one * state at a time. */ public enum State { /** * The task manager has been created, but has never been started. */ NEW, /** * Indicate the task manager is running one task. */ RUNNING, /** * Indicate the task manager is paused, typically call {@link #pause()} method. */ PAUSED, /** * All tasks are finished. */ FINISHED, } /** * The status of the {@link TaskManager} class. */ public enum TaskManagerState { /** * Continue the task manager to run next task. */ CONTINUE, /** * Indicate the task manager pause to run next task. */ PAUSE, } /** * The running task manager collection. */ private static HashMap<String, TaskManager> s_taskManagers = new HashMap<String, TaskManager>(); /** * The task list. */ private LinkedList<Task> mTaskList = new LinkedList<Task>(); /** * The task operation, it will pass from first task to the last task. */ private TaskOperation mTaskOperation = new TaskOperation(); /** * The running thread worker, it own a looper which will be alive until you call * {@link ThreadWorker#quit()} method. */ private ThreadWorker mThreadWorker = null; /** * The current perform task, may be null. */ private Task mCurTask = null; /** * The state of the task manager. */ private State mState = State.NEW; /** * The name of the task manager. */ private String mName = null; /** * The listener. */ private IStateChangeListener mListener = null; /** * The background thread handler, which is associated to a background thread looper. */ private Handler mThreadHandler = null; /** * The UI thread handler. */ private Handler mUIHandler = new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_POST_EXECUTE: Task task = (Task)msg.obj; executeTask(task); // Try to run next task if possible. runNextTask(); break; case MESSAGE_POST_PROGRESS: postProgress(msg.obj); break; } } }; /** * The constructor method. */ public TaskManager() { } /** * The constructor method. * * @param name The name of the task manager. */ public TaskManager(String name) { mName = name; } /** * Add the task to {@link TaskManager} class. * * @param task The task. * * @return the {@link TaskManager} object. */ public TaskManager next(Task task) { if (null != task) { synchronized (mTaskList) { int id = mTaskList.size() + 1; task.setTaskId(id); mTaskList.add(task); } } else { throw new NullPointerException("task is null"); } return this; } /** * Start to execute the tasks in the task manager. */ public void execute() { if (mTaskList.size() > 0) { startThread(); // Set the task to RUNNING. setState(State.RUNNING); // Perform the runnable in the handler which is associated to the background thread. mThreadHandler.post(new Runnable() { @Override public void run() { doInBackground(); } }); } else { quitLooper(); } } /** * Start to execute the tasks in the task manager with the specified parameter. * * @param operation The task operation contains the task parameter. */ public void execute(TaskOperation operation) { if (null != operation) { mTaskOperation = operation; } execute(); } /** * Post execute a task which will be running in UI thread. * * @param task the task to be running. */ public void postExecute(Task task) { if (null == task) { throw new NullPointerException("Task can NOT be null."); } final Task runTask = task; // If the task running status is UI_THREAD. if (RunningStatus.UI_THREAD == runTask.getRunningStatus()) { // The task is running in UI thread. mUIHandler.post(new Runnable() { @Override public void run() { executeTask(runTask); } }); } } /** * Publish the task progress, if you call this method, the {@link Task#onProgressUpdate(Object)} * method will be called, which is running in the UI thread. * * @param progresses The progress. */ public void publishProgress(Object progresses) { mUIHandler.obtainMessage(MESSAGE_POST_PROGRESS, progresses).sendToTarget(); } /** * Cancel the current running task. */ public void cancelCurrentTask() { if (null != mCurTask) { mCurTask.cancel(); } } /** * Remove the tasks in the list. */ public void removeTasks() { synchronized (mTaskList) { if (mTaskList.size() > 0) { mTaskList.clear(); quitLooper(); } } } /** * Remove the specified task. * * @param task The task to be removed. */ public void removeTask(Task task) { synchronized (mTaskList) { mTaskList.remove(task); if (mTaskList.isEmpty()) { quitLooper(); } } } /** * Set the state change listener. * * @param listener */ public void setStateChangeListener(IStateChangeListener listener) { mListener = listener; } /** * Get the task operation. * * @return */ public TaskOperation getTaskOperation() { return mTaskOperation; } /** * @return the mName */ public String getName() { return mName; } /** * Pause the worker thread. */ public void pause() { if (null != mThreadWorker) { setState(State.PAUSED); mThreadWorker.pause(); } } /** * Resume the worker thread from the waiting status. */ public void resume() { if (null != mThreadWorker) { setState(State.RUNNING); mThreadWorker.restart(); } } /** * Quit the looper so that the thread can finish correctly. */ public void quitLooper() { if (null != mThreadWorker) { mThreadWorker.quit(); mThreadWorker = null; } mThreadHandler = null; // Set the task to FINISHED. setState(State.FINISHED); } /** * Blocks the current thread ({@link Thread#currentThread()}) until the receiver finishes its execution and dies. */ public final void join() { if (null != mThreadWorker) { mThreadWorker.join(); } } /** * Get the task manager state. * * @return */ public State getState() { return mState; } /** * Get the running task manager. * * @return HashMap<String, TaskManager>, the task manager's name is the key, and the * task manager object is the value. */ public static HashMap<String, TaskManager> getTaskManagers() { return s_taskManagers; } /** * @see java.lang.Object#toString() */ @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Name = ").append(mName).append(" "); sb.append("State = ").append(mState).append(" "); sb.append(super.toString()); return sb.toString(); } /** * print task execute status * * @param task */ protected void printExecuteTaskState(Task task) { Log.d("TaskManager", " Executer the task: " + task.toString()); } /** * Set the state. * * @param state */ private void setState(State state) { final State oldState = mState; final State newState = state; mState = state; if (mState == State.FINISHED) { popTaskManager(this); } else { pushTaskManager(this); } if (oldState != newState) { printTaskManagerState(oldState, newState); performStateChange(oldState, newState); } } /** * Call this method to start the work thread if can. */ private void startThread() { if (null == mThreadWorker) { String name = TextUtils.isEmpty(mName) ? this.toString() : mName; String threadName = "TaskManager_Thread_" + name; mThreadWorker = new ThreadWorker(threadName); mThreadHandler = new Handler(mThreadWorker.getLooper()); } } /** * This method is running in the background thread. */ private void doInBackground() { mCurTask = null; if (mTaskList.isEmpty()) { return; } Task task = mTaskList.get(0); mCurTask = task; // Remove the first item in the list. synchronized (mTaskList) { mTaskList.remove(0); } // If the task is allowed to be running in background thread, we execute the task // now, the doInBackground() method is running in the background thread. switch (task.getRunningStatus()) { case WORK_THREAD: executeTask(task); // Try to run next task if possible. runNextTask(); break; case UI_THREAD: // Send a message to the UI handler to executer the task. mUIHandler.obtainMessage(MESSAGE_POST_EXECUTE, task).sendToTarget(); break; } } /** * Run the next task. */ private void runNextTask() { // If run next, call the execute() method again. if (isRunNext()) { execute(); } } /** * Check whether run the next task if has one. * * @return true if run next task, otherwise false. */ private boolean isRunNext() { boolean isRunNext = true; boolean hasNext = false; if (null != mTaskOperation) { isRunNext = (mTaskOperation.getTaskManagerStatus() == TaskManagerState.CONTINUE); } if (null != mTaskList) { hasNext = mTaskList.size() > 0; } // No next task, quit the thread. if (!hasNext) { quitLooper(); } return (isRunNext && hasNext); } /** * Execute the task, if will call {@link Task#onExecute(TaskOperation)} method. * * @param task The task object. */ private void executeTask(Task task) { if (null != task) { // Set the status of the task. task.setStatus(Status.RUNNING); // Print the task state. this.printExecuteTaskState(task); try { // Avoid the exception from task interrupting the task manager works. mTaskOperation = task.onExecute(mTaskOperation); } catch (Exception e) { e.printStackTrace(); } // Set the status of the task. task.setStatus(Status.FINISHED); // Print the task state. this.printExecuteTaskState(task); } } /** * Post the progress, it will call {@link Task#onProgressUpdate(Object progresses)} method. * * @param progresses */ private void postProgress(Object progresses) { if (null != mCurTask) { mCurTask.onProgressUpdate(progresses); } } /** * Perform the state change. * * @param oldState * @param newState */ private void performStateChange(final State oldState, final State newState) { if (null != mListener) { mUIHandler.post(new Runnable() { @Override public void run() { mListener.onStateChanged(TaskManager.this, oldState, newState); } }); } } /** * Print the task manager information. * * @param oldState * @param newState */ private void printTaskManagerState(final State oldState, final State newState) { Log.d("TaskManager", "TaskManager state changed, task manager = " + this.toString()); } /** * Push the task manager to the list. * * @param taskManager */ private static void pushTaskManager(TaskManager taskManager) { if (null != taskManager) { String name = taskManager.getName(); if (!TextUtils.isEmpty(name)) { s_taskManagers.put(name, taskManager); } } } /** * Pop the task manager from the list. * @param taskManager */ private static void popTaskManager(TaskManager taskManager) { if (null != taskManager) { String name = taskManager.getName(); s_taskManagers.remove(name); } } }