Android中使用Service实现后台断点下载文件2-多任务多线程实现

Android中使用Service实现后台断点下载文件二----多任务多线程实现

接着上一篇blog,这一篇是为一个下载任务同时使用多个线程去下载,而且可以同时下载多个任务。

具体实现的思路:跟上一篇差不多,只不过有些地方需要作出改进,因为是多线程下载,所以容易引发线程并发的问题,所以我们使用一个单例模式,DBHelper只能有一个对象,这样避免数据库的并发,然后对于数据库操作的方法也是同样,每一次都是只有一个对象,一个数据库实例去访问数据库(前提是他们访问的数据库是同一个表,否则不用)。

改进之后的代码如下:

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DBHelper extends SQLiteOpenHelper
{
	// 表名
	private static final String DB_NAME = "download.db";
	// 版本号
	private static final int VERSION = 1;
	// 建表语句
	private static final String SQL_CREATE = "create table thread_info(_id integer primary key autoincrement,"
	    + "thread_id integer,url text,start integer,end integer,finished integer)";

	private DBHelper(Context context)
	{
		super(context, DB_NAME, null, VERSION);
	}

	private static DBHelper sHelper = null;
	
	/**
	 * 单例模式
	 * @param context
	 * @return
	 */
	public static DBHelper getInstanceDBHelper(Context context)
	{
		//提高效率
		if(sHelper == null)
		{
			//同步锁
			synchronized (DBHelper.class)
      {
	      if(sHelper == null)
	      	sHelper = new DBHelper(context);
      }
		}
		return sHelper;
	}
	
	@Override
	public void onCreate(SQLiteDatabase db)
	{
		// 建表
		db.execSQL(SQL_CREATE);
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
	{
	}

}
对于thread_info表的操作方法,需要把他们改为同步,假如没有synchronized关键字可能出现线程的安全问题

import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import com.example.myasyncktestdemo.entity.ThreadInfo;

public class ThreadDBService
{
	private DBHelper mHelper = null;

	public ThreadDBService(Context context)
	{
		//通过静态方法来获取DBhelper实例
		mHelper = DBHelper.getInstanceDBHelper(context);
	}

	/**
	 * 插入线程信息
	 * 
	 * @param threadInfo
	 */
	public synchronized void insertThread(ThreadInfo threadInfo)
	{
		SQLiteDatabase db = mHelper.getWritableDatabase();
		db.execSQL("insert into thread_info(thread_id,url,start,end,finished) values(?,?,?,?,?)",
		    new Object[]
		    { threadInfo.getId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(),
		        threadInfo.getFinished() });
		db.close();
	}

	/**
	 * 删除线程信息
	 * 
	 * @param url
	 * @param thread_id
	 */
	public synchronized void deletetThread(String url)
	{
		SQLiteDatabase db = mHelper.getWritableDatabase();
		db.execSQL("delete from thread_info where url=?", new Object[]
		{ url });
		db.close();
	}

	/**
	 * 更新线程信息
	 * 
	 * @param url
	 * @param thread_id
	 * @param finished
	 */
	public synchronized void updateThread(String url, int thread_id, int finished)
	{
		SQLiteDatabase db = mHelper.getWritableDatabase();
		db.execSQL("update thread_info set finished=? where url=? and thread_id=?", new Object[]
		{ finished, url, thread_id });
		db.close();
	}

	/**
	 * 获取线程信息
	 * 
	 * @param url
	 * @return
	 */
	public synchronized List<ThreadInfo> getThreads(String url)
	{
		SQLiteDatabase db = mHelper.getReadableDatabase();
		Cursor cursor = db.rawQuery("select * from thread_info where url=?", new String[]
		{ url });
		List<ThreadInfo> list = new ArrayList<ThreadInfo>();
		while (cursor.moveToNext())
		{
			ThreadInfo threadInfo = new ThreadInfo();
			threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
			threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
			threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start")));
			threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
			threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));
			list.add(threadInfo);
		}
		cursor.close();
		db.close();
		return list;
	}

	/**
	 * 查看线程信息想、是否存在
	 * 
	 * @param url
	 * @param thread_id
	 * @return
	 */
	public synchronized boolean isExists(String url, int thread_id)
	{
		SQLiteDatabase db = mHelper.getReadableDatabase();
		Cursor cursor = db.rawQuery("select * from thread_info where url=? and thread_id=?",
		    new String[]
		    { url, thread_id + "" });
		boolean exists = cursor.moveToNext();
		cursor.close();
		db.close();
		return exists;
	}

}
然后关于DownLaodTask类,他是对每一个下载任务都回去分配按照要求的线程数量去下载,下载完成之后再统一把有关该下载任务的类的数据库信息删除。它会启动几条线程去完成下载(有DownloadService传入),然后每一条线程完成之后去判断是否下载完成,发送广播。

