您的位置:首页 > 其它

AsyncTask的详解与服务的结合下载文件例子

2017-03-01 11:07 591 查看

1. 写在前面

前面一两个月一直在赶需求,不管什么功能先在网上或者github上找到做出来再说,最近半个月没事做,一直在改代码。发现以前项目中的软件更新没做好,刚好看到郭神书里面这文件下载例子用于更新下载不错,让后台下载任务在服务中执行,然后通过activity和服务绑定,使activity和service可以进行通信。并且好好的学习了asyncTask,现在框架用的比较多,很少有机会接触到这个了。这个文件下载比较好的地方一个是可以像QQ样,更新时可以在界面上还可以操作,还有一个是实现了断点下载,可以接着上次没下载完的地方继续下载。效果图如下:



2. 学习

a.继承AsyncTask需要指定的三个参数,AsyncTask(params,progress,result),看着api,可以了解他们的含义



params:在使用asynctask时需要传入的参数,可以供后台任务使用,可以为void;

progress:后台任务执行时,如果需要在界面上显示当前进度,则使用这里指定的泛型为单位;

result:当后台任务执行完毕时,需要对结果进行返回,这里指定什么泛型就返回什么样的数据类型,比如,这里指定boolean,则后台任务执行完毕返回true或者false;

当然,继承AsyncTask我们经常要重写的几个方法,看api



解释过来,onPreExecute():在后台任务之前在ui线程中调用,这一步通常用于设置后台任务,比如展示一个进度条在用户面前;

doInBackground(Params…):(这翻译过来很直观)在onpreExecute()方法一结束,立马在后台线程调用,这一步在后台运行计算,可能需要一段时间,异步任务的参数传递到这一步,这一步的这些计算一定要有返回,并且要将这些返回结果返回到最后一步。这一步同样是可以使用publishProgress(Progress…)这个方法去发布一个或多个单元的进度,这些值被发布在ui线程的 onProgressUpdate(Progress…) 这一步中;

onProgressUpdate(Progress…):当publishProgress(Progress…)被使用后,被调用在ui线程,执行的时间是不能确定的,这个方法用与在后台任务一直被执行时,展示在用户面前的进度,它能够被用于带动画的进度条或者文本log;

onPostExecute(Result): 被调用在ui线程,当后台任务计算完成,后台任务计算的结果被用来当做参数在这一步中;

b. 有关File这个类的一些构造方法。郭神在保存下载文件时是将它保存在公共下载目录下,我搞错了一个地方所以对他获取file对象的方法很不理解。网上也查不到自己想要的答案,后面在api中看了了File的构造方法。



c. 活动和服务通信。在活动中控制服务。通过在service中继承Binder写个自己的Binder类,里面写上自己想要在活动中调用的方法。同时注意在service中重写的onBind()中返回我们自己的Binder对象。然后在活动中 创建ServiceConnection的匿名类,在里面重写onServiceConnected()方法和onServiceDisconnected()方法,这两个方法会在活动和服务成功绑定和断开时调用。并且在onServiceConnected()方法中,获得自己的Binder对象,这样子就可以在活动中想调用Binder中的方法就调用什么方法了。

3. 代码

a.

DownloadTask extends AsyncTask<String,Integer,Integer>,


在选择了这样三种参数,即我们传入String类型参数供后台任务使用,用Integer来表示后台任务的进度,后台任务返回的是Integer类型,这样的的返回值就是我们在onPostExecute()方法中的参数。

这里没有使用onPreExecute()方法,所以在执行这个asynctask直接从后台任务开始。

@Override
protected Integer doInBackground(String... params) {

InputStream is = null;
RandomAccessFile savedFile = null ;
File file = null;
try {
long downloadFileLength = 0; //记录已下载的文件长度
String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
//--->  /storage/emulated/0/Download
String directroy = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
Log.i(TAG, "directroyPath--->" + directroy);
file = new File(directroy+fileName);
if(file.exists()){  //存在,拿到这个文件已经下载的长度,从这里开始下载
downloadFileLength = file.length();
}
//获得这个下载文件的总长度,使用okhttp
long contentLength = getContentLength(downloadUrl);
if(contentLength==0){//url地址文件长度为0,下载失败
return TYPE_FAILED;
}else if(contentLength==downloadFileLength){ //下载完成
return TYPE_SUCCESS;
}
//运行到这里,说明既不会这个url地址有问题,也不会说这个已经下载的文件长度已经下载完成了
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(downloadUrl)
.addHeader("RANGE","bytes="+downloadFileLength+"-")
.build();
Response response = client.newCall(request).execute();//同步堵塞
if(response!=null){
is = response.body().byteStream();
savedFile = new RandomAccessFile(file, "rw");
savedFile.seek(downloadFileLength);
byte[] b = new byte[1024];
int total = 0;
int len ;
while((len=is.read(b)) != -1){
if(isCancled){
return TYPE_CANCELED;
}else if(isPaused){
return TYPE_PAUSED;
}else{
total+=len;
savedFile.write(b, 0, len);

//计算已经下载的百分比

int progress = (int) ((total + downloadFileLength) * 100 / contentLength);
publishProgress(progress);

}
}
//当运行到这里说明将url文件剩下的长度读写到文件中了
response.body().close();
return TYPE_SUCCESS;
}
} catch (IOException e) {
e.printStackTrace();
}
finally {
try {
if (is != null) {
is.close();
}
if (savedFile != null) {
savedFile.close();
}
if (isCancled && file != null) {
file.delete();
}
} catch (Exception e) {
e.printStackTrace();
}
}
return TYPE_FAILED;
}


