Android:异步加载——AsyncTask详谈

/ 0评 / 0

写在前面

昨天在看Android中的缓存机制,顺带把异步加载任务复习了一遍,尤其是AsyncTask。由于我在一般情况下使用较多的就是Handler方式执行异步任务,很少用到AsyncTask,所以现在总结一下加深自己的印象,并分享出来与大家交流。后几天会发一些关于Android中的缓存机制。 在本文书写过程中,我借鉴的资料有《第一行代码》、《Android开发艺术探索》、慕课网的一期课程以及相关的博文,链接在文末注明。下面是本文的目录,比较简单。

异步消息处理机制

异步消息处理机制是Android开发中的基础知识。为了方便后面的AsyncTask的理解,我就先简单说一下这个机制,但不作为本文的重点。

组成部分

Android中的异步消息主要是由四个部分组成:

下面我们依次来看:

1、Message

Message就是异步消息中需要传递的消息。它会在内部携带一些少量的信息,用于不同线程之间交换数据。它本身有四个字段,用来处理不同类型的信息:

2、MessageQueue

知道了Message就很好理解这个了,这是一个消息队列。队列中存放着大量的Message,这些Message都是没有被处理的,所以放在队列中等待处理。值得注意的是,每个线程中有且仅有一个MessageQueue对象。

3、Handler

这个我们很熟悉,不好翻译,一般用来处理消息。我们就叫它消息处理者吧。它是处理机制中一个比较重要的角色,是用来发送和处理消息的。主要是两个方法:

也就是说经过Handler发送的消息最终会传递到handleMessage中,进而被处理。

4、Looper

Looper不是很好理解。《第一行代码》中将其看作MessageQueue的管家,比较抽象。而《Android开发艺术探索》中将其定义成消息循环者,这个比较符合逻辑。也就是说,当线程中的Looper调用自身的loop()方法后,它会进入到一个无限循环中,循环监测MessageQueue中是否存在消息,如有存在一条未处理的消息,Looper就会将其取出,传递给Handler的handleMessage方法,让其处理。

这里我们需要注意两点:

基本流程

明白了上述这些基本定义后,我们来看一下异步消息处理机制的大体流程。二话不说,先上图。

异步消息处理流程图

从图中我们可以看出,大体流程如下:

  1. 首先我们需要在主线程中创建一个Handler对象,并且需要重写handleMessage方法;
  2. 其次在子线程中需要与主线程进行通信的时候,就通过Message对象携带一些信息,并通过Handler对象发送出去。
  3. 发送出去后该消息会进入MessageQueue等待处理。
  4. 这个时候Looper就起作用了,它一直循环循环终于监测到队列中有一条待处理的消息,立即取出并分发到handleMessage方法,这样主线程就收到这条消息并开始处理。

关于这个机制我就简单介绍到这,如果读者对这个机制又不懂的地方可以Google相关资料,也有大量优秀博文。文末也有相关博文链接,不再赘述。

初识AsyncTask

基本介绍

AsyncTask是一种轻量级的异步任务类,它与Handler一样,可以在后台执行任务。并可以把执行任务的过程及结果返回给主线程。这样主线程就可以做一些更新UI的操作。

它的本质是一个抽象的泛型类,所以我们在使用的时候需要继承这个AsyncTask类:

public abstract class AsyncTask<Params,Progress,Result>

可以看到,类提供了三个参数:

同时,它有四个核心方法,我们依次来看:

  1. onPreExecute():该方法在主线程中执行,通常用户完成一些准备和初始化的操作;
  2. doInBackground(Params... params):在子线程中执行,执行异步任务的关键方法,必须重写。此方法中可以调用publishProgress方法来更新任务进度,而publishProgress会调用onProgressUpdate方法来进行进度更新。注意此方法需要返回值给onPostExecute方法;
  3. onProgressUpdate(Progress...values):在主线程中执行,用于任务进度的更新;
  4. onPostExecute(Result result):在主线程中执行,该方法的参数是doInBackground的返回值。

当然还有其他方法,但是不怎么常用,这里不再介绍(其实我也不了解)。

当你自定义一个CustomTask继承AsyncTask复写这几个方法后,就需要开启任务,开启方式为:

new CustomTask().execute();

当然在这里我没有写参数,在实际开发中要注意填入相应的参数。

执行顺序

