Android消息机制(三) AsyncTask

参考
Android:异步处理之AsyncTask的应用(二)
使用AsyncTask
深入解析AsyncTask
真的会用AsyncTask么

一、概述

AsyncTask封装了线程池和Handler,方便开发者在子线程中更新UI。

  • 核心线程数等于CPU核心数+1
  • 最大线程数等于CPU核心数*2+1
  • 默认情况下核心线程会在线程池中一直存活,即使它们处于闲置状态
  • 非核心线程通过keepAliveTime设置闲置时长,超时会被回收。
  • 任务队列容量128:new LinkedBlockingQueue(128);

AsyncTask是一个抽象类,需要创建子类来使用它。

1.在继承时可以指定三个泛型参数:

  • Params
    传入的参数
  • Progress
    如果需要在界面显示进度,则使用这里指定的泛型作为进度单位
  • Result
    任务执行完毕的返回值类型

private class ImageLoader extends AsyncTask

2.重写四个方法
重写onPreExecute、doInBackground、onProgressUpdate、onPostExecute,注意不要手动去调用它们。除doInBackground在线程池中执行外,其它三个都在主线程中运行。

  • onPreExecute()
    界面的初始化操作

    protected void onPreExecute() {
    mAbort.setEnabled(true);
    mProgressBar.setVisibility(View.VISIBLE);
    mProgressBar.setProgress(0);
    mImageView.setImageResource(R.drawable.icon);
    }
  • doInBackground(Params...)
    处理耗时任务,通过return将任务结果返回。不可以操作UI,可以调用publishProgress方法来反馈执行进度。

    protected Bitmap doInBackground(String... url) {
    ...
    return BitmapFactory.decodeFile(getFileStreamPath(filename).getAbsolutePath());
    }

在doInBackground()中要检查isCancelled()的返回值,如果你的异步任务是可以取消的话。cancel()仅仅是给AsyncTask对象设置了一个标识位,当调用了cancel()后,发生的事情只有:AsyncTask对象的标识位变了,和doInBackground()执行完成后,onPostExecute()不会被回调了,而doInBackground()和onProgressUpdate()还是会继续执行直到doInBackground()结束。所以要在doInBackground()中不断的检查isCancellled()的返回值,当其返回true时就停止执行,特别是有循环的时候。如上面的例子,如果把读取数据的isCancelled()检查去掉,图片还是会下载,进度也一直会走,只是最后图片不会放到UI上(因为onPostExecute()没被回调)!
想想Java SE的Thread吧,是没有方法将其直接Cacncel掉的,那些线程取消也无非就是给线程设置标识位,然后在run()方法中不断的检查标识而已。所以要在doInBackground()中不断的检查isCancellled()的返回值,当其返回true时就停止执行,特别是有循环的时候

  • onProgressUpdate(Progress...)
    利用参数中的数值可以对UI更新

    protected void onProgressUpdate(Integer... progress) {
    mProgressBar.setProgress(progress[0]);
    }
  • onPostExecute(Result)
    收尾工作

    protected void onPostExecute(Bitmap image) {
    if (image != null) {
    mImageView.setImageBitmap(image);
    }
    mProgressBar.setProgress(100);
    mProgressBar.setVisibility(View.GONE);
    mAbort.setEnabled(false);
    }

    3.启动任务
    由于Handler需要和主线程交互,而Handler又是内置于AsnycTask中的,所以,AsyncTask的创建必须在主线程。
    一个AsyncTask对象只能execute()一次,否则会有异常抛出"java.lang.IllegalStateException: Cannot execute task: the task is already running"

    final ImageLoader loader = new ImageLoader();
    mGetImage = (Button) findViewById(R.id.async_task_get_image);
    mGetImage.setOnClickListener(new View.OnClickListener(){
    public void onClick(View v)
    {
    loader.execute(ImageUrl);
    }
    });
二、例子
Android消息机制(三) AsyncTask_第1张图片
下载前

Android消息机制(三) AsyncTask_第2张图片
下载中

Android消息机制(三) AsyncTask_第3张图片
下载后

完整代码如下:

package com.hilton.effectiveandroid.concurrent;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;

import com.hilton.effectiveandroid.R;