这里有一点需要注意的是关于进度条显示进度值的问题,假如,我们下载的数据比较大的时候,使用mFinished * 100  / mFileInfo.getLength()可能存在溢出的问题,解决办法有两种。

1,(((float) mFinished / mFileInfo.getLength()) * 100)),发送这样的数据,为什么要转为float型?因为下载的长度永远小于等于文件长度,假如没有转型则结果一直为0,所以需要转型。

2,把进条的最大值设置为文件的长度,每一次发送都是发送已经下载的长度,这样做最简单。

DownloadService代码:他有一个管理下载任务的map集合,可以对下载任务进行暂停,以及负责下载的时候获取下载文件的总长度

public class DownloadService extends Service
{

	// 下载任务的集合
	private Map<Integer, DownLoadTask> mTasks = new LinkedHashMap<Integer, DownLoadTask>();

	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		if (Constant.ACTION_START.equals(intent.getAction()))
		{
			System.out.println("有执行开始");
			FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
			new InitThread(fileInfo).start();
		} else if (Constant.ACTION_STOP.equals(intent.getAction()))
		{
			System.out.println("有执行暂停");
			FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
			DownLoadTask task = mTasks.get(fileInfo.getId());
			if (task != null)
			{
				//存在该下载任务,点击的时候就暂停
				task.setPause(true);
				stopSelf();
			}
		}
		return super.onStartCommand(intent, flags, startId);
	}

	@Override
	public IBinder onBind(Intent intent)
	{
		return null;
	}

	// 处理子线程的消息回传
	@SuppressLint("HandlerLeak")
	private Handler mHandler = new Handler()
	{
		public void handleMessage(android.os.Message msg)
		{
			if (msg.what == Constant.MSG_INIT)
			{
				System.out.println("有执行下载");
				FileInfo fileInfo = (FileInfo) msg.obj;
				DownLoadTask task = new DownLoadTask(DownloadService.this, fileInfo, 3);
				task.download();
				mTasks.put(fileInfo.getId(), task);
			}
		};
	};

	// 初始化的线程,获取下载文件的长度
	class InitThread extends Thread
	{
		private FileInfo mFileInfo = null;

		public InitThread(FileInfo mFileInfo)
		{
			super();
			this.mFileInfo = mFileInfo;
		}

		@Override
		public void run()
		{
			HttpURLConnection conn = null;
			RandomAccessFile raf = null;
			try
			{
				URL url = new URL(mFileInfo.getUrl());
				conn = (HttpURLConnection) url.openConnection();
				conn.setReadTimeout(3000);
				conn.setRequestMethod("GET");
				int length = -1;
				if (conn.getResponseCode() == 200)
				{
					// 得到下载文件长度
					length = conn.getContentLength();
				}

				if (length <= 0)
				{
					return;
				}

				File dir = new File(Constant.DOWNLAOD_PATH);
				if (!dir.exists())
				{
					dir.mkdirs();
				}
				// 构建文件对象
				File file = new File(dir, mFileInfo.getFileName());
				raf = new RandomAccessFile(file, "rwd");
				raf.setLength(length);
				mFileInfo.setLength(length);
				mHandler.obtainMessage(Constant.MSG_INIT, mFileInfo).sendToTarget();
			} catch (Exception e)
			{
				e.printStackTrace();
			} finally
			{
					conn.disconnect();
					if(raf!= null)
					{
					try
          {
	          raf.close();
          } catch (IOException e)
          {
	          e.printStackTrace();
          }
					}
				
			}
		}
	}

}
DownLoadTask类,在download方法中,首先去读取数据库信息,把该下载任务对于的几个线程的下载信息读取出来,假如集合长度为0则不存在则新建,然后插入数据库,

使用一个List管理每一个下载任务的线程,方便对下载任务完成时候发送广播删除数据库操作,在DownloadThread中真正完成数据下载。各1秒发送广播更新UI

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.content.Intent;

import com.example.myasyncktestdemo.constant.Constant;
import com.example.myasyncktestdemo.db.ThreadDBService;
import com.example.myasyncktestdemo.entity.FileInfo;
import com.example.myasyncktestdemo.entity.ThreadInfo;

/**
 * 下载任务类
 * 
 * @author Administrator
 *
 */
public class DownLoadTask
{
	private ThreadDBService mDBService = null;
	private Context mContext;
	// 下载完成的进度,所有的该任务的线程公用,可能会导致并发
	// 假如假如synchronized关键字,会导致性能消耗,比较严重,所以不加了
	private FileInfo mFileInfo;
	private int mFinished = 0;
	private boolean isPause = false;
	private int mThreadCount = 1;// 每一个下载任务的下载线程数量
	private List<DownloadThread> mThreadList = null; // 用于管理下载线程

	/**
	 * 设置下载任务暂停,每一个下载任务的所有下载线程公用一个下载监听, 一旦为true,所有该任务的下载线程暂停下载
	 * 
	 * @return
	 */
	public boolean isPause()
	{
		return isPause;
	}

	/**
	 * @param isPause
	 */
	public void setPause(boolean isPause)
	{
		this.isPause = isPause;
	}

	/**
	 * 下载任务的构造方法
	 * 
	 * @param mContext
	 *          上下文
	 * @param mFileInfo
	 *          需要下载的文件
	 * @param mThreadCount
	 *          下载该文件需要启动的线程数量
	 */
	public DownLoadTask(Context mContext, FileInfo mFileInfo, int mThreadCount)
	{
		this.mContext = mContext;
		this.mFileInfo = mFileInfo;
		mDBService = new ThreadDBService(mContext);
		this.mThreadCount = mThreadCount;
	}

	/**
	 * 下载,调用之后先去查询数据库,获取数据之后启动线程下载数据
	 */
	public void download()
	{
		// 首次点击下载的时候为0,之后都是从数据库中读取
		List<ThreadInfo> threads = mDBService.getThreads(mFileInfo.getUrl());
		System.out.println("获取到的单个任务下载的线程数" + threads.size());
		if (threads.size() == 0)
		{
			// 首次下载,需要做的工作时获取上级给的每一个任务的线程数
			// 根据线程数给每一个下载线程分配下载长度
			// 把每一个的下载线程插入到数据库中
			// 获得每个线程下载的长度
			int length = mFileInfo.getLength() / mThreadCount;
			for (int i = 0; i < mThreadCount; i++)
			{
				ThreadInfo threadInfo = new ThreadInfo(i, mFileInfo.getUrl(), length * i, (i + 1) * length
				    - 1, 0);
				mDBService.insertThread(threadInfo);
				if (i + 1 == mThreadCount)
				{
					threadInfo.setEnd(mFileInfo.getLength());
				}
				// 添加到集合中
				threads.add(threadInfo);
			}
		}
		mThreadList = new ArrayList<DownLoadTask.DownloadThread>();
		// 启动线程进行下载
		for (ThreadInfo info : threads)
		{
			DownloadThread thread = new DownloadThread(info);
			thread.start();
			System.out.println("有启动线程" + "线程状态" + isPause);
			// 添加线程到集合中
			mThreadList.add(thread);
		}
	}

	/**
	 * 判断是否所有线程都执行完毕
	 */
	private synchronized void checkAllThreadFinished()
	{
		boolean allFinished = true;
		// 遍历线程集合
		for (DownloadThread thread : mThreadList)
		{
			if (!thread.isFinished)
			{
				allFinished = false;
				break;
			}
		}
		// 下载完成,发送下载完成广播,删除数据库关于该任务的下载进程所有信息
		if (allFinished)
		{
			System.out.println("有发送下载完成广播");
			mDBService.deletetThread(mFileInfo.getUrl());
			Intent intent = new Intent(Constant.ACTION_FINISH);
			intent.putExtra("fileInfo", mFileInfo);
			mContext.sendBroadcast(intent);
		}
	}

	/**
	 * 真正的数据下载线程类
	 * 
	 * @author Administrator
	 *
	 */
	class DownloadThread extends Thread
	{
		private ThreadInfo mThreadInfo;

		public boolean isFinished = false;// 标识是该线程的对于任务是否下载完成

		public DownloadThread(ThreadInfo mThreadInfo)
		{
			super();
			this.mThreadInfo = mThreadInfo;
		}

