您的位置:首页 > 移动开发 > Android开发

Android 实现文件的单线程断点续传下载

2017-01-09 20:45 375 查看

1.实际效果:





2.代码实现

完成这个小项目需要:

基础网络知识(Http)

了解android界面处理机制

Service的绑定与解绑

BroadCastReceiver的注册与消息的处理

本地文件的I/O处理

数据库基础

事件回调原理

在这里我采用了数据库框架GreenDao,方便实现想要的效果,自己独立写几个类来操作SQLite数据库也是可以的。关于怎么使用GreenDao,这里不做叙述了,网上一大堆教程,这里给个教程链接http://m.blog.csdn.net/article/details?id=51893092

下面是项目框架



主要有服务类DownloadService,广播类ProgressReceiver,下载类DownloadTask,线程信息类ThreadInfo,和MainActivity与App(继承Application,用于初始化数据库)。

首先来看看我的布局文件acitivty_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.chen.capton.filedownload.MainActivity">

<TextView
android:id="@+id/info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="文件信息:"
android:layout_alignEnd="@+id/pause" />

<ProgressBar
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/info"
android:layout_alignParentStart="true"
android:id="@+id/progressBar" />

<Button
android:text="开始"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/progressBar"
android:layout_alignParentStart="true"
android:id="@+id/start" />

<Button
android:text="暂停"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:enabled="false"
android:id="@+id/pause"
android:layout_below="@+id/progressBar"
android:layout_toEndOf="@+id/start" />

<TextView
android:text="进度"
android:gravity="right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:id="@+id/progressText"
android:layout_alignParentEnd="true"
android:layout_toEndOf="@+id/info" />
</RelativeLayout>


配置文件menifests.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.chen.capton.filedownload">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
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">
</service>
</application>

</manifest>


最主要的MainActivity:

洋洋洒洒100多行

package com.chen.capton.filedownload;

import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {

private TextView fileInfoText,progressText;
private Button startBtn,pauseBtn;
private ProgressBar mProgressBar;
private ProgressReceiver mReceiver;
private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //设置Action,与ProgressReceiver,DownloadService中的Action都一致
private final String url="http://192.168.1.103/app.zip";  //设置url
private final int maxProgress=100;                        //设置进度条最大进度
private boolean isContinue;                               //是否暂停的标识
private Handler handler=new Handler(){
public void handleMessage(Message msg){
mProgressBar.setProgress(msg.what);//由于发送的是空消息,直接用What作为进度参数使用
progressText.setText("完成度:"+msg.what+"%");
}
}; //用于界面更新的handler。将它作为参数传递至ProgressReceiver,供其发送Message,然后根据Message更新进度
private DownloadService mService;
private ServiceConnection conn=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//获取绑定好的DownloadService对象
mService= ((DownloadService.MyBinder)service).getService();
}
@Override
public void onServiceDisconnected(ComponentName name) {}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

initComponent(); //初始化ProgressReceiver,DownloadService

setContentView(R.layout.activity_main);

initView(); //初始化视图控件
setListener(); //设置点击事件
}

private void initView() {
fileInfoText= (TextView) findViewById(R.id.info);
progressText= (TextView) findViewById(R.id.progressText);
startBtn= (Button) findViewById(R.id.start);
pauseBtn= (Button) findViewById(R.id.pause);
mProgressBar= (ProgressBar) findViewById(R.id.progressBar);
mProgressBar.setMax(maxProgress);
}

/*
* 初始化ProgressReceiver,DownloadService
* 绑定DownloadService,注册ProgressReceiver
* */
private void initComponent() {
Intent intent=new Intent(this,DownloadService.class);
bindService(intent,conn,BIND_AUTO_CREATE);

mReceiver=new ProgressReceiver(handler);
IntentFilter filter=new IntentFilter();
filter.addAction(REFRESH_PROGRESS);
registerReceiver(mReceiver,filter);
}

