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

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

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

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























评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值