Android多线程断点下载的实现示例

首先我们来理解一下多线程断点下载的原理。打个比方,我们要下载从0到9的10个字节长度的文件,我们假设使用3个线程,那么就需要规定这三个线程分别从哪开始下载到那个位置的字节,一般采用  字节长度/线程数 来规定每个线程需要下载的字节,特别地,因为除法不一定能除尽,所以对于最后一个线程,往往都要下载多一点的字节数。上例中线程1从0下载到2,线程2从3下载到5,线程3从6到9。现在我们再讲讲什么是断点下载,我们在下载文件中,可能因为断电等中断下载的行为,那么当我们重新下载时,肯定希望从上次下载的地方继续往后下载。如何实现断点下载呢?原理上很简单,只要将下载时每个线程下载的位置记录下来,下次下载时先读取有没有记录下来的位置,如果有,那么就从记录的位置开始下载。

那么我们现在来具体实现一下,如何在Android中实现这个功能。步骤如下:

  1. 获取下载文件的长度,并且在本地生成一个和该文件大小一样的临时文件
  2. 根据文件长度和需要使用的线程数,来分配每个线程所需要下载的文件大小
  3. 开启多个线程,每一个线程从对应的位置开始下载,并且将位置记录下来
  4. 当每个线程都下载完成时,则该文件已经从服务器上下载到本地了,将记录位置的数据删掉。
  5. 当在下载完成之前停止下载,则下载下载开始时,先读取记录位置的文件,然后重复步骤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); //将信息传递给主线程,告知下载完成。
     }
    }
   }
  }
 }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值