当我们自定义Task并复写这四个核心方法后,来看看这几个方法的执行顺序。测试代码如下:

/**
 * 自定义异步任务类
 */
class CustomTask extends AsyncTask<Void, Void, Void> {

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        Log.w("Task", " ----> onPreExecute");
    }

    @Override
    protected Void doInBackground(Void... params) {
        publishProgress();
        Log.w("Task", " ----> doInBackground");
        return null;
    }

    @Override
    protected void onProgressUpdate(Void... values) {
        super.onProgressUpdate(values);
        Log.w("Task", " ----> onProgressUpdate");
    }

    @Override
    protected void onPostExecute(Void aVoid) {
        super.onPostExecute(aVoid);
        Log.w("Task", " ----> onPostExecute");
    }
}

来看Log日志:

执行顺序Log日志

可以看出执行顺序依次是:

onPreExecute -> doInBackground -> onProgressUpdate -> onPostExecute

注意事项

基本了解了AsyncTask后,我们需要知道一些注意事项和使用过程中的一些条件限制。

AsyncTask的使用

好了,看到这里相信你已经对AsyncTask有了一个基本的认识了,现在我们就通过两个实例在看AsyncTask在项目中的具体使用。

加载网络图片

首先来看第一个Demo,通过图片的URL地址加载网络图片。已经是网络图片了,当然是耗时任务,所以我们需要开启异步任务。当然可以开启子线程来获取,但是这里我们采取AsyncTask这样的方式。完成效果如下:

加载网络图片

来看代码,代码比较简单,注释也写的很清晰,就不再解释了:

/**
 * 异步任务加载网络图片
 */
public class NetPicActivity extends AppCompatActivity {

    private ImageView netPicImage;
    private ProgressBar netPicProgressBar;
    private static String picUrl = "http://www.iamxiarui.com/wp-content/uploads/2016/05/壁纸.jpg";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_net_pic);
        netPicImage = (ImageView) findViewById(R.id.iv_netpic);
        netPicProgressBar = (ProgressBar) findViewById(R.id.pb_netpic);
        //通过调用execute方法开始处理异步任务.相当于线程中的start方法.
        new NetAsyncTask().execute(picUrl);
    }

    /**
     * 自定义网络请求异步任务
     */
    class NetAsyncTask extends AsyncTask<String, Void, Bitmap> {

        /**
         * onPreExecute用于异步处理前的操作
         */
        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //此处将progressBar设置为可见.
            netPicProgressBar.setVisibility(View.VISIBLE);
        }

        /**
         * 在doInBackground方法中进行异步任务的处理
         *
         * @param params 参数为URL
         * @return Bitmap对象
         */
        @Override
        protected Bitmap doInBackground(String... params) {
            //获取传进来的参数
            String url = params[0];
            Bitmap bitmap = null;
            URLConnection connection;
            InputStream is;
            try {
                connection = new URL(url).openConnection();
                is = connection.getInputStream();
                //为了更清楚的看到加载图片的等待操作,将线程休眠3秒钟
                Thread.sleep(3000);
                BufferedInputStream bis = new BufferedInputStream(is);
                //通过decodeStream方法解析输入流
                bitmap = BitmapFactory.decodeStream(bis);
                is.close();
                bis.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return bitmap;
        }

        /**
         * onPostExecute用于UI的更新.此方法的参数为doInBackground方法返回的值
         *
         * @param bitmap 网络图片
         */
        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            //隐藏progressBar
            netPicProgressBar.setVisibility(View.GONE);
            //更新imageView
            netPicImage.setImageBitmap(bitmap);
        }
    }
}

对了,一定要记得添加网络权限,不然没有效果。

<uses-permission android:name="android.permission.INTERNET"/>

模拟进度条

加载网络图片的时候,我们没有用到onProgressUpdate方法,现在就模拟一个进度条加载的Demo,效果如下:

模拟进度条

来看代码,这里我们模拟了进度条的加载,并在doInBackground中调用了publishProgress方法,让进度条更新,具体代码如下:

/**
 * 用异步任务模拟进度条的更新
 */