private void setListener() {
startBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(!isContinue) {
mService.startMission(url,maxProgress);
v.setEnabled(false);
pauseBtn.setEnabled(true);
fileInfoText.setText(getFileName(url));
}else {
mService.continueMission();
v.setEnabled(false);
pauseBtn.setEnabled(true);
}
}
});
pauseBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mService.pauseMission();
v.setEnabled(false);
startBtn.setEnabled(true);
isContinue=true;
startBtn.setText("继续");
}
});
}

/*
* 获取文件名
* */
private String getFileName(String url){
int start=url.lastIndexOf("/")+1;
int end=url.length();
return url.substring(start,end);
}

@Override
protected void onDestroy() {
/*
* 解绑定DownloadService,注销ProgressReceiver
* */
unregisterReceiver(mReceiver);
unbindService(conn);
super.onDestroy();
}

}


用于初始化数据库的App类:

当然也可以在MainActivity中初始化数据库,为了代码简洁和更快地初始化数据库,就写着这里了。

package com.chen.capton.filedownload;

import android.app.Application;
import android.database.sqlite.SQLiteDatabase;

/**
* Created by CAPTON on 2017/1/9.
*/

public class App extends Application {

public static App instances;
@Override
public void onCreate() {
super.onCreate();
setDatabase();
instances = this;
}
public static App getInstances(){
return instances;
}
/**
* 设置greenDao
*/
private DaoMaster.DevOpenHelper mHelper;
private SQLiteDatabase db;
private DaoMaster mDaoMaster;
private DaoSession mDaoSession;
private void setDatabase() {
mHelper = new DaoMaster.DevOpenHelper(this, "Dishes-db", null);
db = mHelper.getWritableDatabase();
mDaoMaster = new DaoMaster(db);
mDaoSession = mDaoMaster.newSession();
}
public DaoSession getDaoSession() {
return mDaoSession;
}
public SQLiteDatabase getDb() {
return db;
}

}


接下来就是贴各个类的代码了

服务类DownloadService:

package com.chen.capton.filedownload;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;

/*
* 实现DownloadTask.RefreshProgressListener接口
* */
public class DownloadService extends Service implements DownloadTask.RefreshProgressListener{
private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //设置Action
private DownloadTask task;
private Intent intent;
@Override
public IBinder onBind(Intent intent) {
return new MyBinder();
}

class MyBinder extends Binder {
public DownloadService getService(){
intent=new Intent();
intent.setAction(REFRESH_PROGRESS);
return DownloadService.this;
}
}

/*
* 供DownloadTask回调的方法,用于发送刷新进度的广播
* */
@Override
public void refressProgress(int progress) {
intent.putExtra("progress",progress); //将进度值写入intent
sendBroadcast(intent);  //发送广播
}

public void startMission(String url,int maxProgress){
task=new DownloadTask(url,maxProgress);
task.setRefreshProgressListener(this);
task.startMission();
};
public void pauseMission(){
task.pauseMission();
}
public void continueMission(){
task.continueMission();
}
}


广播类ProgressReceiver:

package com.chen.capton.filedownload;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;

/**
* Created by CAPTON on 2017/1/9.
*/

public class ProgressReceiver extends BroadcastReceiver {
private Handler handler;  //保存从MainActivity传来的handler;
private final String REFRESH_PROGRESS="REFRESH_PROGRESS";//设置Action
public ProgressReceiver(Handler handler) {
this.handler=handler;
}

@Override
public void onReceive(Context context, Intent intent) {
//判断Action是否一致
if(intent.getAction().equals(REFRESH_PROGRESS)){
//从传来的intent中获取进度值
int progress=intent.getIntExtra("progress",0);
//将带有进度值的intent发送出去,交与MainActivity中的handler处理
handler.sendEmptyMessage(progress);
}
}
}


下载类(重点)DownloadTask:

package com.chen.capton.filedownload;

import android.os.Environment;
import android.util.Log;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

