Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android中使用Service實現後台斷點下載文件二----多任務多線程實現

Android中使用Service實現後台斷點下載文件二----多任務多線程實現

編輯:關於Android編程

 

接著上一篇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 getThreads(String url)
	{
		SQLiteDatabase db = mHelper.getReadableDatabase();
		Cursor cursor = db.rawQuery("select * from thread_info where url=?", new String[]
		{ url });
		List list = new ArrayList();
		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 mTasks = new LinkedHashMap();

	@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 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 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();
		// 啟動線程進行下載
		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 mDatas;

	public MyListViewAdapter(Context mContext, List 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 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 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。

 

 

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved