Android中使用Service实现后台断点下载文件2-多任务多线程实现
Android中使用Service实现后台断点下载文件二----多任务多线程实现
MainActivity类,作用也是差不多
接着上一篇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; }