		@Override
		public void run()
		{
			HttpURLConnection conn = null;
			RandomAccessFile raf = null;
			InputStream input = null;
			try
			{
				conn = (HttpURLConnection) new URL(mThreadInfo.getUrl()).openConnection();
				conn.setRequestMethod("GET");
				conn.setConnectTimeout(3000);
				// 计算该线程的开始下载位置,从他的start位置+他已经完成的
				int start = mThreadInfo.getStart() + mThreadInfo.getFinished();
				conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadInfo.getEnd());
				File file = new File(Constant.DOWNLAOD_PATH, mFileInfo.getFileName());
				raf = new RandomAccessFile(file, "rwd");
				raf.seek(start);
				Intent intent = new Intent(Constant.ACTION_UPDATE);
				mFinished += mThreadInfo.getFinished();
				// 断点下载的是206不是200
				if (206 == conn.getResponseCode())
				{
					System.out.println("有执行123");
					input = conn.getInputStream();
					byte[] buffer = new byte[1024];
					int len;
					long time = System.currentTimeMillis();
					System.out.println("有执行DownloadThread方法下载");
					while ((len = input.read(buffer)) != -1)
					{
						System.out.println("有执行456");
						raf.write(buffer, 0, len);
						// 真个文件的进度
						mFinished += len;// 每一个下载线程都会把他的下载进度假如到mFinished上面
						// 每个线程的下载进度
						mThreadInfo.setFinished(mThreadInfo.getFinished() + len);// 设置该线程的下载完成度
						// 隔一段时间发送广播更新ProgressBar
						if (System.currentTimeMillis() - time > 1000)
						{
							time = System.currentTimeMillis();
							//设置进度条目前的下载长度
							//这样做的目的是防止溢出
							intent.putExtra("finished", (int) (((float) mFinished / mFileInfo.getLength()) * 100));
							intent.putExtra("id", mFileInfo.getId());
							mContext.sendBroadcast(intent);
						}
						// 暂停保存数据库,跳出循环
						if (isPause)
						{
							mDBService.updateThread(mThreadInfo.getUrl(), mThreadInfo.getId(),
							    mThreadInfo.getFinished());
							System.out.println("有执行中途下载暂停");
							return;
						}
						System.out.println("有执行789");
					}
				}
				System.out.println("有执行ABC");
				isFinished = true;
				System.out.println("有线程下载完成");
				// 每一条线程下载完成之后检查是否都执行完毕
				checkAllThreadFinished();
			} catch (Exception e)
			{
				e.printStackTrace();
			} finally
			{
				conn.disconnect();
				if (input != null)
				{
					try
					{
						input.close();
					} catch (IOException e)
					{
						e.printStackTrace();
					}
				}
				if (raf != null)
				{
					try
					{
						raf.close();
					} catch (IOException e)
					{
						e.printStackTrace();
					}
				}
			}
		}
	}

}



适配器类,不多说

import java.util.List;

import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.example.myasyncktestdemo.R;
import com.example.myasyncktestdemo.constant.Constant;
import com.example.myasyncktestdemo.entity.FileInfo;
import com.example.myasyncktestdemo.services.DownloadService;

public class MyListViewAdapter extends BaseAdapter
{

	private Context mContext;
	private List<FileInfo> mDatas;

	public MyListViewAdapter(Context mContext, List<FileInfo> mDatas)
	{
		this.mContext = mContext;
		this.mDatas = mDatas;
	}

	@Override
	public int getCount()
	{
		return mDatas.size();
	}

	@Override
	public Object getItem(int position)
	{
		return mDatas.get(position);
	}

	@Override
	public long getItemId(int position)
	{
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent)
	{
		ViewHolder holder = null;
		if (convertView == null)
		{
			convertView = LayoutInflater.from(mContext).inflate(R.layout.listviewitem, null);
			holder = new ViewHolder();
			holder.tvFileName = (TextView) convertView.findViewById(R.id.mTvTip);
			holder.start = (Button) convertView.findViewById(R.id.start);
			holder.stop = (Button) convertView.findViewById(R.id.stop);
			holder.pbProgress = (ProgressBar) convertView.findViewById(R.id.pbProgress);
			convertView.setTag(holder);
		} else
		{
			holder = (ViewHolder) convertView.getTag();
		}
		final FileInfo mFileInfo = mDatas.get(position);
		holder.tvFileName.setText(mFileInfo.getFileName());
		holder.pbProgress.setProgress(mFileInfo.getFinished());
		holder.start.setOnClickListener(new OnClickListener()
		{
			@Override
			public void onClick(View v)
			{
				Intent intent = new Intent(mContext, DownloadService.class);
				intent.putExtra("fileInfo", mFileInfo);
				intent.setAction(Constant.ACTION_START);
				mContext.startService(intent);
			}
		});
		holder.stop.setOnClickListener(new OnClickListener()
		{
			@Override
			public void onClick(View v)
			{
				Intent intent = new Intent(mContext, DownloadService.class);
				intent.putExtra("fileInfo", mFileInfo);
				intent.setAction(Constant.ACTION_STOP);
				mContext.startService(intent);
			}
		});
		return convertView;
	}