这里面的代码稍微要注意的有,

String downloadUrl = params[0];
String fileName = downloadUrl.substring(downloadUrl.lastIndexOf("/"));
//--->  /storage/emulated/0/Download
String directroy = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
Log.i(TAG, "directroyPath--->" + directroy);
file = new File(directroy+fileName);


获取这个File对象的方法,正如上面所说的,用的构造方法是这个

File(String path)
Constructs a new file using the specified path.


这里郭神在截取filename时故意把这个”/”一起截取下来了,然后就可以和download正常拼接,我这里搞错了,一直不对。

还有一点是这个RandomAccessFile类,平常好像没见过,看了下api很多方法和File是一样的,最大的特点就是随机访问,其实看着翻译也看不出什么,就两点,一个这种随机访问读写文件和FileInputStream or FileOutputStream不一样,还有就是最后一句,每次读写操作后,下次读写的位置可以向前或者向后。这次实现断点下载靠这个类。



构造方法我们这里用的是这种

RandomAccessFile(File file, String mode)
Constructs a new RandomAccessFile based on file and opens it according to the access string in mode.




为什么代码中在构造时使用“rw”也知道了,特点是这个文件被打开用来读写时,如果不存在就会创建。



我们这里使用的方法

savedFile.seek(downloadFileLength);




翻译过来大致意思,在读写或者跳过操作时,可以移动文件的指针去一个新的位置。

最重要的部分代码应该就是这里了

byte[] b = new byte[1024];
int total = 0;
int len ;
while((len=is.read(b)) != -1){
if(isCancled){
return TYPE_CANCELED;
}else if(isPaused){
return TYPE_PAUSED;
}else{
total+=len;
savedFile.write(b, 0, len);

//计算已经下载的百分比

int progress = (int) ((total + downloadFileLength) * 100 / contentLength);
publishProgress(progress);

}
}


其中涉及到文件io读写的代码有这两个地方

(len=is.read(b)
savedFile.write(b, 0, len);


查api可知,

int read(byte[] buffer)
Equivalent to read(buffer, 0, buffer.length).


inputstream.read(byte[] buffer)等同于read(buffer,0,buffer.length),所以我们直接看它的重载方法即可。

public int read (byte[] buffer, int byteOffset, int byteCount)

Added in API level 1
Reads up to byteCount bytes from this stream and stores them in the byte array buffer starting at byteOffset. Returns the number of bytes actually read or -1 if the end of the stream has been reached.

Throws
IndexOutOfBoundsException   if byteOffset < 0 || byteCount < 0 || byteOffset + byteCount > buffer.length.
IOException if the stream is closed or another IOException occurs.


这里的说明太重要了,大致:从io流中byteOffset这个位置开始读取byteCount大小的字节,保存在buffer这个字节数组中,返回这个字节的实际大小,如果读到最后,则返回-1。这里面最重要的是读定额大小的字节保存在字节数组中,只要没有将这些字节写到文件中,会一直在。然后返回的是字节的大小,所以可以是int类型。

然后randomAccessFile的write和file好像差不错,api如下

public void write (byte[] buffer, int byteOffset, int byteCount)

Added in API level 1
Writes byteCount bytes from the byte array buffer to this file, starting at the current file pointer and using byteOffset as the first position within buffer to get bytes.

Parameters
buffer  the buffer to write.
byteOffset  the index of the first byte in buffer to write.
byteCount   the number of bytes from the buffer to write.


意思大致:从字节数组buffer中写byteCount 大小的字节到这个文件,开始于这个文件的当前指针并且使用byteOffset 为第一个位置且从buffer获取字节。

用于和文件中已经下载的文件长度的目标文件长度方法,这里没什么就是一个okhttp的使用。

/**
* 获取目标文件的长度
* @param url
* @return
*/
private long getContentLength(String url) throws IOException {
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
Response response =  client.newCall(request).execute();
if(response!=null && response.isSuccessful()){
long contentLength =  response.body().contentLength();
response.body().close();
return contentLength;
}
return 0;
}


前面发布进度方法 publishProgress(progress)后需要调用的方法,也没什么

@Override
protected void onProgressUpdate(Integer... values) {
int progress = values[0];
if(progress>lastProgress){
listener.onProgress(progress);
lastProgress = progress;
}
}


后台任务执行完成后调用的方法,很简单。

@Override
protected void onPostExecute(Integer integer) {

switch (integer){

case TYPE_CANCELED:
listener.onCanccled();
break;

case TYPE_FAILED:
listener.onFailed();
break;

case TYPE_PAUSED:
listener.onPaused();
break;

case TYPE_SUCCESS:
listener.onSuccess();
break;

}
}


b. 因为在活动中我们需要控制服务,所以要写出服务,因为存在前台服务通知,和通知获取Notification是一样的,所以郭神直接提取来了

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){
builder.setContentText(progress+"%");

/**
* public Notification.Builder setProgress (int max, int progress, boolean indeterminate)
* 这三个参数的意思,前面两个不用说了,第三个查了下资料//设置为true,表示流动,设置为false,表示刻度,大致了解,多验证下true或者false即可
* Set the progress this notification represents. The platform template will represent this using a ProgressBar.
* 查开发api发现这个方法用在这里还挺不错的,翻译大致意思:设置这个通知代表的进度,至于这进度的表现形式是平台所有的 进度条
*
*/
builder.setProgress(100, progress, false);
}
return builder.build();
}


