- Android的UI线程是非阻塞线程,不能在主线程做一些耗时的操作。否则一旦超过五秒,会抛出ANR(Application Not Responding)异常,就是应用无响应。
故此,一些耗时的操作都要放置在子线程去做,但同时在子线程内又不能更新UI,因为UI线程非阻塞,要是你改一点他改一点,岂不是乱了套。 - 但是恰巧有的时候,我们恰恰需要在耗时操作后去更新UI,这个时候就需要用到Handler机制了。
Handler机制在Android中应用十分普及,什么地方都能看到它的影子。但是了解它的同学都知道异步消息处理机制整个流程还是颇有些繁琐的。有没有更好用的方法呢? 答案自然是有的。谷歌工程师在API为我们设计了这样一个类,AsyncTask异步任务类。我第一次接触时整整花了一个下午才搞好,但只要学会了,以后立马保证你爱不释手。因为太方便了。
AsyncTask是一个抽象类,需要一个实现类才能使用。同时它还有三个泛型参数:
- Params:执行AsyncTask需要传入的参数,用于在后台任务中使用
- Progress:后台任务执行时,需要在界面显示当前进度,则使用这里的指定泛型作为进度单位
- Result:任务执行完毕后,需要对结果进行返回,则使用指定的泛型作为返回值类型
同时还有几个经常需要重写的方法:
- onPreExcute()方法:此方法会在后台任务开始执行之前调用,用于进行一些界面上的初始化操作,比如显示一个进度条对话框等等
- doInBackground(params……)方法:此方法中所有代码都会在子线程运行,也就是我们把耗时的操作以及一些需要放在子线程的操作搁在这里面就行了,譬如网络连接。
此间任务完成后可以通过return语句将任务执行结果返回并传递到onPostExute(Result)的方法参数中。当然若是你的泛型参数Progress设置为Void的话,那么此方法就会返回null了。 - onProgressUpdate(Progress……)方法:此方法的参数也是接受一个不定大小的数组,当然此方法必须在doInBackground()方法中调用publishProgress(Progress……)才会被调用。
在此方法中多执行的代码就是实时更新进度条进度的代码了。 - onPostExcute()方法:在之前提过了,此方法是最后一个才会被调用的方法,用于处理一些收尾的工作。当然也能接受doInBackground方法返回的结果来处理一些UI操作。还有一些关闭进度条以及对话框的操作等。
~~接下来,会给大家贴一个Demo来实践异步任务类,毕竟实践出真知。掌不掌握,只有敲了才知道。
~~Demo演示之前,先给大家奉上一张图,表示三个泛型参数和四个方法之间的联系,略丑不过简单明了,初学者一看便知。
~~好了,贴Demo。此次演示一个从服务器下载图片并显示在界面上的Demo。
- 首先,构建一个Activity中的内部AsyncTask类。方便调用一些组件对象。
class InternetAsyncTask extends AsyncTask<String, Integer, Bitmap> {
//底层是使用线程池使用的。有一个bug,第一次进入后若是没有等到进度条走完就退出,第二次进入时,进度条会等待
//一段时间才会开始走。这时因为这个线程在等待上一个线程走完
@Override
protected Bitmap doInBackground(String... params) {
String url = params[0];//拿到asyncTask.execute(URL);传递的参数
Bitmap bitmap = null;
try {
//网络连接的代码
java.net.URL url1 = new URL(url);
URLConnection connection = url1.openConnection();
InputStream inputStream = connection.getInputStream();
BufferedInputStream stream = new BufferedInputStream(inputStream);
//目标文件的大小
int count = url.length();
long totalSize = 0;//已下载文件的大小
bitmap = BitmapFactory.decodeStream(stream);
for (int i = 0; i < count; i++) {
totalSize += stream.read();//read每次读一个字节,则总大小就不断的加这个值
SystemClock.sleep(1000);//沉睡一秒
//将进度条值传递到onProgressUpdate方法
publishProgress((int) ((i / (float) count) * 100));
// 如果判定此时AsyncTask的状态为取消状态,就打破循环。相应代码为onPause的
//asyncTask.cancel(true);
if (isCancelled()) break;
}
inputStream.close();//关闭流
stream.close();
} catch (Exception e) {
e.printStackTrace();
}
return bitmap;//返回的值会传递到onPostExecute方法的参数中
}
public InternetAsyncTask() {
super();
}
@Override
protected void onPreExecute() {
//初始化操作,初始化进度条
progressBar.setVisibility(View.VISIBLE);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
//得到Bitmap参数,设置图片并关闭进度条
imageView.setImageBitmap(bitmap);
progressBar.setVisibility(View.GONE);
}
@Override
protected void onProgressUpdate(Integer... values) {
super.onProgressUpdate(values);
progressBar.setProgress(values[0]);
}
- 以上就是自定义AsynTask的代码了,在上面代码中我们再doInBackground方法中实现网络连接下载图片,并将图片的结果返回到onPostExcute方法中,在此方法中实现UI更新操作。
- 同时,我们还加了一个进度条的使用。其实进度条的使用十分简单,在onPreExecute()方法中显示进度条,在doInBackground方法中将读取流的循环操作中,将实时文件的大小略作处理,传递给onProgressUpdate方法即可。
- 还有一点需要我们注意,那就是一个小bug。
- 当我们点进去这个下载活动时,进度条正在走动。这个时候我们再退出此活动,再次进入此活动时。会发现进度条刻度为零,并且等了一会会才开始走动。这无疑会带来非常差的用户体验,用户不知道还以为机器卡顿呢。那么原因是什么呢?
- 因为AsyncTask底层是使用线程池技术来实现的。也就是开辟了一个新的线城会等到另一个线程执行完毕,才会执行。第一次进入时,旧线程暂没执行完毕。第二次进入时,新线程自然要等待旧线程执行完毕才会执行了。
- 解决办法其实很简单:
只要在Activity的onPause方法中设置AsyncTask的状态为取消状态,然后在doInBackground方法的循环流操作中判定此时是否为取消状态,是的话就跳出循环就行了。
Demo演示如下:
@Override
protected void onPause() {
super.onPause();
//如果异步任务类不为空并且状态正在运行
if (asyncTask != null && asyncTask.getStatus() == AsyncTask.Status.RUNNING) {
//cancel方法只是将对应的AsyncTask标记为cancel状态,不是真正取消线程执行
asyncTask.cancel(true);
}
}
- 最后,直接贴上启动AsyncTask的代码:
asyncTask = new InternetAsyncTask();
//调用自定义asyncTask,将URL参数传进去。这个参数会传递到doInBackground方法的参数数组中,
//因为只传递了一个参数,所以在doInBackground中调用使用的是params[0];
asyncTask.execute(Url);
以上。