	static class ViewHolder
	{
		TextView tvFileName;
		ProgressBar pbProgress;
		Button start, stop;
	}


	/**
	 * 更新进度条
	 * @param id 哪一个item的进度条
	 * @param progress 目前完成的进度
	 */
	public void setProgressbar(int id, int progress)
	{
		FileInfo mFileInfo = mDatas.get(id);
		mFileInfo.setFinished(progress);
		System.out.println("有收到长度");
		notifyDataSetChanged();
	}

}


MainActivity类,作用也是差不多

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.widget.ListView;
import android.widget.Toast;

import com.example.myasyncktestdemo.adapter.MyListViewAdapter;
import com.example.myasyncktestdemo.constant.Constant;
import com.example.myasyncktestdemo.entity.FileInfo;

public class MainActivity extends Activity
{

	private ListView mListView;
	private MyListViewAdapter mAapter;
	private List<FileInfo> mDatas;

	@Override
	protected void onCreate(Bundle savedInstanceState)
	{
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);

		mListView = (ListView) this.findViewById(R.id.mListview);
		mDatas = new ArrayList<FileInfo>();

		FileInfo fileInfo01 = new FileInfo("imooc.apk", 0, "http://www.imooc.com/mobile/imooc.apk", 0,
		    0);
		FileInfo fileInfo02 = new FileInfo("Activator.exe", 1,
		    "http://www.imooc.com/download/Activator.exe", 0, 0);
		FileInfo fileInfo03 = new FileInfo("iTunes64Setup.exe", 2,
		    "http://imooc.com/download/iTunes64Setup.exe", 0, 0);
		FileInfo fileInfo04 = new FileInfo(
		    "kugou_V7.exe",
		    3,
		    "http://dlsw.baidu.com/sw-search-sp/soft/1a/11798/kugou_V7.6.85.17344_setup.1427079848.exe",
		    0, 0);
		mDatas.add(fileInfo01);
		mDatas.add(fileInfo02);
		mDatas.add(fileInfo03);
		mDatas.add(fileInfo04);

		mAapter = new MyListViewAdapter(getApplicationContext(), mDatas);
		mListView.setAdapter(mAapter);
		IntentFilter filter = new IntentFilter();
		filter.addAction(Constant.ACTION_FINISH);
		filter.addAction(Constant.ACTION_UPDATE);
		registerReceiver(mReceiver, filter);// 代码注册Receive
	}

	protected void onDestroy()
	{
		super.onDestroy();
		unregisterReceiver(mReceiver);
	};

	BroadcastReceiver mReceiver = new BroadcastReceiver()
	{
		@Override
		public void onReceive(Context context, Intent intent)
		{
			if (intent.getAction().equals(Constant.ACTION_UPDATE))
			{
				int finished = intent.getIntExtra("finished", 0);
				int id = intent.getIntExtra("id", -1);
				mAapter.setProgressbar(id, finished);
			} else if (Constant.ACTION_FINISH.equals(intent.getAction()))
			{
				FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");
				mAapter.setProgressbar(fileInfo.getId(), 0);
				Toast.makeText(getApplicationContext(), fileInfo.getFileName() + "下载完成", 0).show();
			}
		}
	};

}
常量类

import android.os.Environment;

public class Constant
{
	public static final String ACTION_FINISH = "ACTION_FINISH";
	public static final String ACTION_UPDATE = "ACTION_UPDATE";
	public static final String DOWNLAOD_PATH = Environment.getExternalStorageDirectory()
	    .getAbsolutePath() + "/liweijie/downloads/";
	public static final String ACTION_START = "ACTION_START";
	public static final String ACTION_STOP = "ACTION_STOP";
	public static final int MSG_INIT = 0;
}


Entity类就不贴出来了,一个是ThreadInfo一个是FileInfo,然后大家记得加入对于的权限和注册Service。