Android 系统默认会在主线程(UI 线程)执行任务,但是如果有耗时程序就会阻塞 UI 线程,导致页面卡顿。这时候我们通常会将耗时任务放在独立的线程,然后通过 Handler 等线程间通信机制完成 UI 的刷新。很多时候我们也许只是想执行一个简单的任务,为此写一套 Handler 线程通信就会显得比较复杂,不用担心,Android 系统为我们提供了一个专门用于执行异步任务的工具——AsyncTask
在Android的android.os.AsyncTask的类前注释(2021-05),对AsyncTask有着比较负面的评价,但不会影响我们在短时间的后台线程操作中使用它:
AyncTask 旨在轻松和正确使用UI线程,但是最常见的用例是集成到UI中,这将导致Context泄漏,遗漏回调或者因配置更改导致crash等问题,在平台的不同版本中,它的行为也不一致,隐藏了doInBackground的异常,并且与直接使用Executor相比,并没有提供太多实用程序。AsyncTask是围绕Thread和Handler的辅助类,并不是通用的线程框架。理想情况下,AsyncTask应该用于短时间的操作(最多几秒钟),如果需要长时间保持线程运行,强烈建议您使用java提供的java.util.concurrent软件包,例如ThreadPoolExecutor和FutureTask
我们首先先来看一下AsynTask的基本用法,由于AsyncTask是一个抽象类,所以我们不能创建AsyncTask,应该是继承自AsyncTask实现一个它的子类,在继承时我们可以为AsyncTask类指定3个泛型参数,这3个参数的用途如下:
public class MyAsyncTask extends AsyncTask {
@Override
protected void onPreExecute() {
//运行在主线程,在doBackground前执行
super.onPreExecute();
}
@Override
protected void onPostExecute(Long aLong) {
//执行完毕,更新UI
super.onPostExecute(aLong);
}
@Override
protected void onProgressUpdate(Integer... values) {
//任务执行进度更新
super.onProgressUpdate(values);
}
@Override
protected Long doInBackground(URL... urls) {
//执行后台耗时任务
return null;
}
}
这里我们把AsyncTask的第一个泛型参数指定为void,表示在执行AsyncTask的时候不需要传入参数给后台任务。第二个泛型参数指定为Integer,表示使用整型数据来作为进度显示单位。第二个泛型参数指定为Boolean,则表示使用Boolean 数据来反馈执行结果。
本节,是参考《第一行代码》第二版 第十章关于AsyncTask的例子,实现了下载QQ的安装包的例子
package com.example.servicebestpratice;
import android.os.AsyncTask;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import okhttp3.OkHttp;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class DownloadTask extends AsyncTask<String, Integer, Integer> {
private static final String TAG = "DownloadTask";
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;
}
/**
* 用于在后台执行具体的下载逻辑
*
* @param params
* @return
*/
@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();
Log.d(TAG, "doInBackground directory is" + directory);
file = new File(directory + fileName);
if (file.exists()) {
downloadedLength = file.length();
Log.d(TAG, "doInBackground file is exists, downloadLength is:" + downloadedLength);
}
long contentLength = getContentLength(downloadUrl);
Log.d(TAG, "get contentLength from url,the length:" + contentLength);
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) {
Log.e(TAG, "doInBackground,response is null");
return TYPE_FAILED;
}
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);
Log.d(TAG, "loading ,the progress is " + progress);
publishProgress(progress);
}
}
Log.d(TAG, "response.body.close");
response.body().close();
return TYPE_SUCCESS;
} catch (Exception e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (savedFile != null) {
savedFile.close();
}
if (isCanceled && file != null) {
file.delete();
}
} catch (Exception e) {
Log.e(TAG, e.getMessage());
e.printStackTrace();
Log.e(TAG, "download failed");
}
}
return TYPE_FAILED;
}
/**
* 用于在界面上更新当前下载进度
*
* @param values
*/
@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if (progress > lastProgress) {
listener.onProgress(progress);
lastProgress = progress;
}
}
/**
* 用于通知最终的下载结果
*
* @param status
*/
@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();
break;
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();
Log.i(TAG, "getContentLength response.isSuccessful(),the contentLength is" + contentLength);
return contentLength;
} else {
Log.d(TAG, "getContentLength response is:" + response.isSuccessful() + ",downloadUrl is" + downloadUrl);
}
return 0;
}
}
package com.example.servicebestpratice;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Binder;
import android.os.Environment;
import android.os.IBinder;
import android.util.Log;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import java.io.File;
public class DownloadService extends Service {
private static final String TAG="DownloadService";
private DownloadTask downloadTask;
private String downloadUrl;
private DownloadListener listener=new DownloadListener() {
//触发通知,通知正在下载中
@Override
public void onProgress(int progress) {
getNotificationManager().notify(1,getNotification("Downloading...",progress));
}
//将正在下载的前台通知关闭,然后创建一个新的通知用于告诉用户下载成功
@Override
public void onSuccess() {
downloadTask=null;
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download success",-1));
Toast.makeText(DownloadService.this,"Download Success",Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed() {
downloadTask=null;
//下载失败时将前台服务通知关闭,并创建一个下载失败的通知
stopForeground(true);
getNotificationManager().notify(1,getNotification("Download Failed",-1));
Toast.makeText(DownloadService.this,"Download Failed",Toast.LENGTH_SHORT).show();
}
@Override
public void onPaused() {
downloadTask=null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
}
@Override
public void onCanceled() {
}
};
public DownloadService() {
}
private DownloadBinder mBinder=new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private NotificationManager getNotificationManager(){
return (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
}
class DownloadBinder extends Binder{
public void startDownload(String url){
if(downloadTask==null){
downloadUrl=url;
downloadTask=new DownloadTask(listener);
//调用execute 开启下载
downloadTask.execute(downloadUrl);
startForeground(1,getNotification("Downloading...",0));
Toast.makeText(DownloadService.this,"Downloading...",Toast.LENGTH_SHORT).show();
}
}
public void pauseDownload(){
if(downloadTask!=null){
downloadTask.pauseDownload();
}
}
public void cancelDownload(){
if(downloadTask!=null){
downloadTask.cancelDownload();
}else{
if(downloadUrl!=null){
//取消下载时需将文件删除,并将通知关闭
String fileName=downloadUrl.substring(downloadUrl.lastIndexOf("/"));
String directory= Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
Log.d(TAG,directory);
File file=new File(directory+fileName);
if(file.exists()){
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this,"Canceled",Toast.LENGTH_SHORT).show();
}
}
}
}
private Notification getNotification(String title,int progress){
Intent intent=new Intent(this,MainActivity.class);
PendingIntent pi=PendingIntent.getActivity(this,0,intent,0);
NotificationCompat.Builder builder=new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher));
builder.setContentIntent(pi);
builder.setContentTitle(title);
if(progress>0){
//当progress 大于或者等于0才需显示下载进度
builder.setContentText(progress+"%");
//显示进度:最大进度,当前进度,是否使用模糊进度条
builder.setProgress(100,progress,false);
}
return builder.build();
}
}
package com.example.servicebestpratice;
public interface DownloadListener {
/**
*通知当前下载进度
* @param progress
*/
void onProgress(int progress);
void onSuccess();
void onFailed();
/**
* 用于通知下载暂停
*/
void onPaused();
void onCanceled();
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/start_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Download"/>
<Button
android:id="@+id/pause_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Pause Download"/>
<Button
android:id="@+id/cancel_download"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="cancel Download"/>
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="https://down.qq.com/qqweb/QQ_1/android_apk/Android_8.7.0.5295_537068059.apk"/>
LinearLayout>
package com.example.servicebestpratice;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.Manifest;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.IBinder;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private DownloadService.DownloadBinder downloadBinder;
//构建ServiceConnection的实现对象,通过onServiceConnected实现来创建Binder对象
private ServiceConnection connection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder=(DownloadService.DownloadBinder)service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button startDownload=(Button)findViewById(R.id.start_download);
Button pauseDownload=(Button)findViewById(R.id.pause_download);
Button cancelDownload=(Button)findViewById(R.id.cancel_download);
startDownload.setOnClickListener(this);
pauseDownload.setOnClickListener(this);
cancelDownload.setOnClickListener(this);
Intent intent=new Intent(this,DownloadService.class);
startService(intent);//启动服务
bindService(intent,connection,BIND_AUTO_CREATE);//绑定服务
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)!= PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermissions(MainActivity.this,new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE},1);
}
}
@Override
public void onClick(View v) {
if(downloadBinder==null){
return;
}
switch (v.getId()){
case R.id.start_download:
String url="https://down.qq.com/qqweb/QQ_1/android_apk/Android_8.7.0.5295_537068059.apk";
downloadBinder.startDownload(url);
break;
case R.id.pause_download:
downloadBinder.pauseDownload();
break;
case R.id.cancel_download:
downloadBinder.cancelDownload();
break;
default:
break;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode){
case 1:
if(grantResults.length>0 && grantResults[0]!=PackageManager.PERMISSION_GRANTED){
Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
}