使用多线程断点续传下载器在下载的时候多个线程并发可以占用服务器端更多资源,从而加快下载速度,在下载过程中记录每个线程已拷贝数据的数量,如果下载中断,比如无信号断线、电量不足等情况下,这就需要使用到断点续传功能,下次启动时从记录位置继续下载,可避免重复部分的下载。这里采用数据库来记录下载的进度。
效果图
断点续传
1.断点续传需要在下载过程中记录每条线程的下载进度
2.每次下载开始之前先读取数据库,查询是否有未完成的记录,有就继续下载,没有则创建新记录插入数据库
3.在每次向文件中写入数据之后,在数据库中更新下载进度
4.下载完成之后删除数据库中下载记录
Handler传输数据
这个主要用来记录百分比,每下载一部分数据就通知主线程来记录时间
1.主线程中创建的View只能在主线程中修改,其他线程只能通过和主线程通信,在主线程中改变View数据
2.我们使用Handler可以处理这种需求
主线程中创建Handler,重写handleMessage()方法
新线程中使用Handler发送消息,主线程即可收到消息,并且执行handleMessage()方法
动态生成新View
可实现多任务下载
1.创建XML文件,将要生成的View配置好
2.获取系统服务LayoutInflater,用来生成新的View
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
3.使用inflate(int resource, ViewGroup root)方法生成新的View
4.调用当前页面中某个容器的addView,将新创建的View添加进来
示例
进度条样式 download.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" > <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_weight="1" > <!--进度条样式默认为圆形进度条,水平进度条需要配置style属性, ?android:attr/progressBarStyleHorizontal --> <ProgressBar android:layout_width="fill_parent" android:layout_height="20dp" style="?android:attr/progressBarStyleHorizontal" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="0%" /> </LinearLayout> <Button android:layout_width="40dp" android:layout_height="40dp" android:onClick="pause" android:text="||" /> </LinearLayout>
顶部样式 main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" android:id="@+id/root" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="请输入下载路径" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_marginBottom="30dp" > <EditText android:id="@+id/path" android:layout_width="fill_parent" android:layout_height="wrap_content" android:singleLine="true" android:layout_weight="1" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="下载" android:onClick="download" /> </LinearLayout> </LinearLayout>
MainActivity.Java
public class MainActivity extends Activity { private LayoutInflater inflater; private LinearLayout rootLinearLayout; private EditText pathEditText; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); //动态生成新View,获取系统服务LayoutInflater,用来生成新的View inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); rootLinearLayout = (LinearLayout) findViewById(R.id.root); pathEditText = (EditText) findViewById(R.id.path); // 窗体创建之后, 查询数据库是否有未完成任务, 如果有, 创建进度条等组件, 继续下载 List<String> list = new InfoDao(this).queryUndone(); for (String path : list) createDownload(path); } /** * 下载按钮 * @param view */ public void download(View view) { String path = "http://192.168.1.199:8080/14_Web/" + pathEditText.getText().toString(); createDownload(path); } /** * 动态生成新View * 初始化表单数据 * @param path */ private void createDownload(String path) { //获取系统服务LayoutInflater,用来生成新的View LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE); LinearLayout linearLayout = (LinearLayout) inflater.inflate(R.layout.download, null); LinearLayout childLinearLayout = (LinearLayout) linearLayout.getChildAt(0); ProgressBar progressBar = (ProgressBar) childLinearLayout.getChildAt(0); TextView textView = (TextView) childLinearLayout.getChildAt(1); Button button = (Button) linearLayout.getChildAt(1); try { button.setOnClickListener(new MyListener(progressBar, textView, path)); //调用当前页面中某个容器的addView,将新创建的View添加进来 rootLinearLayout.addView(linearLayout); } catch (Exception e) { e.printStackTrace(); } } private final class MyListener implements OnClickListener { private ProgressBar progressBar; private TextView textView; private int fileLen; private Downloader downloader; private String name; /** * 执行下载 * @param progressBar //进度条 * @param textView //百分比 * @param path //下载文件路径 */ public MyListener(ProgressBar progressBar, TextView textView, String path) { this.progressBar = progressBar; this.textView = textView; name = path.substring(path.lastIndexOf("/") + 1); downloader = new Downloader(getApplicationContext(), handler); try { downloader.download(path, 3); } catch (Exception e) { e.printStackTrace(); Toast.makeText(getApplicationContext(), "下载过程中出现异常", 0).show(); throw new RuntimeException(e); } } //Handler传输数据 private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 0: //获取文件的大小 fileLen = msg.getData().getInt("fileLen"); //设置进度条最大刻度:setMax() progressBar.setMax(fileLen); break; case 1: //获取当前下载的总量 int done = msg.getData().getInt("done"); //当前进度的百分比 textView.setText(name + " " + done * 100 / fileLen + "%"); //进度条设置当前进度:setProgress() progressBar.setProgress(done); if (done == fileLen) { Toast.makeText(getApplicationContext(), name + " 下载完成", 0).show(); //下载完成后退出进度条 rootLinearLayout.removeView((View) progressBar.getParent().getParent()); } break; } } }; /** * 暂停和继续下载 */ public void onClick(View v) { Button pauseButton = (Button) v; if ("||".equals(pauseButton.getText())) { downloader.pause(); pauseButton.setText("▶"); } else { downloader.resume(); pauseButton.setText("||"); } } } }
Downloader.java
public class Downloader { private int done; private InfoDao dao; private int fileLen; private Handler handler; private boolean isPause; public Downloader(Context context, Handler handler) { dao = new InfoDao(context); this.handler = handler; } /** * 多线程下载 * @param path 下载路径 * @param thCount 需要开启多少个线程 * @throws Exception */ public void download(String path, int thCount) throws Exception { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); //设置超时时间 conn.setConnectTimeout(3000); if (conn.getResponseCode() == 200) { fileLen = conn.getContentLength(); String name = path.substring(path.lastIndexOf("/") + 1); File file = new File(Environment.getExternalStorageDirectory(), name); RandomAccessFile raf = new RandomAccessFile(file, "rws"); raf.setLength(fileLen); raf.close(); //Handler发送消息,主线程接收消息,获取数据的长度 Message msg = new Message(); msg.what = 0; msg.getData().putInt("fileLen", fileLen); handler.sendMessage(msg); //计算每个线程下载的字节数 int partLen = (fileLen + thCount - 1) / thCount; for (int i = 0; i < thCount; i++) new DownloadThread(url, file, partLen, i).start(); } else { throw new IllegalArgumentException("404 path: " + path); } } private final class DownloadThread extends Thread { private URL url; private File file; private int partLen; private int id; public DownloadThread(URL url, File file, int partLen, int id) { this.url = url; this.file = file; this.partLen = partLen; this.id = id; } /** * 写入操作 */ public void run() { // 判断上次是否有未完成任务 Info info = dao.query(url.toString(), id); if (info != null) { // 如果有, 读取当前线程已下载量 done += info.getDone(); } else { // 如果没有, 则创建一个新记录存入 info = new Info(url.toString(), id, 0); dao.insert(info); } int start = id * partLen + info.getDone(); // 开始位置 += 已下载量 int end = (id + 1) * partLen - 1; try { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setReadTimeout(3000); //获取指定位置的数据,Range范围如果超出服务器上数据范围, 会以服务器数据末尾为准 conn.setRequestProperty("Range", "bytes=" + start + "-" + end); RandomAccessFile raf = new RandomAccessFile(file, "rws"); raf.seek(start); //开始读写数据 InputStream in = conn.getInputStream(); byte[] buf = new byte[1024 * 10]; int len; while ((len = in.read(buf)) != -1) { if (isPause) { //使用线程锁锁定该线程 synchronized (dao) { try { dao.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } raf.write(buf, 0, len); done += len; info.setDone(info.getDone() + len); // 记录每个线程已下载的数据量 dao.update(info); //新线程中用Handler发送消息,主线程接收消息 Message msg = new Message(); msg.what = 1; msg.getData().putInt("done", done); handler.sendMessage(msg); } in.close(); raf.close(); // 删除下载记录 dao.deleteAll(info.getPath(), fileLen); } catch (IOException e) { e.printStackTrace(); } } } //暂停下载 public void pause() { isPause = true; } //继续下载 public void resume() { isPause = false; //恢复所有线程 synchronized (dao) { dao.notifyAll(); } } }
Dao:
DBOpenHelper:
public class DBOpenHelper extends SQLiteOpenHelper { public DBOpenHelper(Context context) { super(context, "download.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("CREATE TABLE info(path VARCHAR(1024), thid INTEGER, done INTEGER, PRIMARY KEY(path, thid))"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
InfoDao:
public class InfoDao { private DBOpenHelper helper; public InfoDao(Context context) { helper = new DBOpenHelper(context); } public void insert(Info info) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("INSERT INTO info(path, thid, done) VALUES(?, ?, ?)", new Object[] { info.getPath(), info.getThid(), info.getDone() }); } public void delete(String path, int thid) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("DELETE FROM info WHERE path=? AND thid=?", new Object[] { path, thid }); } public void update(Info info) { SQLiteDatabase db = helper.getWritableDatabase(); db.execSQL("UPDATE info SET done=? WHERE path=? AND thid=?", new Object[] { info.getDone(), info.getPath(), info.getThid() }); } public Info query(String path, int thid) { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT path, thid, done FROM info WHERE path=? AND thid=?", new String[] { path, String.valueOf(thid) }); Info info = null; if (c.moveToNext()) info = new Info(c.getString(0), c.getInt(1), c.getInt(2)); c.close(); return info; } public void deleteAll(String path, int len) { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT SUM(done) FROM info WHERE path=?", new String[] { path }); if (c.moveToNext()) { int result = c.getInt(0); if (result == len) db.execSQL("DELETE FROM info WHERE path=? ", new Object[] { path }); } } public List<String> queryUndone() { SQLiteDatabase db = helper.getWritableDatabase(); Cursor c = db.rawQuery("SELECT DISTINCT path FROM info", null); List<String> pathList = new ArrayList<String>(); while (c.moveToNext()) pathList.add(c.getString(0)); c.close(); return pathList; } }