/*
 *AsyncTask cannot be reused, i.e. if you have executed one AsyncTask, you must discard it, you cannot execute it again.
 *If you try to execute an executed AsyncTask, you will get "java.lang.IllegalStateException: Cannot execute task: the task is already running"
 *In this demo, if you click "get the image" button twice at any time, you will receive "IllegalStateException".
 *About cancellation:
 *You can call AsyncTask#cancel() at any time during AsyncTask executing, but the result is onPostExecute() is not called after
 *doInBackground() finishes, which means doInBackground() is not stopped. AsyncTask#isCancelled() returns true after cancel() getting
 *called, so if you want to really cancel the task, i.e. stop doInBackground(), you must check the return value of isCancelled() in
 *doInBackground, when there are loops in doInBackground in particular.
 *This is the same to Java threading, in which is no effective way to stop a running thread, only way to do is set a flag to thread, and check
 *the flag every time in Thread#run(), if flag is set, run() aborts.
 */
public class AsyncTaskDemoActivity extends Activity {
    private static final String ImageUrl = "http://i1.cqnews.net/sports/attachement/jpg/site82/2011-10-01/2960950278670008721.jpg";
    private ProgressBar mProgressBar;
    private ImageView mImageView;
    private Button mGetImage;
    private Button mAbort;
    @Override
    public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    setContentView(R.layout.async_task_demo_activity);
    mProgressBar = (ProgressBar) findViewById(R.id.async_task_progress);
    mImageView = (ImageView) findViewById(R.id.async_task_displayer);
    final ImageLoader loader = new ImageLoader();
    mGetImage = (Button) findViewById(R.id.async_task_get_image);
    mGetImage.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
        loader.execute(ImageUrl);
        }
    });
    mAbort = (Button) findViewById(R.id.asyc_task_abort);
    mAbort.setOnClickListener(new View.OnClickListener() {
        public void onClick(View v) {
        loader.cancel(true);
        }
    });
    mAbort.setEnabled(false);
    }
    private class ImageLoader extends AsyncTask {
    private static final String TAG = "ImageLoader";
    @Override
    protected void onPreExecute() {
        // Initialize progress and image
        mGetImage.setEnabled(false);
        mAbort.setEnabled(true);
        mProgressBar.setVisibility(View.VISIBLE);
        mProgressBar.setProgress(0);
        mImageView.setImageResource(R.drawable.icon);
    }
    @Override
    protected Bitmap doInBackground(String... url) {
         //Fucking ridiculous thing happened here, to use any Internet connections, either via HttpURLConnection
        // or HttpClient, you must declare INTERNET permission in AndroidManifest.xml. Otherwise you will get
         //"UnknownHostException" when connecting or other tcp/ip/http exceptions rather than "SecurityException"
        // which tells you need to declare INTERNET permission.
        try {
        URL u;
        HttpURLConnection conn = null;
        InputStream in = null;
        OutputStream out = null;
        final String filename = "local_temp_image";
        try {
            u = new URL(url[0]);
            conn = (HttpURLConnection) u.openConnection();
            conn.setDoInput(true);
            conn.setDoOutput(false);
            conn.setConnectTimeout(20 * 1000);
            in = conn.getInputStream();
            out = openFileOutput(filename, Context.MODE_PRIVATE);
            byte[] buf = new byte[8196];
            int seg = 0;
            final long total = conn.getContentLength();
            long current = 0;
            //Without checking isCancelled(), the loop continues until reading whole image done, i.e. the progress
             //continues go up to 100. But onPostExecute() will not be called.
             //By checking isCancelled(), we can stop immediately, i.e. progress stops immediately when cancel() is called.
            while (!isCancelled() && (seg = in.read(buf)) != -1) {
            out.write(buf, 0, seg);
            current += seg;
            int progress = (int) ((float) current / (float) total * 100f);
            publishProgress(progress);
            SystemClock.sleep(1000);
            }
        } finally {
            if (conn != null) {
            conn.disconnect();
            }
            if (in != null) {
            in.close();
            }
            if (out != null) {
            out.close();
            }
        }
        return BitmapFactory.decodeFile(getFileStreamPath(filename).getAbsolutePath());
        } catch (MalformedURLException e) {
        e.printStackTrace();
        } catch (IOException e) {
        e.printStackTrace();
        }
        return null;
    }
    @Override
    protected void onProgressUpdate(Integer... progress) {
        mProgressBar.setProgress(progress[0]);
    }
    @Override
    protected void onPostExecute(Bitmap image) {
        if (image != null) {
        mImageView.setImageBitmap(image);
        }
        mProgressBar.setProgress(100);
        mProgressBar.setVisibility(View.GONE);
        mAbort.setEnabled(false);
    }
    }
}

