前几天项目中用到多线程断点续传,看了一些资料,实现了该功能,未免再次用到时忘记,把过程记录下来。
说到多线程下载,也许大家会觉得很迷惑,但多线程的原理实际上与单线程下载的原理并无区别。
多线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为3条线程,因为线程过多会占用系统资源,而且线程间的相互竞争也会导致下载变慢。
其次下载的时候将文件分割为三份(假设用3条线程下载)下载,在java中就要用到上次提到的RandomAccessFile这个API,它的开始结束为止用以下代码确定:
connection.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.getEnd());最后就是断点续传了,只需要才程序停止下载的时候记录下最后的下载位置就好了,当下次下载的时候从当前停止的位置开始下载。
重写布局
这次下载需要展示多个下载的文件,所以使用ListView控件,界面效果如下
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>
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.getFileName() + "还没有开始下载哦", 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进行通信的,这有待于大家学习。