Android线程: AsyncTask全解析(上) AsyncTask的使用

AsyncTask是一种轻量级的异步任务类,它可以在线程池中执行后台任务,然后把执行的进度和最终的结果传递给主线程并在主线程中更新UI。

从实现上来说,AysncTask封装了Thread和Handler,通过AsyncTask可以更加方便地执行后台任务以及主线程中访问UI,但是AsyncTask并不适合进行特别耗时的后台操作,对于特别耗时的任务来说,建议使用线程池。

(一) 概念性知识

AsyncTask是一个抽象的泛型类,所以如果我们想使用它,就必须要创建一个子类去继承它。在继承时我们可以为AsyncTask类指定三个泛型参数,这三个参数的用途如下:

1.Params
在执行AsyncTask时需要传入的参数,可用于在后台任务中使用

2.Progress
后台任务执行时,如果需要在界面上显示当前的进度,则使用这里指定的泛型作为进度单位。

3.Result
当任务执行完毕后,如果需要对结果进行返回,则使用这里指定的泛型作为返回值类型。

因此,一个最简单的自定义AsyncTask就可以写成如下方式:

代码片1:
    class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
     
    	……
    }

这里我们把AsyncTask的第一个泛型参数指定为Void,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位。第三个泛型参数指定为Boolean,则表示使用布尔型数据来反馈执行结果。

当然,目前我们自定义的DownloadTask还是一个空任务,并不能进行任何实际的操作,我们还需要去重写AsyncTask中的几个方法才能完成对任务的定制。经常需要去重写的方法有以下四个:

1.onPreExecute()

这个方法在主线程中执行,在异步任务执行之前,此方法会被调用。此方法一般用于进行一些界面上的初始化操作,比如显示一个进度条对话框等。

2.doInBackground(Params…params)

这个方法在线程池中执行,此方法中的所有代码都会在子线程中运行,我们应该在这里去处理所有的耗时任务。任务一旦完成就可以通过return语句来将任务的执行结果进行返回,如果AsyncTask的第三个泛型参数指定的是Void,就可以不返回任务执行结果。注意,在这个方法中是不可以进行UI操作的,如果需要更新UI元素,比如说反馈当前任务的执行进度,可以调用publishProgress(Progress…)方法来完成

另外,此方法需要返回计算结果给onPostExecute。

3.onProgressUpdate(Progress…)

此方法在主线程中执行。当在后台任务中调用了publishProgress(Progress…)方法后,这个方法就很快会被调用,方法中携带的参数就是在后台任务中传递过来的。在这个方法中可以对UI进行操作,利用参数中的数值就可以对界面元素进行相应的更新

4.onPostExecute(Result)

此方法在主线程中执行。当后台任务执行完毕并通过return语句进行返回时,这个方法就很快会被调用。返回的数据会作为参数传递到此方法中,可以利用返回的数据来进行一些UI操作,比如说提醒任务执行的结果,以及关闭掉进度条对话框等。

上述这几个方法,onPreExecute先执行,接着是doInBackgroud,最后才是onPostExecute。除了上述四个方法以外,AsyncTask还提供了onCancelled()方法,它同样在主线程中执行,当异步任务被取消时,onCancelled()方法会被调用,这个使用onPostExecute则不会被调用。

一个比较完整的自定义AsyncTask就可以写成如下方式:

代码片2:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> {
     

        @Override
        protected void onPreExecute() {
     
            progressDialog.show();
        }

        @Override
        protected Boolean doInBackground(Void... params) {
     
            try {
     
                while (true) {
     
                    int downloadPercent = doDownload();
                    publishProgress(downloadPercent);
                    if (downloadPercent >= 100) {
     
                        break;
                    }
                }
            } catch (Exception e) {
     
                return false;
            }
            return true;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
     
            progressDialog.setMessage("当前下载进度:" + values[0] + "%");
        }

        @Override
        protected void onPostExecute(Boolean result) {
     
            progressDialog.dismiss();
            if (result) {
     
                Toast.makeText(context, "下载成功", Toast.LENGTH_SHORT).show();
            } else {
     
                Toast.makeText(context, "下载失败", Toast.LENGTH_SHORT).show();
            }
        }
    }

这里我们模拟了一个下载任务,在doInBackground()方法中去执行具体的下载逻辑,在onProgressUpdate()方法中显示当前的下载进度,在onPostExecute()方法中来提示任务的执行结果。如果想要启动这个任务,只需要简单地调用以下代码即可:

代码片3:

//0到多个参数不等
new DownloadTask().execute();

以上就是AsyncTask的基本用法,怎么样,是不是感觉在子线程和UI线程之间进行切换变得灵活了很多?我们并不需求去考虑什么异步消息处理机制,也不需要专门使用一个Handler来发送和接收消息,只需要调用一下publishProgress()方法就可以轻松地从子线程切换到UI线程了。

说明(重点)

1.代码片3中execute() 是否传参数,以及传参数的值,影响代码片2第10行代码 doInBackground(Void… params)中params数组的值.

2.代码片2第11行代码 doInBackground(Void… params)的返回值,影响onPostExecute() 参数值及参数值类型.

3.而onProgressUpdate(Integer… values)的参数值及参数类型,是由doInBackground方法中调用AsyncTask的publishProgress方法确定参数值及参数类型;

上述的三点,是AsyncTask使用过程中最核心的部分(在第二部分实战案例中代码部分由注释).

为了便于更好的理解,这里通过一个真实的demo进行详细演示整个使用过程。

(二) 实战案例:

提供一个可以下载/继续、暂停下载、取消下载功能的demo.
界面如下:
Android线程: AsyncTask全解析(上) AsyncTask的使用_第1张图片
这里重点介绍与 AsyncTask相关的代码,完整代码见源码:

先设置下载过程中的回调监听

代码片4
public interface DownloadListener {
     

    void onProgress(int progress);

    void onSuccess();

    void onFailed();

    void onPaused();

    void onCanceled();

}

MainActivity.class中代码如下:

代码片5
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
     

    String downFileUrl = "https://raw.githubusercontent" +
            ".com/guolindev/eclipse/master/eclipse-inst-win64" +
            ".exe";
    
    private Context      mContext;
    private DownloadTask downloadTask;
    private ProgressBar  mProgressBar;
    private TextView     tv, status;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
     
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
        mProgressBar = findViewById(R.id.progressbar);
        tv = findViewById(R.id.tv);
        status = findViewById(R.id.status);

        findViewById(R.id.start_download).setOnClickListener(this);
        findViewById(R.id.pause_download).setOnClickListener(this);
        findViewById(R.id.cancel_download).setOnClickListener(this);
    }

    private DownloadListener listener = new DownloadListener() {
     
        @Override
        public void onProgress(int progress) {
     
            mProgressBar.setProgress(progress);
            tv.setText(progress + "   %");
            status.setText("当前状态:正在下载");
        }

        @Override
        public void onSuccess() {
     
            downloadTask = null;
            mProgressBar.setVisibility(View.GONE);
            Toast.makeText(mContext, "下载成功", Toast.LENGTH_SHORT).show();
            status.setText("当前状态:下载完成");
        }

        @Override
        public void onFailed() {
     
            downloadTask = null;
            mProgressBar.setVisibility(View.GONE);
            Toast.makeText(mContext, "下载失败", Toast.LENGTH_SHORT).show();
            status.setText("当前状态:下载失败");
        }

        @Override
        public void onPaused() {
     
            downloadTask = null;
            Toast.makeText(mContext, "暂停", Toast.LENGTH_SHORT).show();
            status.setText("当前状态:暂停下载");
        }

        @Override
        public void onCanceled() {
     
            downloadTask = null;
            tv.setText(0 + "   %");
            status.setText("当前状态:取消下载");
            mProgressBar.setVisibility(View.GONE);
            Toast.makeText(mContext, "取消", Toast.LENGTH_SHORT).show();
            String fileName = downFileUrl.substring(downFileUrl.lastIndexOf("/"));
            String directory =
                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            File file = new File(directory + fileName);
            if (file.exists()) {
     
                file.delete();
            }
        }
    };

    @Override
    public void onClick(View v) {
     
        switch (v.getId()) {
     
            case R.id.start_download:
                if (downloadTask == null) {
     
                    downloadTask = new DownloadTask(listener);
                    downloadTask.execute(downFileUrl);
                }
                break;
            case R.id.pause_download:
                if (downloadTask != null) {
     
                    downloadTask.pauseDownload();
                }
                break;
            case R.id.cancel_download:
                if (downloadTask != null) {
     
                    downloadTask.cancelDownload();
                }
                break;
            default:
                break;
        }
    }
}

