您的位置:首页 > 移动开发 > Android开发

Android实战:多线程断点续传下载器实现

2017-05-08 15:08 225 查看
前几天项目中用到多线程断点续传,看了一些资料,实现了该功能,未免再次用到时忘记,把过程记录下来。

说到多线程下载,也许大家会觉得很迷惑,但多线程的原理实际上与单线程下载的原理并无区别。

多线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢。

其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在java中就要用到上次提到的RandomAccessFile这个API,它的开始结束为止用以下代码确定:

connection.setRequestProperty("Range",
"bytes=" + start + "-" + mThreadInfo.getEnd());

最后就是断点续传了,只需要才程序停止下载的时候记录下最后的下载位置就好了,当下次下载的时候从当前停止的位置开始下载。

重写布局

这次下载需要展示多个下载的文件,所以使用ListView控件,界面效果如下



下载界面.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 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: