首先我们来理解一下多线程断点下载的原理。打个比方,我们要下载从0到9的10个字节长度的文件,我们假设使用3个线程,那么就需要规定这三个线程分别从哪开始下载到那个位置的字节,一般采用 字节长度/线程数 来规定每个线程需要下载的字节,特别地,因为除法不一定能除尽,所以对于最后一个线程,往往都要下载多一点的字节数。上例中线程1从0下载到2,线程2从3下载到5,线程3从6到9。现在我们再讲讲什么是断点下载,我们在下载文件中,可能因为断电等中断下载的行为,那么当我们重新下载时,肯定希望从上次下载的地方继续往后下载。如何实现断点下载呢?原理上很简单,只要将下载时每个线程下载的位置记录下来,下次下载时先读取有没有记录下来的位置,如果有,那么就从记录的位置开始下载。
那么我们现在来具体实现一下,如何在Android中实现这个功能。步骤如下:
-
获取下载文件的长度,并且在本地生成一个和该文件大小一样的临时文件
-
根据文件长度和需要使用的线程数,来分配每个线程所需要下载的文件大小
-
开启多个线程,每一个线程从对应的位置开始下载,并且将位置记录下来
-
当每个线程都下载完成时,则该文件已经从服务器上下载到本地了,将记录位置的数据删掉。
-
当在下载完成之前停止下载,则下载下载开始时,先读取记录位置的文件,然后重复步骤3、4
由于Android提供了sqlite数据库,我们就利用该数据库进行线程和下载位置的记录和存取。因为这比通过文件来得方便和快捷。
Android界面很简单,我们只需要一个文本框和一个“下载”按钮,当点击按钮,实现下载文本框中的http地址指向的文件。
URL url = new URL(DownPath); //DownPath为http下载地址
HttpURLConnection connection = (HttpURLConnection) url .openConnection();
connection.setConnectTimeout(5000); //设置超时时间
connection.setRequestMethod("GET");//设置请求方式
int code = connection.getResponseCode(); //返回的请求码
if (code == 200) {
// 服务器文件的长度
int length = connection.getContentLength();
// 在客户端创建一个和服务器文件大小一样的文件
RandomAccessFile accessFile = new RandomAccessFile("/sdcard/setup.exe", "rwd");
accessFile.setLength(length);// 指定创建文件的长度
accessFile.close(); //随手关闭文件是个好习惯
// 假设有5个线程进行下载,每个线程平均下载的内容为blockSize
int brockSize = length / ThreadCount;
for (int i = 1; i <= ThreadCount; i++) {
int startIndex = (i - 1) * brockSize; //文件下载起始位置
int endIndex = i * brockSize - 1; //文件下载末位置
// 最后一个线程
if (i == ThreadCount) {
endIndex = length;
}
//创建一个内部类,变量context,传入的线程id,线程下载的起始位置,线程下载末位置
new DownloadThread(i, startIndex, endIndex, DownPath, context).start();
}
再看内部类之前,我们先讲讲数据库的实现
在创建数据库之前,我们先创建一个类,该类有两个私有变量,
private int ThreadId;//线程的id
private int Download;//线程已经下载到的位置
并实现该类有参无参的构造方法和Getters和Setters的方法。
现在我们可以创建数据库了。。。。下面就是创建数据的代码:
public class Database extends SQLiteOpenHelper {
//变量声明final类型,不能修改,可以避免再次启动程序时创建数据库,并提示数据库已存在
private static final String name = "DownloadTemp.db";
private static final int version = 2;
public Database(Context context) {
super(context, name, null, version);
// TODO Auto-generated constructor stub
}
@Override
public void onCreate(SQLiteDatabase db) {
//创建downtemp表,里面有_id属性,表示线程id,download,表示下载的位置
db.execSQL("create table downtemp(_id integer primary key,download integer)"); }
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO Auto-generated method stub
}
}
那么我们接下来要做的就是对这个downtemp表的增删改查了,具体代码如下:
public class downtempDAO {
private SQLiteDatabase sqLiteDatabase;
private Database database;
public downtempDAO(Context context) {
database = new Database(context);
}
// 往暂存记录表中记录线程的id,和下载的位置
public void add(DownTemp downTemp) {
sqLiteDatabase = database.getWritableDatabase();
sqLiteDatabase.execSQL(
"insert into downtemp(_id,download) values(?,?)", new Object[] {
downTemp.getThreadId(), downTemp.getDownload() });
}
// 删除暂存记录表中的线程所下载的信息
public void delete(int id) {
sqLiteDatabase = database.getWritableDatabase();
sqLiteDatabase.execSQL("delete from downtemp where _id = ?",
new Object[] { id });
}
// 更新线程下载的信息
public void update(DownTemp downTemp) {
sqLiteDatabase = database.getWritableDatabase();
sqLiteDatabase
.execSQL(
"update downtemp set download=? where _id=?",
new Object[] { downTemp.getDownload(),
downTemp.getThreadId() });
}
// 判断暂存记录表中是否有线程
public boolean getCount(int id) {
sqLiteDatabase = database.getWritableDatabase();
Cursor cursor = null;
try {
cursor = sqLiteDatabase.rawQuery(
"select * from downtemp where _id=?",
new String[] { String.valueOf(id) });
// 判断数据库中有无数据
if (!cursor.moveToNext()) {
return false;
}
return true;
} finally {
// 避免数据库泄露,一定要关闭cursor
cursor.close();
}
}
// 查找暂存记录表中线程的数据
public DownTemp find(int id) {
sqLiteDatabase = database.getWritableDatabase();
Cursor cursor =null;
try{
cursor=sqLiteDatabase.rawQuery(
"select * from downtemp where _id=?",
new String[] { String.valueOf(id) });
if (cursor.moveToNext()) {
int id1 = cursor.getInt(cursor.getColumnIndex("_id"));
int Download = cursor.getInt(cursor.getColumnIndex("download"));
System.out.println("线程" + id1 + "的Download:" + Download);
return new DownTemp(id1, Download);
}
return null;
}finally{
cursor.close();// 避免数据库泄露,一定要关闭cursor
}
}
}
现在我们终于可以写这个内部类,内部类具体实现的就是在下载的同时记录下载的位置,来看看代码吧:
public class DownloadThread extends Thread {
private int Threadid; //线程id
private int startIndex;//线程开始下载的位置
private int endIndex;//线程末下载位置
private String path; //下载的路径
private Context context; //上下文
private downtempDAO downtempDAO; //记录线程和下载位置的数据库
private DownTemp downTemp; //该类有线程id和下载位置两个变量
public DownloadThread(int threadid, int startIndex, int endIndex,
String path, Context context) {
Threadid = threadid;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.path = path;
this.context = context;
downtempDAO = new downtempDAO(context);
}
@Override
public void run() {
try {
// 查找数据库文件中是否有之前下过文件所记录的下载的位置
if (downtempDAO.getCount(Threadid)) {
// 将查找到的位置记录为开始下载的位置
System.out.println(downtempDAO.getCount(Threadid));
//查找该线程id在数据库中的数据
downTemp = downtempDAO.find(Threadid);
//得到该线程下载的位置
int DownloadIndex = downTemp.getDownload();
//如果有之前的下载位置记录,那么startIndex就变化了
startIndex = DownloadIndex;
}
System.out.println("线程" + Threadid + "从" + startIndex + "开始下载");
URL url = new URL(path);
HttpURLConnection connection = (HttpURLConnection) url
.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
// 重要:请求服务器下载部分文件的下载位置
connection.setRequestProperty("Range", "bytes=" + startIndex
+ "-" + endIndex);
int code = connection.getResponseCode();// 从服务器请求全部资源,返回码为200,请求部分源码为206
if (code == 206) {
InputStream inputStream = connection.getInputStream();
RandomAccessFile accessFile = new RandomAccessFile(
"/sdcard/setup.exe", "rwd"); //rwd的含义是将数据不经缓存直接存进硬盘中
accessFile.seek(startIndex);// 定位文件
// inputStream.available();此方法只能适用于本地文件的读取,但是用于网络通讯、下载时,
// 由于服务器发送数据是间断性的,一串字节往往分几批发送,
// 本地程序调用available()方法有时会得到0,这可能是对方还没有响应,
// 也可能是对方响应了,但是数据还没有送达本地。例如对方发送了1000个
// 字节给你,也许分3批到达,这你就要用调用3次available()方法才能将数据总数全部得到。
//所以我们采用直接给byte数组赋固定的值比较简单
byte[] bs = new byte[1024 * 1024 * 1 / 5];
int leng = 0;
int total = 0;
while ((leng = inputStream.read(bs)) != -1) {
accessFile.write(bs, 0, leng);// 写文件到accessFile
total += leng;
downTemp = new DownTemp(Threadid, total + startIndex);//下载的内容加上开始的位置,就是现在的下载位置
// 将线程id和下载文件到的位置存储在数据库中
// 如果数据库之前没有下载文件位置的记录,则使用add方法,否则使用update方法
if (!downtempDAO.getCount(Threadid)) {
downtempDAO.add(downTemp);
} else {
downtempDAO.update(downTemp);
}
}
inputStream.close();
accessFile.close();//随手关闭文件永远不会错
System.out.println("线程" + Threadid + "下载完毕了.....");
} else {
System.out.println("线程" + Threadid + "下载失败了*****");
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
// 下载完成之后清除记录下载位置的文件
runningThread--;
System.out.println("runningThread的值:" + runningThread);
if (runningThread == 0) {
for (int i = 1; i <= ThreadCount; i++) {
downtempDAO.delete(i);
System.out.println("关闭" + i);
Message message = handler.obtainMessage();
message.what = Download_Successful;
handler.sendMessage(message); //将信息传递给主线程,告知下载完成。
}
}
}
}
}