参考
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);
}
});
二、例子
完整代码如下:
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么