紧接着上篇文章的Handler机制,AsyncTask是一个轻量级的用于处理异步任务的类,内部封装了Handler与线程池。
Hanlder我们都知道,线程池的解释是这样的:由于线程是一种受限的系统资源,即线程不可无限制的产生,并且线程的创建和销毁都有相应的开销。为了避免频繁创建和销毁线程所带来的系统开销,就采用了线程池,池中会缓存一定数量的线程,进而达到效果。
由上述解释来看,AsyncTask比Handler更为简洁高效。当然,Handler与AsyncTask都只适用于简单的异步操作,平常使用中更多的是采用成熟的第三方框架例如Volley,OkHttp等,这些之后有机会的话会深入学习,但是掌握基础的知识还是很有必要的。本篇文章主要学习下AsyncTask的基本知识,并给出一个apk下载及自动安装的Demo,这里是基于安卓4.4的,所以没有应用添加运行时权限。
基本概念
AsyncTask是一个抽象类,我们知道抽象类肯定是要被继承的并且子类要实现这个抽象类的所有抽象方法,否则这个子类就得声明为抽象类。那么AsyncTask常用的几个核心方法如下:
- onPreExecute():可以没有,运行在主线程中,可以直接对UI进行更新。通常在执行后台耗时操作前被调用,用于一些初始化操作,例如进度条的显示。
- doInBackground(Params…params):必须实现的方法,运行在子线程中,在onPreExecute方法执行后马上执行,通常用于处理所有的耗时任务,并可以调用publishProgress(Progress…)来实时更新任务进度。任务一旦完成就通过return语句将任务的执行结果返回,若Result被指定为void,就可不返回执行结果。
- publishProgress(Progress…):更新任务进度。
- onPostExecute(Result result):必须实现的方法,运行在主线程中。在doInBackground(Params…params)执行完成后,onPostExecute方法将被UI主线程调用,后台的任务结果(比如下载成功或失败)将通过该方法传递到UI线程中,并且在UI界面显示给用户。
- onProgressUpdate(Progress…values):运行在主线程中,在publishProgress(Progress…)方法被调用后,UI线程将调用这个方法从界面上显示任务的进度情况(可利用方法参数Progress来对UI进行相应地更新),例如通过一个进度条显示。
- onCancelled():运行在主线程,当异步任务被取消时被调用。
注意不能手动调用onPreExecute()、doInBackground()、onProgressUpdate()、onPostExecute()、onCancelled()这几个方法。
其次,AsyncTask还是抽象的泛型类,它包含三个泛型参数:
- Params:表示启动AsyncTask任务需要传入的参数,可用于在后台任务中使用,例如http请求的url,对应doInBackground方法参数。
- Progress:表示后台任务执行的进度,看上面参数为Progress…的方法。
- Result: 表示后台任务返回结果的类型,如String、Integer等,对应onPostExecute方法参数。
启动和结束异步任务的方法,都必须在主线程中调用:
- execute(Params…params):表示启动一个异步任务,并且一个异步对象只能调用一次execute()方法。
- cancel(booleanmayInterruptIfRunning):表示结束一个异步任务。
文件下载
看完了上述基本概念,这里来学习一下文件下载的代码,参考资料为《第一行代码》。这里改下载的文件为apk文件,并添加了apk安装的功能。
首先定义布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始下载"/>
<Button
android:id="@+id/pause"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="暂停下载"/>
<Button
android:id="@+id/cancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="取消下载"/>
</LinearLayout>
定义一个接口DownloadListener,用于对下载过程中的各种状态进行监听和回调:
public interface DownloadListener {
void onProgress(int progress);//用于通知当前的下载进度
void onSuccess();//用于通知下载成功事件
void onFailed();//用于通知下载失败事件
void onPaused();//用于通知下载暂停事件
void onCanceled();//用于通知下载取消事件
}
接下来定义任务下载类,利用上面说的AsyncTask来实现,新建DownloadTask继承AsyncTask,参数类型分别为<String(也就是传入的url类型为String),Integer(进度值类型),Integer(返回结果的类型)>:
public class DownloadTask extends AsyncTask<String,Integer,Integer>{
//下载状态 类变量
private static final int TYPE_SUCCESS = 0;
private static final int TYPE_FAILED = 1;
private static final int TYPE_PAUSED = 2;
private static final int TYPE_CANCELED = 3;
private DownloadListener downloadListener;
private boolean isPaused = false;
private boolean isCanceled = false;
private int lastProgress;
public DownloadTask(DownloadListener downloadListener) {
this.downloadListener = downloadListener;
}
@Override
protected Integer doInBackground(String... strings) {//参数类似于一个不定长的String数组
//执行耗时操作也就是下载文件
InputStream is = null;
RandomAccessFile saveFile = null;
File file = null;
try{
long downloadedLength = 0L;//记录已经下载的文件的长度
String downloadUrl = strings[0];//下载链接
String filename = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//下载文件名
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();//下载目录
file = new File(directory + filename);//下载文件路径加名称
if(file.exists()){
downloadedLength = file.length();//如果文件已经存在,计算出文件的长度赋值给downloadedLength
}
long contentLength = getContentLength(downloadUrl);//自定义函数,计算要下载的文件长度
if(contentLength == 0){//下载文件长度为0
return TYPE_FAILED;
}else if(contentLength == downloadedLength){
return TYPE_SUCCESS;//下载文件长度和已下载文件长度相同 证明下载已完成
}
OkHttpClient okHttpClient = new OkHttpClient();
Request request = new Request.Builder().addHeader("RANGE","bytes=" + downloadedLength +"-").url(downloadUrl).build();
Response response = okHttpClient.newCall(request).execute();
if(response != null){
is = response.body().byteStream();
saveFile = new RandomAccessFile(file,"rw");
saveFile.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 = total + len;
saveFile.write(b,0,len);
int progress = (int)((total + downloadedLength)*100/contentLength);
publishProgress(progress);
}
}
response.body().close();
return TYPE_SUCCESS;
}
}catch (Exception e){
e.printStackTrace();
}finally {//不管有没有下载完成都会执行
try{
if (is!=null){
is.close();
}
if(saveFile!=null){
saveFile.close();
}
if(isCanceled&&file!=null){//如果取消了就删除文件
file.delete();
}
}catch (Exception e){
e.printStackTrace();
}
}
return TYPE_FAILED;
}
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();
return contentLength;
}
return 0;
}
@Override
protected void onProgressUpdate(Integer... values) {
//根据显示任务执行进度
int progress = values[0];
if(progress > lastProgress){
downloadListener.onProgress(progress);
lastProgress = progress;
}
}
@Override
protected void onPostExecute(Integer integer) {
//根据doInBackground方法中的返回情况判断任务的执行结果
switch(integer){
case TYPE_SUCCESS:
downloadListener.onSuccess();
break;
case TYPE_FAILED:
downloadListener.onFailed();
break;
case TYPE_PAUSED:
downloadListener.onPaused();
break;
case TYPE_CANCELED:
downloadListener.onCanceled();
default:
break;
}
}
public void pauseDownload(){
//更改暂停变量值为true
isPaused = true;
}
public void cancelDownload(){
//更改取消变量值为true
isCanceled = true;
}
}
重要的就是上面我们提到的几个方法:
- doInBackground():用于在后台执行具体的下载逻辑 。
- onProgressUpdate():用于在界面上更新当前的下载进度。
- onPostExecute():用于通知最终的下载结果。
完成上面的下载逻辑后,为了保证DownloadTask可以一直在后台运行,还需要创建一个下载的服务,新建一个DownloadService,在DownloadService中启动DownloadTask异步任务:
public class DownloadService extends Service {
private DownloadTask downloadTask;
private String downloadUrl;
int lastprogress;
private DownloadListener listener = new DownloadListener() {
@Override
public void onProgress(int progress) {
lastprogress = progress;
getNotificationManager().notify(1,getNotification("下载中...",progress));
}
@Override
public void onSuccess() {
downloadTask = null;
//将前台服务通知关闭
stopForeground(true);
getNotificationManager().notify(1,getSuccessNotification("下载完成",-1));
Toast.makeText(DownloadService.this,"下载完成",Toast.LENGTH_SHORT).show();
}
@Override
public void onFailed() {
downloadTask = null;
//将前台服务通知关闭
stopForeground(true);
getNotificationManager().notify(1,getNotification("下载失败",-1));
Toast.makeText(DownloadService.this,"下载失败",Toast.LENGTH_SHORT).show();
}
@Override
public void onPaused() {
downloadTask = null;
getNotificationManager().notify(1,getSuccessNotification("下载暂停",lastprogress));
Toast.makeText(DownloadService.this,"下载暂停",Toast.LENGTH_SHORT).show();
}
@Override
public void onCanceled() {
downloadTask = null;
//将前台服务通知关闭
stopForeground(true);
Toast.makeText(DownloadService.this,"下载取消",Toast.LENGTH_SHORT).show();
}
};
private Notification getSuccessNotification(String title,int progress) {
String filename = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//下载文件名
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();//下载目录
File file = new File(directory + filename);//下载文件路径加名称
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
Notification.Builder mBuilder = new Notification.Builder(this);
mBuilder.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
.setContentIntent(pi)
.setContentTitle(title)
.setContentText("点击安装")
.setAutoCancel(true);
if(progress > 0){
mBuilder.setContentText(progress+"%").setProgress(100,progress,false);
}
return mBuilder.build();
}
private NotificationManager getNotificationManager(){
return (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
}
private Notification getNotification(String title,int progress){
//首先设置点击通知后跳转的页面
Intent intent = new Intent(this,MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this,0,intent,0);
Notification.Builder mBuilder = new Notification.Builder(this);
mBuilder.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.mipmap.ic_launcher))
.setContentIntent(pi)
.setContentTitle(title);
if(progress > 0){
mBuilder.setContentText(progress+"%").setProgress(100,progress,false);
}
return mBuilder.build();
}
class DownloadBinder extends Binder{
public void startDownload(String url){
if(downloadTask == null){
downloadUrl = url;
downloadTask = new DownloadTask(listener);
downloadTask.execute(downloadUrl);//传入AsyncTask的参数
Toast.makeText(DownloadService.this,"下载中...",Toast.LENGTH_SHORT).show();
}
}
public void pauseDownload(){
if(downloadTask!=null){
downloadTask.pauseDownload();
}
}
public void cancelDownload(){
if (downloadTask != null) {
downloadTask.cancelDownload();
}else {
//比如说暂停下载后想取消下载
String filename = downloadUrl.substring(downloadUrl.lastIndexOf("/"));//下载文件名
String directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();//下载目录
File file = new File(directory + filename);//下载文件路径加名称
if(file.exists()){
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this,"下载取消",Toast.LENGTH_SHORT).show();
}
}
}
private DownloadBinder mBinder = new DownloadBinder();
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
在MainActivity中,主要就是绑定DownloadService,然后与DownloadService通信,根据按钮点击事件的不同分别调用DownloadService的不同方法,DownloadService中的不同方法实际上又启动了DownloadTask或者调用了DownloadTask的一些方法:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private DownloadService.DownloadBinder downloadBinder;
private Button btn_start;
private Button btn_pause;
private Button btn_cancel;
private ServiceConnection connection = new ServiceConnection() {
//Service与Activity通信
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//利用这个downloadBinder就可以调用Service中DownloadBinder类的方法
downloadBinder = (DownloadService.DownloadBinder) service;
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindviews();
btn_start.setOnClickListener(this);
btn_pause.setOnClickListener(this);
btn_cancel.setOnClickListener(this);
Intent intent = new Intent(this,DownloadService.class);
startService(intent);
bindService(intent,connection,BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
}
private void bindviews() {
btn_start = findViewById(R.id.start);
btn_pause = findViewById(R.id.pause);
btn_cancel = findViewById(R.id.cancel);
}
@Override
public void onClick(View v) {
if(downloadBinder == null){
return;
}
switch (v.getId()){
case R.id.start:
//根据不同的点击事件调用downloadBinder中的不同方法
String url = "http://apk.fr18.mmarket.com/rs/gcenter/data/330000003213/53/000000194602/1.apk";
downloadBinder.startDownload(url);
break;
case R.id.pause:
downloadBinder.pauseDownload();
break;
case R.id.cancel:
downloadBinder.cancelDownload();
break;
default:
break;
}
}
}
修改AndroidManifest.xml文件,这里因为是4.4的测试真机,就没有设置运行时权限,如果安装在6.0以上的设备记得申请下运行时权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.demo.asynctask">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".DownloadService"
android:enabled="true"
android:exported="true"/>
</application>
</manifest>
最终效果如下:
点击开始下载:
接着点击暂停:
然后再次点击开始下载:
下载完成:
点击安装: