多线程下载是加快下载速度的一种方式,通过开启多个线程去执行一个任务,可以使任务的执行速度变快。多线程的任务下载时常都会使用得到断点续传下载,就是我们在一次下载未结束时退出下载,第二次下载时会接着第一次下载的进度继续下载。对于android中的下载,我想分多个部分去讲解分析。今天,我们就首先开始android中下载断点续传代码的实现。源码下载:java多线程断点续传(一) 。关于多线程下载单个文件的实现,请参见博客:android程序---->android多线程下载(二)
目录导航
一、 断点续传的实现步骤:
第一步: 我们要获得下载资源的的长度,用http请求中HttpURLConnection的getContentLength()方法
第二步:在本地创建一个文件,设计其长度。File file = new File()
第三步:从数据库中获得上次下载的进度,当暂停下载时,存储下载的状态,用到数据库的知识
第四步:从上次下载的位置下载数据,同时保存进度到数据库:RandomAccessFile的seek方法与HttpURLConnection的setRequestProperty方法
第五步:将下载进度回传到Activity,可以通过Intent将数据广播到Activity中
第六步:下载完成后删除下载信息,在数据库中删除相应的信息
二、 断点续传实现的流程图:
明白了上述的实现流程,现在我们开始一个android项目,开始断点续传代码的编写,项目结构如下:
运行的截图如下:
一、 编写基本的UI,三个TextView,分别显示文件名、下载进度和下载速度,一个ProgressBar。二个Button,分别用于开始下载、暂停下载和取消下载。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" 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.example.linux.continuedownload.MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:layout_marginLeft="80dp" android:id="@+id/progress" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <TextView android:layout_marginLeft="80dp" android:id="@+id/speed" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> <ProgressBar android:visibility="invisible" android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <Button android:id="@+id/start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="开始下载" /> <Button android:layout_marginLeft="20dp" android:id="@+id/stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="暂停下载" /> <Button android:layout_marginLeft="20dp" android:id="@+id/cancel" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="取消下载" /> </LinearLayout> </LinearLayout>
二、 在MainActivity中初始化一些组件,绑定按钮的事件:
在onCreate方法中初始化一些组件:
// 初始化组件 textView = (TextView) findViewById(R.id.textView); progressView = (TextView) findViewById(R.id.progress); speedView = (TextView) findViewById(R.id.speed); progressBar = (ProgressBar) findViewById(R.id.progressBar); progressBar.setMax(100); startButton = (Button) findViewById(R.id.start); stopButton = (Button) findViewById(R.id.stop); cancelButton = (Button) findViewById(R.id.cancel); // 创建一个文件信息对象 final FileInfo fileInfo = new FileInfo(0, fileUrl, "huhx.apk", 0, 0);
在onCreate方法中绑定开始下载按钮事件:点击start按钮,设置进度条可见,并且设置start的Action,启动服务。
startButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { textView.setText(fileInfo.getFileName()); progressBar.setVisibility(View.VISIBLE); // 通过Intent传递参数给service Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_START); intent.putExtra("fileInfo", fileInfo); startService(intent); } });
在onCreate方法中绑定暂停下载按钮事件:点击stop按钮,设置stop的Action,启动服务。
stopButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 通过Intent传递参数给service Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_STOP); intent.putExtra("fileInfo", fileInfo); startService(intent); } });
在onCreate方法中绑定取消下载按钮事件:点击cancel按钮,设置cancel的Action,启动服务,之后更新UI。
cancelButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 通过Intent传递参数给service Intent intent = new Intent(MainActivity.this, DownloadService.class); intent.setAction(DownloadService.ACTION_CANCEL); intent.putExtra("fileInfo", fileInfo); startService(intent); // 更新textView和progressBar的显示UI textView.setText(""); progressBar.setVisibility(View.INVISIBLE); progressView.setText(""); speedView.setText(""); } });
注册广播,用于Service向Activity传递一些下载进度信息:
// 静态注册广播 IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(DownloadService.ACTION_UPDATE); registerReceiver(broadcastReceiver, intentFilter); /** * 更新UI */ BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) { int finished = intent.getIntExtra("finished", 0); int speed = intent.getIntExtra("speed", 0); Log.i("Main", finished + ""); progressBar.setProgress(finished); progressView.setText(finished + "%"); speedView.setText(speed + "KB/s"); } } };
三、 在AndroidManifest.xm文件中声明权限,定义服务
<service android:name="com.huhx.services.DownloadService" android:exported="true" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
二、 我们定义一些实体类,用于断点续传过程的信息的良好封装:
下载文件信息: 省略了get和set方法,以及toString和构造方法
public class FileInfo implements Serializable{ // 文件Id,用于标识文件 private int fileId; // 文件的下载地址 private String url; // 文件的名称 private String fileName; // 文件的长度,也就是大小 private int length; // 文件已经的下载量 private int finished; }
下载资源的线程信息:省略同上
public class ThreadInfo { // 线程ID private int threadId; // 下载资源的地址 private String url; //下载资源的开始处 private int start; //下载资源的结束处 private int end; //资源已经的下载量 private int finished; }
三、 我们开始数据库方面的编写,它用于存储更新线程的下载的进度信息
首先我们要创建一个数据库的工具类:
package com.huhx.util; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; /** * Created by huhx on 2016/4/9. */ public class SqliteDBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "download.db"; private static final int version = 1; private static final String CREATE_THREADINFO = "create table thread_info(_id integer primary key autoincrement, " + "thread_id integer, url text, start integer, end integer, finished integer)"; private static final String DROP_THREADINFO = "drop table if exists thread_info"; public SqliteDBHelper(Context context) { super(context, DB_NAME, null, version); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_THREADINFO); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP_THREADINFO); db.execSQL(CREATE_THREADINFO); } }
定义一个Dao接口,用于数据库对线程信息的CRUD操作:
/** * Created by Linux on 2016/4/9. */ public interface ThreadDao { // 插入线程信息 public void insertThread(ThreadInfo threadInfo); // 删除线程信息 public void deleteThread(String url, int threadId); // 删除所有关于这个url的线程 public void deleteThread(String url); // 更新线程信息 public void updateThread(String url, int threadId, int finished); // 查询线程信息 public List<ThreadInfo> queryThread(String url); // 线程信息是否存在 public boolean isThreadInfoExist(String url, int threadId); }
具体实现上述Dao的Impl类:
package com.huhx.util; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import com.huhx.model.ThreadInfo; import java.util.ArrayList; import java.util.List; /** * Created by huhx on 2016/4/9. */ public class ThreadDaoImpl implements ThreadDao { private SqliteDBHelper sqliteDBHelper; public ThreadDaoImpl(Context context) { sqliteDBHelper = new SqliteDBHelper(context); } @Override public void insertThread(ThreadInfo threadInfo) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ threadInfo.getThreadId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished() }; database.execSQL("insert into thread_info(thread_id, url, start, end, finished) values(?,?,?,?,?)", objects); database.close(); } @Override public void deleteThread(String url, int threadId) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ url, threadId }; database.execSQL("delete from thread_info where url = ? and thread_id = ?", objects); database.close(); } @Override public void deleteThread(String url) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ url }; database.execSQL("delete from thread_info where url = ?", objects); database.close(); } @Override public void updateThread(String url, int threadId, int finished) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Object[] objects = new Object[]{ finished, url, threadId }; database.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?", objects); database.close(); } @Override public List<ThreadInfo> queryThread(String url) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); List<ThreadInfo> threadInfos = new ArrayList<>(); Cursor cursor = database.rawQuery("select * from thread_info where url = ?", new String[]{url}); while (cursor.moveToNext()) { ThreadInfo threadInfo = new ThreadInfo(); threadInfo.setThreadId(cursor.getInt(cursor.getColumnIndex("thread_id"))); threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url"))); threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start"))); threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex("end"))); threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished"))); threadInfos.add(threadInfo); } cursor.close(); database.close(); return threadInfos; } @Override public boolean isThreadInfoExist(String url, int threadId) { SQLiteDatabase database = sqliteDBHelper.getWritableDatabase(); Cursor cursor = database.rawQuery("select * from thread_info where url = ? and thread_id = ?", new String[]{url, threadId+""}); boolean isExist = cursor.moveToNext(); cursor.close(); database.close(); return isExist; } }
四、 最后我们开始最重要的Service以及核心的下载代码的编写,我们按照上述的开始、暂停、取消的顺序,来讲解断点续传的实现过程。
我们在DownloadService中onStartCommand方法中接收的Intent,关于Service的使用请参见:android基础---->service的生命周期
@Override public int onStartCommand(Intent intent, int flags, int startId) { // 获得Activity传过来的参数 if (ACTION_START.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); // 启动初始化线程 new InitThread(fileInfo).start(); } else if (ACTION_STOP.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); if (downloadTask != null) { downloadTask.isPause = true; } } else if (ACTION_CANCEL.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); if (downloadTask != null) { downloadTask.isPause = true; } // 删除本地文件 File file = new File(DOWNLOAD_PATH, fileInfo.getFileName()); if (file.exists()) { file.delete(); } handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget(); } return super.onStartCommand(intent, flags, startId); }
五、 文件的开始下载流程:
开始下载时,启动一个初始化线程,并把文件信息传递给线程,该线程通过Http请求得到文件的长度,在本地创建下载文件的载体,设置大小并发送下载的消息给Handler:
/** * 初始化子线程 */ class InitThread extends Thread { private FileInfo fileInfo = null; public InitThread(FileInfo fileInfo) { this.fileInfo = fileInfo; } @Override public void run() { // 连接网络文件 HttpURLConnection connection = null; RandomAccessFile randomAccessFile = null; try { URL url = new URL(fileInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(3000); connection.setRequestMethod("GET"); connection.connect(); int length = -1; if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { // 获取文件的长度 length = connection.getContentLength(); } if (length <= 0) { return; } // 在本地创建文件 File dir = new File(DOWNLOAD_PATH); if (dir.exists()) { dir.mkdir(); } File file = new File(dir, fileInfo.getFileName()); // 设置文件长度 randomAccessFile = new RandomAccessFile(file, "rwd"); randomAccessFile.setLength(length); fileInfo.setLength(length); handler.obtainMessage(DOWNLOAD_MESSAGE, fileInfo).sendToTarget(); } catch (Exception e) { e.printStackTrace(); } finally { try { randomAccessFile.close(); connection.disconnect(); } catch (IOException e) { e.printStackTrace(); } } } }
handler接收消息,并加以处理:注意这里有两种消息,我们暂时只考虑DOWNLOAD_MESSAGE消息,它启动下载任务
private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case DOWNLOAD_MESSAGE: FileInfo fileInfo = (FileInfo) msg.obj; // 启动下载任务 downloadTask = new DownloadTask(DownloadService.this, fileInfo); downloadTask.download(); break; case DOWNLOAD_CANCEL: FileInfo fileCancelInfo = (FileInfo) msg.obj; downloadTask = new DownloadTask(DownloadService.this); downloadTask.cancelDownload(fileCancelInfo); break; } } };
在download方法中,首先判断是否有线程下载过文件,如果没有就创建一个。有的话,从数据库直接得到。而且开启了下载的任务线程
public void download() { // 读取数据库的线程信息 List<ThreadInfo> threadInfos = threadDao.queryThread(fileInfo.getUrl()); ThreadInfo threadInfo = null; if (threadInfos.size() == 0) { threadInfo = new ThreadInfo(0, fileInfo.getUrl(), 0, fileInfo.getLength(), 0); } else { threadInfo = threadInfos.get(0); } new DownloadThread(threadInfo).start(); }
在下载的线程中,通过Http请求数据并通过字节流的方式存储在本地的文件中。间隔500毫秒,就发送一次更新UI的广播。如果收到了暂停的信号,就暂停下载。在下载完成之后,删除数据库中的线程信息
class DownloadThread extends Thread { private ThreadInfo threadInfo = null; public DownloadThread(ThreadInfo threadInfo) { this.threadInfo = threadInfo; } @Override public void run() { // 向数据库插入线程信息 if (!threadDao.isThreadInfoExist(threadInfo.getUrl(), threadInfo.getThreadId())) { threadDao.insertThread(threadInfo); } HttpURLConnection connection = null; RandomAccessFile randomAccessFile = null; InputStream inputStream = null; try { URL url = new URL(threadInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.setRequestMethod("GET"); int start = threadInfo.getStart() + threadInfo.getFinished(); connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd()); File file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFileName()); randomAccessFile = new RandomAccessFile(file, "rwd"); randomAccessFile.seek(start); Intent intent = new Intent(DownloadService.ACTION_UPDATE); // 开始下载 finished += threadInfo.getFinished(); if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { inputStream = connection.getInputStream(); byte[] buffer = new byte[4 * 1024]; int len = -1; long time = System.currentTimeMillis(); long time1; while ((len = inputStream.read(buffer)) != -1) { randomAccessFile.write(buffer, 0, len); finished += len; if ((time1 = System.currentTimeMillis() - time) > 500) { time = System.currentTimeMillis(); intent.putExtra("finished", finished * 100 / fileInfo.getLength()); intent.putExtra("speed", (int) (len / time1)); context.sendBroadcast(intent); } if (isPause) { threadDao.updateThread(threadInfo.getUrl(), threadInfo.getThreadId(), finished); return; } } // 删除线程信息,再次发送广播避免上面的广播延迟 intent.putExtra("finished", finished * 100 / fileInfo.getLength()); context.sendBroadcast(intent); threadDao.deleteThread(threadInfo.getUrl(), threadInfo.getThreadId()); Log.i("Main", "finished: " + finished + ", and file length: " + fileInfo.getLength()); } } catch (Exception e) { e.printStackTrace(); } finally { try { connection.disconnect(); randomAccessFile.close(); inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
六、 文件的暂停下载流程:如果下载任务在启动,那么设置isPause为true,在上述的讲解中我们知道,此时字节流停止的传输。
if (downloadTask != null) { downloadTask.isPause = true; }
七、 文件的取消下载流程:
暂停下载的流程,然后删除本地文件,最后发送取消下载的消息:
FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); if (downloadTask != null) { downloadTask.isPause = true; } // 删除本地文件 File file = new File(DOWNLOAD_PATH, fileInfo.getFileName()); if (file.exists()) { file.delete(); } handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();
handler处理取消下载的消息:调用DownloadTask的cancelDownload方法,并把文件信息传入
case DOWNLOAD_CANCEL: FileInfo fileCancelInfo = (FileInfo) msg.obj; downloadTask = new DownloadTask(DownloadService.this); downloadTask.cancelDownload(fileCancelInfo); break;
在cancelDownload方法中删除数据库中的线程信息:
// 取消下载任务 public void cancelDownload(FileInfo fileInfo) { threadDao.deleteThread(fileInfo.getUrl()); }
最后在MainActivity中更新UI:
// 更新textView和progressBar的显示UI textView.setText(""); progressBar.setVisibility(View.INVISIBLE); progressView.setText(""); speedView.setText("");
关于android中多线程的下载,请参见我的博客: android程序---->android多线程下载(二)