doInBackground和onProgresssUpdate方法中的参数均包含...字样,在JAVA中...表示参数的数量不定,是一种数组型参数,可以使用new DownloadFilesTask().execute(url1,url2,url3);。注意上述例子中直接使用了数组中第一个数据。

三、注意事项
  • 如果要在AsyncTask中使用网络,一定不要忘记在AndroidManifest中声明INTERNET权限,否则会报出很诡异的异常信息,比如上面的例子,如果把INTERNET权限拿掉会抛出"UnknownHostException"。刚开始很疑惑,因为模拟器是可以正常上网的,后来Google了下才发现原来是没权限。
  • 与主线程有交互时用AsyncTask,否则就用Thread
    AsyncTask被设计出来的目的就是为了满足Android的特殊需求:非主线程不能操作(UI)组件,所以AsyncTask扩展Thread增强了与主线程的交互的能力。如果你的应用没有与主线程交互,那么就直接使用Thread就好了
  • 当有需要大量线程执行任务时,一定要创建线程池。线程的开销是非常大的,特别是创建一个新线程,否则就不必设计线程池之类的工具了。当需要大量线程执行任务时,一定要创建线程池,无论是使用AsyncTask还是Thread,因为使用AsyncTask它内部的线程池有数量限制,可能无法满足需求;使用Thread更是要线程池来管理,避免虚拟机创建大量的线程。比如从网络上批量下载图片,你不想一个一个的下,或者5个5个的下载,那么就创建一个CorePoolSize为10或者20的线程池,每次10个或者20个这样的下载,即满足了速度,又不至于耗费无用的性能开销去无限制的创建线程。
  • 在Android1.6前,AsyncTask是串行执行任务的,android1.6时采用线程池并行任务,但是从Androide3.0开始,为了避免并发错误,又采用一个线程串行执行任务。因为应用中可能还有其他地方使用AsyncTask,所以到网络取图片的AsyncTask也许会等待到其他任务都完成时才得以执行而不是调用executor()之后马上执行。API10及以前版本内部的线程池限制是5个,也就是说同时只能有5个线程运行,超过的线程只能等待。
    那么解决方法其实很简单,要么直接使用Thread。要么从Android 3.0 API11及以后版本中,创建一个单独的线程池(Executors.newCachedThreadPool()),使用接口#executeOnExecutor()提供。详情参见深入解析AsyncTask
  • 我们开发App过程中使用AsyncTask请求网络数据的时候,一般都是习惯在onPreExecute显示进度条,在数据请求完成之后的onPostExecute关闭进度条。这样做看似完美,但是如果您的App没有明确指定屏幕方向和configChanges时,当用户旋转屏幕的时候Activity就会重新启动,而这个时候您的异步加载数据的线程可能正在请求网络。当一个新的Activity被重新创建之后,可能由重新启动了一个新的任务去请求网络,这样之前的一个异步任务不经意间就泄露了,假设你还在onPostExecute写了一些其他逻辑,这个时候就会发生意想不到异常。
    一般简单的数据类型的,对付configChanges我们很好处理,我们直接可以通过onSaveInstanceState()和onRestoreInstanceState()进行保存与恢复。 Android会在销毁你的Activity之前调用onSaveInstanceState()方法,于是,你可以在此方法中存储关于应用状态的数据。然后你可以在onCreate()或onRestoreInstanceState()方法中恢复。
    但是,对于AsyncTask怎么办?问题产生的根源在于Activity销毁重新创建的过程中AsyncTask和之前的Activity失联,最终导致一些问题。
    详情参见真的会用AsyncTask么

你可能感兴趣的:(Android消息机制(三) AsyncTask)