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

来源:互联网 发布:电脑网络共享怎么取消 编辑:程序博客网 时间:2024/06/14 02:52

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

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

多线程下载只需要确定好下载一个文件需要多少个线程,一般来说最好为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>

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进行通信的,这有待于大家学习。


最后附上源码下载地址:
http://download.csdn.net/detail/matangtang/9836502 























0 0
原创粉丝点击