public class ProgressActivity extends AppCompatActivity {
    private ProgressBar mainProgressBar;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_progress);

        mainProgressBar = (ProgressBar) findViewById(R.id.pb_main);

        //开启异步任务
        new PbAsyncTask().execute();
    }

    /**
     * 自定义异步任务类
     */
    class PbAsyncTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            //使用for循环来模拟进度条的进度.
            for (int i = 0; i < 100; i++) {
                //调用publishProgress方法将自动触发onProgressUpdate方法来进行进度条的更新.
                publishProgress(i);
                try {
                    //通过线程休眠模拟耗时操作
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            //通过publishProgress方法传过来的值进行进度条的更新.
            mainProgressBar.setProgress(values[0]);
        }
    }
}

注意,这里我们来看下面的图,我们可以发现当第一次进度条没有全部完成时,返回重新点击开启进度条。此时进度条并没有开始运行,而是等待一段时间后,才开始更新进度。

进度条异常

为什么呢?因为AsyncTask是基于线程池进行实现的,当一个线程没有结束时,后面的线程是不能执行的。也就是说必须等到第一个task的for循环结束后,才能执行第二个task。

那么如何解决呢?我们知道,当我们点击BACK键时会调用Activity的onPause()方法,所以我们可以在Activity的onPause()方法中将正在执行的task标记为cancel状态,在doInBackground方法中进行异步处理时判断是否是cancel状态来决定是否取消之前的task。

更改后的代码如下:

/**
 * 用异步任务模拟进度条的更新
 */
public class ProgressActivity extends AppCompatActivity {
    private ProgressBar mainProgressBar;
    private PbAsyncTask pbAsyncTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_progress);

        mainProgressBar = (ProgressBar) findViewById(R.id.pb_main);

        //开启异步任务
        pbAsyncTask = new PbAsyncTask();
        pbAsyncTask.execute();
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (pbAsyncTask != null && pbAsyncTask.getStatus() == AsyncTask.Status.RUNNING) {
            //cancel方法只是将对应的AsyncTask标记为cancelt状态,并不是真正的取消线程的执行.
            pbAsyncTask.cancel(true);
        }
    }

    /**
     * 自定义异步任务类
     */
    class PbAsyncTask extends AsyncTask<Void, Integer, Void> {

        @Override
        protected Void doInBackground(Void... params) {
            //使用for循环来模拟进度条的进度.
            for (int i = 0; i < 100; i++) {
                //如果task是cancel状态,则终止for循环,以进行下个task的执行.
                if (isCancelled()) {
                    break;
                }
                //调用publishProgress方法将自动触发onProgressUpdate方法来进行进度条的更新.
                publishProgress(i);
                try {
                    //通过线程休眠模拟耗时操作
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Integer... values) {
            super.onProgressUpdate(values);
            //通过publishProgress方法传过来的值进行进度条的更新.
            mainProgressBar.setProgress(values[0]);
        }
    }
}

效果如下:

进度条异常解决

已经完美解决了这个问题,在这里我们要注意,cancel方法只是将对应的AsyncTask标记为cancel状态,并不是真正的取消线程的执行,想要真正取消线程,还是需要在doInBackground方法中停止。

AsyncTask和Handler两种异步方式区别

已经看完两个Demo了,应该对AsyncTask有了很深的理解了。但是想到这里不得不提出一个疑问,这看起来不就是开启一个子线程嘛,有啥不同的。其实AsyncTask和Handler两种异步方式还真有很大的区别。

首先来看AsyncTask,它是Android提供的轻量级的异步类,可以直接继承AsyncTask。在类中实现异步操作,并提供接口来反馈当前异步执行的程度,也就是所谓的进度更新,最后反馈执行的结果给UI主线程。这种方法使用起来简单快捷,过程清晰明了而且便于控制。不足的是在在使用多个异步操作和并需要进行Ui变更时,就变得复杂起来,代码也看起来比较臃肿。

其次是Handler,在本文开头的时候,就说了异步消息处理机制。它是通过Handler, Looper, Message,Thread四个对象之间的联系来进行处理消息的。这种方式在功能上比较清晰,有多个后台任务的时候代码看起来比较有序。

所以说在实际开发过程中,根据需要来选择异步任务处理方式,就我个人而言,还是Handler方式用的比较多。

好了,本文基本结束了。由于我技术水平有限,如有错误或不同意见,欢迎指正与交流。

优秀资料来源

慕课网 - Android必学-AsyncTask基础

Android必学之AsyncTask - caobotao

Android消息机制浅析 - xiasuhuei321

AsyncTask和Handler两种异步方式的实现和区别比较

项目源码

Github - AsyncTaskDemo - IamXiaRui

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注