MainActivity.class中代码都属于比较常规的代码,对控件进行实例化、三个按钮的点击事件,还有就是回调之后的操作。

第80-83行代码需要说明一下:

代码片6
 if (downloadTask == null) {
     
     downloadTask = new DownloadTask(listener);
     downloadTask.execute(downFileUrl);
 }

实例化AsyncTask, 调用execute()方法开始下载,并将下载文件的URL地址传入到execute()方法中。

重点看DownloadTask.class类:

代码片7
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
     

    public static final int TYPE_SUCCESS  = 0;
    public static final int TYPE_FAILED   = 1;
    public static final int TYPE_PAUSED   = 2;
    public static final int TYPE_CANCELED = 3;

    private DownloadListener listener;

    private boolean isCanceled = false;

    private boolean isPaused = false;

    private int lastProgress;

    public DownloadTask(DownloadListener listener) {
     
        this.listener = listener;
    }

    //doInBackground方法返回值确定 第106行代码中 onPostExecute(Integer status) 方法的参数类型.
    //doInBackground(String... params)方法的参数,由代码片5第82行代码downloadTask.execute(downFileUrl)决定.如果传入1个参数,String... params 就只有一个有值;如果传入2个参数,就params[0]、params[1]都对应有值.当前的demo只传了一个值downFileUrl,即第30行代码的params[0]其实就是为downFileUrl.
    @Override
    protected Integer doInBackground(String... params) {
     
        InputStream is = null;
        RandomAccessFile savedFile = null;
        File file = null;
        try {
     
            long downloadedLength = 0; // 记录已下载的文件长度
            String downloadUrl = params[0];
            String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
            String directory =
                    Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
            file = new File(directory + fileName);
            if (file.exists()) {
     
                downloadedLength = file.length();
            }
            long contentLength = getContentLength(downloadUrl);
            if (contentLength == 0) {
     
                return TYPE_FAILED;
            } else if (contentLength == downloadedLength) {
     
                // 已下载字节和文件总字节相等,说明已经下载完成了
                return TYPE_SUCCESS;
            }
            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder()
                    // 断点下载,指定从哪个字节开始下载
                    .addHeader("RANGE", "bytes=" + downloadedLength + "-")
                    .url(downloadUrl)
                    .build();
            Response response = client.newCall(request).execute();
            if (response != null) {
     
                is = response.body().byteStream();
                savedFile = new RandomAccessFile(file, "rw");
                savedFile.seek(downloadedLength); // 跳过已下载的字节
                byte[] b = new byte[1024];
                int total = 0;
                int len;
                while ((len = is.read(b)) != -1) {
     
                    if (isCanceled) {
     
                        return TYPE_CANCELED;
                    } else if (isPaused) {
     
                        return TYPE_PAUSED;
                    } else {
     
                        total += len;
                        savedFile.write(b, 0, len);
                        // 计算已下载的百分比
                        int progress = (int) ((total + downloadedLength) * 100 / contentLength);
                        //下面的方法决定第97行代码onProgressUpdate(Integer... values)中的参数类型、个数及对应的值
                        publishProgress(progress);
                    }
                }
                response.body().close();
                return TYPE_SUCCESS;
            }
        } catch (Exception e) {
     
            e.printStackTrace();
        } finally {
     
            try {
     
                if (is != null) {
     
                    is.close();
                }
                if (savedFile != null) {
     
                    savedFile.close();
                }
                if (isCanceled && file != null) {
     
                    file.delete();
                }
            } catch (Exception e) {
     
                e.printStackTrace();
            }
        }
        return TYPE_FAILED;
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
     
        int progress = values[0];
        if (progress > lastProgress) {
     
            listener.onProgress(progress);
            lastProgress = progress;
        }
    }

    @Override
    protected void onPostExecute(Integer status) {
     
        switch (status) {
     
            case TYPE_SUCCESS:
                listener.onSuccess();
                break;
            case TYPE_FAILED:
                listener.onFailed();
                break;
            case TYPE_PAUSED:
                listener.onPaused();
                break;
            case TYPE_CANCELED:
                listener.onCanceled();
            default:
                break;
        }
    }

    public void pauseDownload() {
     
        isPaused = true;
    }


    public void cancelDownload() {
     
        isCanceled = true;
    }

    private long getContentLength(String downloadUrl) throws IOException {
     
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(downloadUrl)
                .build();
        Response response = client.newCall(request).execute();
        if (response != null && response.isSuccessful()) {
     
            long contentLength = response.body().contentLength();
            response.close();
            return contentLength;
        }
        return 0;
    }
}

