Android实战:多线程断点续传下载器实现
2017-05-08 15:08
225 查看
前几天项目中用到多线程断点续传,看了一些资料,实现了该功能,未免再次用到时忘记,把过程记录下来。
说到多线程下载,也许大家会觉得很迷惑,但多线程的原理实际上与单线程下载的原理并无区别。
多线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢。
其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在java中就要用到上次提到的RandomAccessFile这个API,它的开始结束为止用以下代码确定:
最后就是断点续传了,只需要才程序停止下载的时候记录下最后的下载位置就好了,当下次下载的时候从当前停止的位置开始下载。
![](http://upload-images.jianshu.io/upload_images/1492901-8c7d9ddbc859afd7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
在这个项目中,我们运用的不再是单一的组件只是,而是将组件综合运用起来,如何在listView中操作,数据库如何增删改查,Service如何与Activity通信,Notification通知栏又是怎样显示的····
这些组件我们都刷了一遍,相信下次再次使用的时候就不会像刚开始一样无从下手了。
这个项目看上去貌似不错,但仔细思量仍是有种种的不足之处,还拥有一些BUG待解决。而且在Activity与Service之间的通信用BroadCast广播,虽然会更简单些,但对于真正的项目而已可能不是这样的。
因为广播是系统组件,这样大材小用是资源的浪费,而且效率是偏低的。在一个项目中的单线程多进程中,应该使用Handler加上Messenger进行通信的,这有待于大家学习。
说到多线程下载,也许大家会觉得很迷惑,但多线程的原理实际上与单线程下载的原理并无区别。
多线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢。
其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在java中就要用到上次提到的RandomAccessFile这个API,它的开始结束为止用以下代码确定:
connection.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.getEnd());
最后就是断点续传了,只需要才程序停止下载的时候记录下最后的下载位置就好了,当下次下载的时候从当前停止的位置开始下载。
重写布局
这次下载需要展示多个下载的文件,所以使用ListView控件,界面效果如下![](http://upload-images.jianshu.io/upload_images/1492901-8c7d9ddbc859afd7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
下载界面.png
activity_main.xml代码如下
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" > <ListView android:id="@+id/lv_downLoad" android:layout_width="match_parent" android:layout_height="match_parent" > </ListView> </RelativeLayout>[code]
item的布局:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:id="@+id/tv_fileName" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="imooc.apk" /> <ProgressBar android:id="@+id/pb_progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/tv_fileName"/> <Button android:id="@+id/btn_stop" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_below="@id/pb_progress" android:text="暂停" /> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@+id/pb_progress" android:layout_toLeftOf="@id/btn_stop" android:text="下载" /> </RelativeLayout>
建立FileAdapter类
public class FileListAdapter extends BaseAdapter { private Context mContext; private List<FileInfo> mList; private LayoutInflater inflater; public FileListAdapter(Context context, List<FileInfo> fileInfos) { this.mContext = context; this.mList = fileInfos; LayoutInflater.from(context); } /** * @see android.widget.Adapter#getCount() */ @Override public int getCount() { return mList.size(); } /** * @see android.widget.Adapter#getView(int, android.view.View, android.view.ViewGroup) */ @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder viewHolder = null; if (convertView != null) { viewHolder = new ViewHolder(); convertView = inflater.inflate(R.layout.item, null); viewHolder.mFileName = (TextView) convertView.findViewById(R.id.tv_fileName); viewHolder.mProgressBar = (ProgressBar) convertView.findViewById(R.id.pb_progress); viewHolder.mStartBtn = (Button) convertView.findViewById(R.id.btn_start); viewHolder.mStopBtn = (Button) convertView.findViewById(R.id.btn_stop); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } final FileInfo fileInfo = mList.get(position); viewHolder.mFileName.setText(fileInfo.getFileName()); viewHolder.mProgressBar.setMax(100); viewHolder.mStartBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (!fileInfo.isDownLoad()) { fileInfo.setDownLoad(true); // 通知Service开始下载 Intent intent = new Intent(mContext, DownloadService.class); intent.setAction(DownloadService.ACTION_START); intent.putExtra("fileInfo", fileInfo); mContext.startService(intent); } else { Toast.makeText(mContext, fileInfo.getFileName() + "已经开始下载了", Toast.LENGTH_SHORT).show(); } } }); viewHolder.mStopBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (fileInfo.isDownLoad()) { // 通知Service停止下载 Intent intent = new Intent(mContext, DownloadService.class); intent.setAction(DownloadService.ACTION_STOP); intent.putExtra("fileInfo", fileInfo); mContext.startService(intent); fileInfo.setDownLoad(false); } else { Toast.makeText(mContext, fileInfo.getFil 1a4bc eName() + "还没有开始下载哦", Toast.LENGTH_SHORT).show(); } } }); // 将viewHolder.mFileName的Tag设为fileInfo的ID,用于唯一标识viewHolder.mFileName viewHolder.mFileName.setTag(Integer.valueOf(fileInfo.getId())); viewHolder.mProgressBar.setProgress(fileInfo.getFinished()); return convertView; } /** * 更新列表项中的进度条 * * @param id * @param progress * @return void * @author Yann * @date 2015-8-9 下午1:34:14 */ public void updateProgress(int id, int progress) { FileInfo fileInfo = mList.get(id); fileInfo.setFinished(progress); notifyDataSetChanged(); } private static class ViewHolder { TextView mFileName; ProgressBar mProgressBar; Button mStartBtn; Button mStopBtn; } /** * @see android.widget.Adapter#getItem(int) */ @Override public Object getItem(int position) { return null; } /** * @see android.widget.Adapter#getItemId(int) */ @Override public long getItemId(int position) { return 0; } }
再贴上MainActivity的代码:
public class MainActivity extends Activity { public static MainActivity mMainActivity = null; private ListView mListView = null; private List<FileInfo> mFileInfoList = null; private FileListAdapter mAdapter = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mListView = (ListView) findViewById(R.id.lv_downLoad); mFileInfoList = new ArrayList<FileInfo>(); // 初始化文件信息对象 FileInfo fileInfo1 = new FileInfo(0, "http://gdown.baidu.com/data/wisegame/91319a5a1dfae322/baidu_16785426.apk", "imooc.apk", 0, 0, false); FileInfo fileInfo2 = new FileInfo(1, "http://gdown.baidu.com/data/wisegame/91319a5a1dfae322/baidu_16785426.apk", "Activator.exe", 0, 0, false); FileInfo fileInfo3 = new FileInfo(2, "http://gdown.baidu.com/data/wisegame/91319a5a1dfae322/baidu_16785426.apk", "iTunes64Setup.exe", 0, 0, false); FileInfo fileInfo4 = new FileInfo(3, "http://gdown.baidu.com/data/wisegame/91319a5a1dfae322/baidu_16785426.apk", "BaiduPlayerNetSetup_100.exe", 0, 0, false); mFileInfoList.add(fileInfo1); mFileInfoList.add(fileInfo2); mFileInfoList.add(fileInfo3); mFileInfoList.add(fileInfo4); mAdapter = new FileListAdapter(this, mFileInfoList); mListView.setAdapter(mAdapter); // 注册广播接收器 IntentFilter filter = new IntentFilter(); filter.addAction(DownloadService.ACTION_UPDATE); filter.addAction(DownloadService.ACTION_FINISHED); registerReceiver(mReceiver, filter); mMainActivity = this; } protected void onDestroy() { super.onDestroy(); unregisterReceiver(mReceiver); } /** * 更新UI的广播接收器 */ BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) { int finised = intent.getIntExtra("finished", 0); int id = intent.getIntExtra("id", 0); mAdapter.updateProgress(id, finised); } else if (DownloadService.ACTION_FINISHED.equals(intent.getAction())) { // 下载结束 FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); fileInfo.setDownLoad(false); mAdapter.updateProgress(fileInfo.getId(), 0); Toast.makeText(MainActivity.this, mFileInfoList.get(fileInfo.getId()).getFileName() + "下载完毕", Toast.LENGTH_SHORT).show(); } } }; /** * 监听返回键 * * @see android.app.Activity#onKeyUp(int, android.view.KeyEvent) */ @Override public boolean onKeyUp(int keyCode, KeyEvent event) { if (KeyEvent.KEYCODE_BACK == keyCode) // 按了返回键时应暂停下载 { // 模拟按下暂停按钮 } return super.onKeyUp(keyCode, event); } }
文件信息FileInfo基础类
public class FileInfo implements Serializable { private int id; private String url; private String fileName; private int length; private int finished; private boolean isDownLoad; /** *@param id *@param url *@param fileName *@param length *@param finished */ public FileInfo(int id, String url, String fileName, int length, int finished,boolean isDownLoad) { this.id = id; this.url = url;//文件的现在地址 this.fileName = fileName; this.length = length;//文件的长度 this.finished = finished;//文件的进度 this.isDownLoad=isDownLoad;//是否处于下载状态 } public int getId() { return id; } public void setId(int id) { this.id = id; } public boolean isDownLoad() { return isDownLoad; } public void setDownLoad(boolean downLoad) { isDownLoad = downLoad; } public int getFinished() { return finished; } public void setFinished(int finished) { this.finished = finished; } public int getLength() { return length; } public void setLength(int length) { this.length = length; } public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } @Override public String toString() { return "FileInfo{" + "id=" + id + ", url='" + url + '\'' + ", fileName='" + fileName + '\'' + ", length=" + length + ", finished=" + finished + ", isDownLoad=" + isDownLoad + '}'; } }
开启下载服务
public class DownloadService extends Service { public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/downloads/"; public static final String ACTION_START = "ACTION_START"; public static final String ACTION_STOP = "ACTION_STOP"; public static final String ACTION_UPDATE = "ACTION_UPDATE"; public static final String ACTION_FINISHED = "ACTION_FINISHED"; public static final int MSG_INIT = 0; private String TAG = "DownloadService"; private Map<Integer, DownloadTask> mTasks = new LinkedHashMap<Integer, DownloadTask>(); /** * @see android.app.Service#onStartCommand(android.content.Intent, int, int) */ @Override public int onStartCommand(Intent intent, int flags, int startId) { // 获得Activity传过来的参数 if (ACTION_START.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); Log.i(TAG , "Start:" + fileInfo.toString()); // 启动初始化线程 new InitThread(fileInfo).start(); } else if (ACTION_STOP.equals(intent.getAction())) { FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo"); Log.i(TAG , "Stop:" + fileInfo.toString()); // 从集合中取出下载任务 DownloadTask task = mTasks.get(fileInfo.getId()); if (task != null) { task.isPause = true; } } return super.onStartCommand(intent, flags, startId); } private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { switch (msg.what) { case MSG_INIT: FileInfo fileInfo = (FileInfo) msg.obj; Log.i(TAG, "Init:" + fileInfo); // 启动下载任务 DownloadTask task = new DownloadTask(DownloadService.this, fileInfo, 3); task.downLoad(); // 把下载任务添加到集合中 mTasks.put(fileInfo.getId(), task); break; default: break; } }; }; private class InitThread extends Thread { private FileInfo mFileInfo = null; public InitThread(FileInfo mFileInfo) { this.mFileInfo = mFileInfo; } /** * @see java.lang.Thread#run() */ @Override public void run() { HttpURLConnection connection = null; RandomAccessFile raf = null; try { // 连接网络文件 URL url = new URL(mFileInfo.getUrl()); connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(5000); connection.setRequestMethod("GET"); int length = -1; if (connection.getResponseCode() == HttpStatus.SC_OK) { // 获得文件的长度 length = connection.getContentLength(); } if (length <= 0) { return; } File dir = new File(DOWNLOAD_PATH); if (!dir.exists()) { dir.mkdir(); } // 在本地创建文件 File file = new File(dir, mFileInfo.getFileName()); raf = new RandomAccessFile(file, "rwd"); // 设置文件长度 raf.setLength(length); mFileInfo.setLength(length); mHandler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget(); } catch (Exception e) { e.printStackTrace(); } finally { if (connection != null) { connection.disconnect(); } if (raf != null) { try { raf.close(); } catch (IOException e) { e.printStackTrace(); } } } } } /** * @see android.app.Service#onBind(android.content.Intent) */ @Override public IBinder onBind(Intent intent) { return null; } }
下载任务类
public class DownloadTask
{
private Context mContext = null;
private FileInfo mFileInfo = null;
private ThreadDAO mDao = null;
private int mFinised = 0;
public boolean isPause = false;
private int mThreadCount = 1; // 线程数量
private List<DownloadThread> mDownloadThreadList = null; // 线程集合
/**
*@param mContext
*@param mFileInfo
*/
public DownloadTask(Context mContext, FileInfo mFileInfo, int count)
{
this.mContext = mContext;
this.mFileInfo = mFileInfo;
this.mThreadCount = count;
mDao = new ThreadDAOImpl(mContext);
}
public void downLoad()
{
// 读取数据库的线程信息
List<ThreadInfo> threads = mDao.getThreads(mFileInfo.getUrl());
ThreadInfo threadInfo = null;
if (0 == threads.size())
{
// 计算每个线程下载长度
int len = mFileInfo.getLength() / mThreadCount;
Log.e("TAG",len+"---------len----downLoad----");
Log.e("TAG",mFileInfo.getLength()+"---------mFileInfo.getLength()-----downLoad---");
for (int i = 0; i < mThreadCount; i++)
{
// 初始化线程信息对象
threadInfo = new ThreadInfo(i, mFileInfo.getUrl(),
len * i, (i + 1) * len - 1, 0);
if (mThreadCount - 1 == i) // 处理最后一个线程下载长度不能整除的问题
{
threadInfo.setEnd(mFileInfo.getLength());
}
// 添加到线程集合中
threads.add(threadInfo);
mDao.insertThread(threadInfo);
}
}
mDownloadThreadList = new ArrayList<DownloadTask.DownloadThread>();
// 启动多个线程进行下载
for (ThreadInfo info : threads)
{
DownloadThread thread = new DownloadThread(info);
thread.start();
// 添加到线程集合中
mDownloadThreadList.add(thread);
}
}
/**
* 下载线程
* @author Yann
* @date 2015-8-8 上午11:18:55
*/
private class DownloadThread extends Thread
{
private ThreadInfo mThreadInfo = null;
public boolean isFinished = false; // 线程是否执行完毕
/**
*@param mInfo
*/
public DownloadThread(ThreadInfo mInfo)
{
this.mThreadInfo = mInfo;
}
/**
* @see java.lang.Thread#run()
*/
@Override
public void run()
{
HttpURLConnection connection = null;
RandomAccessFile raf = null;
InputStream inputStream = null;
try
{
URL url = new URL(mThreadInfo.getUrl());
connection = (HttpURLConnection) url.openConnection();
connection.setConnectTimeout(5000);
connection.setRequestMethod("GET");
// 设置下载位置
int start = mThreadInfo.getStart() + mThreadInfo.getFinished();
connection.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.getEnd());
// 设置文件写入位置
File file = new File(DownloadService.DOWNLOAD_PATH,
mFileInfo.getFileName());
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);
Intent updateIntent = new Intent();
updateIntent.setAction(DownloadService.ACTION_UPDATE);
mFinised += mThreadInfo.getFinished();
Log.e("TAG", mThreadInfo.getId() + "finished = " + mThreadInfo.getFinished());
Log.e("TAG", "connection.getResponseCode() = " + connection.getResponseCode());
// 开始下载
if (connection.getResponseCode() == HttpStatus.SC_PARTIAL_CONTENT)
{
// 读取数据
inputStream = connection.getInputStream();
byte buf[] = new byte[1024 * 4];
int len = -1;
long time = System.currentTimeMillis();
while ((len = inputStream.read(buf)) != -1)
{
// 写入文件
raf.write(buf, 0, len);
// 累加整个文件完成进度
mFinised += len;
// 累加每个线程完成的进度
mThreadInfo.setFinished(mThreadInfo.getFinished() + len);
Log.e("TAG", "mFinised * 100 / mFileInfo.getLength() = " + mFinised * 100 / mFileInfo.getLength());
Log.e("TAG",mFileInfo.getFinished() + "-mFileInfo.getFinished() = " );
Log.e("TAG",mFileInfo.getLength() + "-mFileInfo.getLength() = " );
Log.e("TAG", "mFinised=== " +mFinised);
if (System.currentTimeMillis() - time > 500)
{
time = System.currentTimeMillis();
int f = mFinised * 100 / mFileInfo.getLength();
if (f > mFileInfo.getFinished())
{
updateIntent.putExtra("finished", f);
updateIntent.putExtra("id", mFileInfo.getId());
Log.e("TAG", mFileInfo.getId() + "-finised2 = " + f);
mContext.sendBroadcast(updateIntent);
}
}
// 在下载暂停时,保存下载进度
if (isPause)
{
mDao.updateThread(mThreadInfo.getUrl(),
mThreadInfo.getId(),
mThreadInfo.getFinished());
Log.i("mThreadInfo", mThreadInfo.getId() + "finished = " + mThreadInfo.getFinished());
return;
}
}
// 标识线程执行完毕
isFinished = true;
checkAllThreadFinished();
}
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
try
{
if (connection != null)
{
connection.disconnect();
}
if (raf != null)
{
raf.close();
}
if (inputStream != null)
{
inputStream.close();
}
}
catch (Exception e2)
{
e2.printStackTrace();
}
}
}
}
/**
* 判断所有的线程是否执行完毕
* @return void
* @author Yann
* @date 2015-8-9 下午1:19:41
*/
private synchronized void checkAllThreadFinished()
{
boolean allFinished = true;
// 遍历线程集合,判断线程是否都执行完毕
for (DownloadThread thread : mDownloadThreadList)
{
if (!thread.isFinished)
{
allFinished = false;
break;
}
}
if (allFinished)
{
// 删除下载记录
mDao.deleteThread(mFileInfo.getUrl());
// 发送广播知道UI下载任务结束
Intent intent = new Intent(DownloadService.ACTION_FINISHED);
intent.putExtra("fileInfo", mFileInfo);
mContext.sendBroadcast(intent);
}
}
}
线程信息
public class ThreadInfo { private int id; private String url; private int start; private int end; private int finished; public ThreadInfo() { } /** *@param id *@param url *@param start *@param end *@param finished */ public ThreadInfo(int id, String url, int start, int end, int finished) { this.id = id; this.url = url; this.start = start; this.end = end; this.finished = finished; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public int getStart() { return start; } public void setStart(int start) { this.start = start; } public int getEnd() { return end; } public void setEnd(int end) { this.end = end; } public int getFinished() { return finished; } public void setFinished(int finished) { this.finished = finished; } @Override public String toString() { return "ThreadInfo [id=" + id + ", url=" + url + ", start=" + start + ", end=" + end + ", finished=" + finished + "]"; } }
总结
一个小小的简陋的项目终于完成了!但是对于刚入门的小伙伴们相信还是废了不少的功夫。在这个项目中,我们运用的不再是单一的组件只是,而是将组件综合运用起来,如何在listView中操作,数据库如何增删改查,Service如何与Activity通信,Notification通知栏又是怎样显示的····
这些组件我们都刷了一遍,相信下次再次使用的时候就不会像刚开始一样无从下手了。
这个项目看上去貌似不错,但仔细思量仍是有种种的不足之处,还拥有一些BUG待解决。而且在Activity与Service之间的通信用BroadCast广播,虽然会更简单些,但对于真正的项目而已可能不是这样的。
因为广播是系统组件,这样大材小用是资源的浪费,而且效率是偏低的。在一个项目中的单线程多进程中,应该使用Handler加上Messenger进行通信的,这有待于大家学习。
最后附上源码下载地址:
http://download.csdn.net/detail/matangtang/9836502
相关文章推荐
- 实现android支持多线程断点续传下载器功能
- Android编程开发实现多线程断点续传下载器实例
- helloPe的android项目实战之连连看—实现篇(一)
- helloPe的android项目实战之连连看—实现篇(二)
- (Android实战)ProgressBar+AsyncTask实现界面数据异步加载(含效果图)
- Android实战技巧:用TextView实现Rich Text---在同一个TextView中设置不同的字体风格
- (Android实战)ProgressBar+AsyncTask实现界面数据异步加载(含效果图)
- Android--多线程断点续传下载器
- Android实现网络多线程断点续传下载
- Android实战: 如何实现 图片分享菜单加入指定程序
- Android实战技巧:用TextView实现Rich Text---在同一个TextView中设置不同的字体风格
- Android实现网络多线程断点续传下载
- helloPe的android项目实战之连连看—实现篇(二)
- 【郭林专刊】Android实战技巧:用TextView实现Rich Text---在同一个TextView中设置不同的字体风格
- Android实战技巧:用TextView实现Rich Text---在同一个TextView中设置不同的字体风格
- (android硬件应用实战)摄像头拍照实现和总结
- helloPe的android项目实战之连连看—实现篇(一)
- Android开发多线程断点续传下载器
- Android实战技巧:用TextView实现Rich Text---在同一个TextView中设置不同的字体风格
- Android实现网络多线程断点续传下载