/**
* Created by CAPTON on 2017/1/9.
*/

public class DownloadTask {

private String url;           //下载地址
private ThreadInfo threadInfo;//线程信息
private ThreadInfoDao dao; //数据库入口对象
private DownloadThread thread; //下载线程
public boolean isPause;//是否断开连接的标志位,很关键,呵呵
private int maxProgress; //最大进度
public int filedLength;  //文件长度
public int finishedLength; //完成的文件长度(待保存的文件进度)
private RefreshProgressListener listener;//进度刷新的监听器用于调用Service的更新方法

public DownloadTask(String url,int maxProgress) {
this.url = url;
this.maxProgress=maxProgress;
threadInfo=new ThreadInfo();
DaoSession session=App.getInstances().getDaoSession();  //从App中获取初始化好的DaoSession对象
dao=session.getThreadInfoDao();
thread=new DownloadThread(url,threadInfo,maxProgress);
}

/*
* 开始下载线程
* */
public void startMission(){
thread.start();
}

/*
* 暂停任务,即跳出while循环,将线程信息保存到数据库
* */
public void pauseMission(){
isPause=true;
ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique();
//第一次保存进度时插入纪录,之后更新纪录即可
if(info==null) {
info=new ThreadInfo(null, filedLength, finishedLength);
dao.insert(info);
}else {
info.setFileLength(filedLength);
info.setFinishedLength(finishedLength);
dao.update(info);
}
}
/*
*  从数据库读取上次保存的线程信息,新建线程从指定位置下载剩下的部分
* */
public void continueMission(){
isPause=false;
ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique();
if(info!=null){
thread=new DownloadThread(url,info,maxProgress);
thread.start();
}
}

/*
* 下载线程,核心
* */
class DownloadThread extends Thread{
private String url;
private ThreadInfo threadInfo;
private HttpURLConnection conn; //httpUrl连接
private InputStream is;   //输入流
private File fileDir; //建立一个文件夹存放文件
private File file;
private RandomAccessFile raFile;  //可随机读写的File类,实际又不是继承File,呵呵,断点续传必用。
private int maxProgress;

public DownloadThread(String url, ThreadInfo threadInfo, int maxProgress) {
this.url = url;
this.threadInfo=threadInfo;
this.maxProgress=maxProgress;
fileDir=new File(Environment.getExternalStorageDirectory(),"test");
if(!fileDir.exists()){
fileDir.mkdir(); //第一次下载时,应该没有text目录,新建一个
}
file=new File(fileDir,getFileName(url));
try {
raFile=new RandomAccessFile(file,"rw"); //设置文件读写模式,"rw"为可读可写
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}

public void run(){
/*
* 第一次连接,先获取文件长度,供后续设置文件的传输范围(byte)
* */
try {
URL Url = new URL(url);
HttpURLConnection conn= (HttpURLConnection) Url.openConnection();
conn.setRequestMethod("GET");
conn.setReadTimeout(3000);
filedLength=conn.getContentLength();
threadInfo.setFileLength(filedLength);
conn.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
/*
* 第二次连接,根据文件长度,上次保存的进度,设置传输范围,建立连接开始下载
* */
try {
URL Url=new URL(url);
conn= (HttpURLConnection) Url.openConnection();
conn.setRequestMethod("GET");  //设置连接方式
conn.setReadTimeout(5000);  //设置连接超时
//设置传输的范围,例如"bytes=0-1231540",‘-’后面没写说明结束端为传输文件的最后一字节
conn.setRequestProperty("Range","bytes="+threadInfo.getFinishedLength()+"-"+threadInfo.getFileLength());
conn.connect();
is=conn.getInputStream();  //从连接对象中获取输入流
//数据输入流,也可以用BufferedInputStream;
DataInputStream dis=new DataInputStream(is);
try {
//设置一定的延时,等待服务器响应报文
Thread.sleep(1800);
} catch (InterruptedException e) {
e.printStackTrace();
}

/*
* 根据响应码判断是否成功连接服务器
* */
if(conn.getResponseCode()==HttpURLConnection.HTTP_OK||
conn.getResponseCode()==HttpURLConnection.HTTP_PARTIAL) {
raFile.seek(threadInfo.getFinishedLength());//跳转至上一次暂停时保存的位置
byte[] b = new byte[1024];  //设置byte数组,大小适度即可;
int len;    //每次写入b中的实际字节数
long now=System.currentTimeMillis(); //设置循环初始时间
while ((len = dis.read(b)) != -1) {
raFile.write(b, 0, len); //将保存在b中的数据写入文件
finishedLength += len;   //累加下载长度
/*
* 判断文件(下载)写入消耗的时间是否大于100ms,若是才跟新进度,不设置的话,
* 刷新频率=文件长度(很大的数)/1kb,会明显地限制传输速度
* */
if(System.currentTimeMillis()-now>=100) {
now=System.currentTimeMillis();
//根据公式算出实际进度大小,然后调用DownloadService的实现方法refressProgress(int progress);
listener.refressProgress((int) ((long) finishedLength * maxProgress / threadInfo.getFileLength()));
}else {
/*当文件(下载)写入消耗的时间小于100ms时,判断是否下载完成,若是则把进度设置为最大这个判断存在的意义在于,当文件下载完全时,消耗时间又小于100ms,进度显示为100%, 若不设置,则进度显示会卡在90%-100%之间,文件越小,显示误差越大*/
if (finishedLength>=threadInfo.getFileLength()){
listener.refressProgress(maxProgress);
}
}
if (isPause) {
break;
}
}
threadInfo.setFinishedLength(finishedLength); //保存下载信息
//写入完毕或者暂停则断开连接
is.close();
conn.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
};

/*
* 设置回调方法,和回调接口,让DownloadService实现接口用于刷新进度。
* */
public void setRefreshProgressListener(RefreshProgressListener listener){
this.listener=listener;
}
public interface RefreshProgressListener{
void refressProgress(int progress);
}
private String getFileName(String url){
int start=url.lastIndexOf("/")+1;
int end=url.length();
return url.substring(start,end);
}
}


线程信息类ThreadInfo:

package com.chen.capton.filedownload;

import org.greenrobot.greendao.annotation.Entity;
import org.greenrobot.greendao.annotation.Id;
import org.greenrobot.greendao.annotation.Generated;

/**
* Created by CAPTON on 2017/1/9.
*/

//声明此类是GreenDao框架的Entity实体类
@Entity
public class ThreadInfo {
//声明这是自增的唯一键
@Id
private Long id;
private int fileLength;
private int finishedLength;

/*
*用Android Studio 点击Build选项下的"Make Project",后面的代码会自动生成
*/
@Generated(hash = 956576157)
public ThreadInfo(Long id, int fileLength, int finishedLength) {
this.id = id;
this.fileLength = fileLength;
this.finishedLength = finishedLength;
}
@Generated(hash = 930225280)
public ThreadInfo() {
}
public Long getId() {
return this.id;
}
public void setId(Long id) {
this.id = id;
}
public int getFileLength() {
return this.fileLength;
}
public void setFileLength(int fileLength) {
this.fileLength = fileLength;
}
public int getFinishedLength() {
return this.finishedLength;
}
public void setFinishedLength(int finishedLength) {
this.finishedLength = finishedLength;
}
}


代码就全部贴完了,至于思路是怎么屡清楚的,主要看你对各个知识点掌握的熟练程度。

3.设计思路

写这个小demo,我的思路是:

❶,先要明确目标:文件的单线程断点续传,首先单线程先不管,后面还有多线程呢,断点续传是重点,说道断点续传你就应该明白要保存暂停时的进度了,保存进度需要用什么途径?SharePreference,SQLite(这里采用的途径),文件保存?如果是单线程,只需要保存一个进度信息,SharePreference是最方便了,把文件长度,已下载长度两个参数写进xml文件里就行了,需要续传文件时从xml里读就行了,当然这只是最简单的情况;如果是多线程下载时,比如10个线程,你就需要至少20个命名空间来保存参数,这样很麻烦,读写信息都很麻烦,不如数据库来的方便了。至于用文件存储信息就不要去想了,更麻烦,可以自行琢磨。

❷,信息的保存方式确定了,接下来是考虑如何下载文件了,当然不要去用什么框架来下载了,自己手写,从http连接开始写,到文件写入,关闭http连接为止,都自己写。这里Http连接用到的是HttpUrlConnection,也可以用HttpClient。文件的来源弄懂了,然后是文件输出,断点续传要用到RandomAccessFile这个类,可以实现文件的随机位置的读写,当然这个类并不是继承File类,而是实现 DataOutput, DataInput, Closeable这几个接口,所以我们在导入数据的时候用DataInputStream比较好,BufferdInputStream也是可以的。

❸文件如何下载写入和保存搞定了,接下来是进度刷新的问题,这个就涉及到Service,BroadCastReceiver,Handler这三大块的知识了,把这些基础知识先掌握好。其实我刚开始学的时候没用Service,BroadCastReceiver,直接在MainActivity里写,当然代码量就吓人了,结构也看起来很复杂,不过感觉下载速度确实最快(估计是Service不用一直发送消息给BroadCastReceiver,发送一个消息虽然很快,但是进度是不断刷新的,积少成多,我们的设备要处理的消息就多了,效率就下降了,从而影响文件传输速度了)。

❹各大类的调用关系

(1)MainActivity(点击”开始”,”暂停”按钮)调用DownloadService内的方法

(2)DownloadService继续调用DownloadTask的相应方法

(3)DownloadTask开始其中的下载线程,线程下载一定字节的后,回调DownloadService的refressProgress(int progress)方法

(4)refressProgress方法发送广播给ProgressReceiver,ProgressReceiver根据发来的信息通过Handler转发到MainActivity的Handler中

(5)MainActivity中的Handler收到最终消息,更新UI。

若是点击”暂停”按钮,“开始”按钮变为“继续”按钮,->(3)中跳出文件读取的循环,并把进度写入数据库,再次点击“继续”按钮续传文件,DownloadTask读取数据库数据,重新开始(3)(4)(5)。

前面的是大致思路,具体细节要深入到代码里去剖析,如果你是大神,余光一瞥就能理解每一行代码的用意;如果你对各个知识掌握的还不够熟练,可能就卡在某处了。几乎所有重要的方法和类我都一一注释了其用意了,剩下一些繁文缛节就没有叙述了,希望大家都能明白。

建议:

❶最好在个人电脑上构建一个局域网服务器,通过局域网来测试下载任务(不用流量),我用的是WAMP,简单粗暴,把文件丢进根目录下的“www”目录即可通过类似“http://192.168.1.10x/xxx.xxx“的url找到你的文件。建好服务器启动后发现手机无法访问地址,可以试试关闭电脑的防火墙(百试不厌),当然用完最好恢复回去。

❷文件最好选择一些设备需要检查其完整性的格式,如zip,rar,apk, 尝试打开文件来检验是否下载完整

❸手机最好已经root,方便查看数据库的信息(注意:GreenDao输出的数据库格式可能不是以db结尾(受初始化时的命名决定),手动改成”.db”结尾就可以打开了)。这里推荐“RE文件管理器”这款软件来查看root后的手机文件夹。当然不root的话,如果是数据出错,就手动写代码检查数据库喽。

4.相关文件

单线程断点续传 demo apk 链接:http://pan.baidu.com/s/1sltoHrB 密码:v4tj

完整项目demo 链接:http://pan.baidu.com/s/1kVBbTcj 密码:qcb7
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息