这段代码就比较长了,我们需要一步步地进行分析。

首先看一下AsyncTask中的3个泛型参数:第一个泛型参数指定为String,表示在执行AsyncTask的时候需要传入一个字符串参数给后台任务(下载文件的URL);第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位;第三个泛型参数指定为Integer,则表示使用整型数据来反馈执行结果(成功、失败、暂停、取消)。

接下来我们定义了4个整型常量用于表示下载的状态,TYPE_ SUCCESS 表示下载成功,TYPE_ FAILED 表示下载失败,TYPE_ PAUSED 表示暂停下载, TYPE_ CANCELED 表示取消下载。然后在DownloadTask的构造函数中要求传入一个刚刚定义的DownloadListener参数,我们待会就会将下载的状态通过这个参数进行回调。

接着就是要重写doInBackground()、onProgressUpdate( )和onPostExecute()这3个方法了,前面已经学习过这3个方法各自的作用,因此在这里它们各自所负责的任务也是明确的: doInBackground()方法用于在后台执行具体的下载逻辑,onProgressUpdate()方法用于在界面上更新当前的下载进度,onPostExecute()用于通知最终的下载结果。

那么先来看一下doInBackground()方法,首先我们从参数中获取到了下载的URL地址,并根据URL地址解析出了下载的文件名,然后指定将文件下载到Environment.DIRECTORY_DOWNLOADS目录下,也就是SD卡的Download目录。我们还要判断一下Download 目录中是不是已经存在要下载的文件了,如果已经存在的话则读取已下载的字节数,这样就可以在后面启用断点续传的功能。接下来先是调用了getContentLength()方法来获取待下载文件的总长度,如果文件长度等于0则说明文件有问题,直接返回TYPE_ FAILED,如果文件长度等于已下载文件长度,那么就说明文件已经下载完了,直接返回TYPE_ SUCCESS 即可。紧接着使用OKHttp来发送一条网络请求,需要注意的是,这里在请求中添加了一个header,用于告诉服务器我们想要从哪个字节开始下载,因为已下载过的部分就不需要再重新下载了。接下来读取服务器响应的数据,并使用Java 的文件流方式,不断从网络上读取数据,不断写入到本地,一直到文件全部下载完成为止。在这个过程中,我们还要判断用户有没有触发暂停或者取消的操作,如果有的话则返回TYPE_ PAUSED 或TYPE_ CANCELED 来中断下载,如果没有的话则实时计算当前的下载进度,然后调用publishProgress()方法进行通知。暂停和取消操作都是使用一个布尔型的变量来进行控制的,调用pauseDownload( )或cancelDownload()方法即可更改变量的值。

接下来看一下onProgressUpdate()方法,这个方法就简单得多了,它首先从参数中获取到当前的下载进度,然后和上一次的下载进度进行对比,如果有变化的话则调用DownloadListener的onProgress()方法来通知下载进度更新。

最后是onPostExecute()方法,也非常简单,就是根据参数中传入的下载状态来进行回调。下载成功就调用DownloadListener的onSuccess()方法,下载失败就调用onFailed()方法,暂停下载就调用onPaused()方法,取消下载就调用onCanceled()方法。

(三) 使用限制:

(1)AsyncTask的类必须在主线程中加载,这也意味着第一次访问AsyncTask必须发生在主线程,当然这个过程在Android4.1及以上版本中已经被系统自动完成。

(2)AsyncTask的对象必须在主线程中创建。

(3) execute方法必须在UI线程调用。

(4)不要在程序中直接调用onPreExecute() 、onPostExecute、doInBackgroud和onProgressUpdate方法。

(5)一个AsyncTask对象只能执行一次,即只能调用一次execute方法,否则会报运行时异常。

(6)在Android1.6之前,AsyncTask是串行执行任务的,Android 1.6的时候AsyncTask开始采用线程池里处理并行任务,但是从Android3.0开始,为了避免AsyncTask所带来的并发错误,AsyncTask又采用一个线程来串行执行任务。当然,在Android3.0以后,我们仍然可以通过AsyncTask的executeOnExecute方法来并行执行任务。

关于AsyncTask的使用到此结束,在下一篇,我们将对AsyncTask的源码进行解析。

你可能感兴趣的:(Android进程框架)