里面要注意已经写在代码注释中了。然后在服务中将下载监听器的匿名内部类写出来

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;
Toast.makeText(DownloadService.this,"Paused",Toast.LENGTH_SHORT).show();

}

@Override
public void onCanccled() {

downloadTask = null;
stopForeground(true);
Toast.makeText(DownloadService.this,"Cancled",Toast.LENGTH_SHORT).show();

}
};


因为我们需要在活动中绑定服务,所以需要继承写自己的Binder,Binder

里面的方法很容易看懂。

private DownloadBinder downloadBinder = new DownloadBinder();

@Nullable
@Override
public IBinder onBind(Intent intent) {
return downloadBinder;
}

class DownloadBinder extends Binder{

public void startDownload(String url){

if(downloadTask==null){
downloadUrl = url;
downloadTask = new DownloadTask(listener);
downloadTask.execute(downloadUrl);
startForeground(1,getNotification("Download...",0));
Toast.makeText(DownloadService.this,"Download...",Toast.LENGTH_SHORT).show();
}
}

public void pauseDownload(){

if(downloadTask != null){
downloadTask.pausedDownload();
//只要后台任务执行过程中接收到暂停,任务就会停止
}
else {
downloadTask = new DownloadTask(listener);
downloadTask.execute(downloadUrl);
Toast.makeText(DownloadService.this,"Download...",Toast.LENGTH_SHORT).show();
}
}

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();
File file = new File(directory+fileName);
if(file.exists()){
file.delete();
}
getNotificationManager().cancel(1);
stopForeground(true);
Toast.makeText(DownloadService.this,"Cancled",Toast.LENGTH_SHORT).show();
}
}
}

}


c. 在活动中,首先获取我们自定义的Binder,通过转型。

private DownloadService.DownloadBinder downloadBinder;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
downloadBinder = (DownloadService.DownloadBinder) service;
}

@Override
public void onServiceDisconnected(ComponentName name) {

}
};


然后启动服务和绑定服务,这一定都是需要的

Intent intent = new Intent(this, DownloadService.class);
startService(intent); //启动服务
bindService(intent, connection, BIND_AUTO_CREATE); //绑定服务


记得在onDestroy()取消绑定,启动服务保证服务一直在后台运行,绑定服务保证活动和服务通信。我开始一直不明白,为什么这app退出到手机界面还能下载,因为退出会取消绑定,后面才明白,取消绑定只是使活动和服务通信,但是服务开启后一直在运行,没有关闭(这里有点奇怪,按理应该在下载完成后调用stopSelf()),其实我觉得通信的意思,就是在活动中控制服务,虽然退出app后取消绑定,不能控制服务,但是前台通知服务还是一直在运行,所以就会出现即在下载,通知进度也在改变的现象。

因为郭神提供的那个url地址好像放在github上,下载特别慢,我改了个

@Override
public void onClick(View v) {
if(downloadBinder==null){
return;
}
switch (v.getId()){
case R.id.start_download:
String url = "http://m.g.shouji.360tpcdn.com/170112/737a726496e525b0c17deb16073a8be2/com.supercell.clashroyale.qihoo_131.apk";
//                String url = "https://raw.githubusercontent.com/guolindev/eclipse/master/eclipse-inst-win64.exe";
downloadBinder.startDownload(url);
break;
case R.id.pause_download:
if(flag%2==0){
pauseDownload.setText("暂停下载");
}else{
pauseDownload.setText("继续下载");
}
flag++;
downloadBinder.pauseDownload();
break;
case R.id.cancel_download:
downloadBinder.cancelDownload();
break;
}

}


4. 源码地址

地址下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: