Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android的斷點下載詳細分析二

Android的斷點下載詳細分析二

編輯:關於Android編程

由於一篇blog寫不完,這裡是接著上一篇blog的。

寫完了MVC中的View,寫著我們需要考慮Control層了,他的任務是在後台利用多線程實現斷點下載。

先看源碼:

 

public class FileDownloader
{
	/* TAG,便於調試 */
	private static final String TAG = "FileDownloader";
	/* 上下文 */
	private Context context;
	/* 用於對數據庫的操作 */
	private FileService fileService;
	/* 停止下載 */
	private boolean exit;
	/* 已下載文件長度 */
	private int downloadSize = 0;
	/* 原始文件長度 */
	private int fileSize = 0;
	/* 線程數 */
	private DownloadThread[] threads;
	/* 本地保存文件 */
	private File saveFile;
	/* 緩存各線程下載的長度 */
	private Map data = new ConcurrentHashMap();
	/* 每條線程下載的長度 */
	private int block;
	/* 下載路徑 */
	private String downloadUrl;

	/**
	 * 獲取線程數
	 */
	public int getThreadSize()
	{
		return threads.length;
	}

	/**
	 * 退出下載
	 */
	public void exit()
	{
		this.exit = true;
	}

	public boolean getExit()
	{
		return this.exit;
	}

	/**
	 * 獲取文件大小
	 * @return 文件大小
	 */
	public int getFileSize()
	{
		return fileSize;
	}

	/**
	 * 累計已下載大小
	 * @param size
	 */
	protected synchronized void append(int size)
	{
		downloadSize += size;
	}

	/**
	 * 更新指定線程最後下載的位置
	 * @param threadId 線程id
	 * @param pos 最後下載的位置
	 */
	protected synchronized void update(int threadId, int pos)
	{
		this.data.put(threadId, pos);
		this.fileService.update(this.downloadUrl,threadId, pos);
	}

	/**
	 * 構建文件下載器
	 * @param downloadUrl 下載路徑
	 * @param fileSaveDir 文件保存目錄
	 * @param threadNum 下載線程數
	 */
	public FileDownloader (Context context ,String downloadUrl ,File fileSaveDir , int threadNum)
	{
		try
		{
			this.context = context;
			this.downloadUrl = downloadUrl;
			fileService = new FileService(this.context);
			URL url = new URL(this.downloadUrl);
			if (!fileSaveDir.exists())
				fileSaveDir.mkdirs();// 不存在目錄則創建
			this.threads = new DownloadThread[threadNum];
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			conn.setConnectTimeout(5 * 1000);
			conn.setRequestMethod("GET");
			conn.setDoInput(true);
			conn.setRequestProperty("Accept",
					"image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
			conn.setRequestProperty("Accept-Language", "zh-CN");
			conn.setRequestProperty("Referer",downloadUrl);
			conn.setRequestProperty("Charset","UTF-8");
			conn.setRequestProperty("User-Agent",
					"Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
			conn.setRequestProperty("Connection","Keep-Alive");
			/*
			 * connect()方法的作用 首先創建對象,然後建立連接。 在創建對象之後,建立連接之前,可指定各種選項(例如,doInput 和
			 * UseCaches)。 連接後再進行設置就會發生錯誤。 連接後才能進行的操作(例如
			 * getContentLength),如有必要,將隱式執行連接。
			 */
			conn.connect();
			if (conn.getResponseCode() == 200)
			{
				this.fileSize = conn.getContentLength();// 根據響應獲取文件總大小
				if (this.fileSize <= 0)
					throw new RuntimeException("Unkown file size ");

				String filename = getFileName(conn);// 獲取文件名稱
				this.saveFile = new File(fileSaveDir, filename);// 構建保存文件
				Map logdata = fileService.getData(downloadUrl);// 獲取下載記錄
				if (logdata.size() > 0)
				{
					// 如果存在下載記錄,則把各條線程已經下載的數據長度放入data中
					//這一步很重要,比如當用戶由於某一種原因關閉退出了應用,
					//那麼他再次進入應用的時候,應該是從上次未完成的地方開始下載而不是從新開始下載
					for (Map.Entry entry : logdata.entrySet())
						data.put(entry.getKey(),entry.getValue());
				}
				if (this.data.size() == this.threads.length)
				{// 下面計算所有線程已經下載的數據總長度
					for (int i = 0; i < this.threads.length; i++)
					{
						this.downloadSize += this.data.get(i + 1);
					}
				}
				// 計算每條線程下載的數據長度
				this.block = (this.fileSize % this.threads.length) == 0 ? this.fileSize
						/ this.threads.length
						: this.fileSize
								/ this.threads.length
								+ 1;
			} else
			{
				throw new RuntimeException(
						"server no response ");
			}
		} catch (Exception e)
		{
			throw new RuntimeException(
					"don't connection this url");
		}
	}

	/**
	 * 獲取文件名
	 */
	private String getFileName(HttpURLConnection conn)
	{
		/*
		 * 以URL地址中的後綴作為文件名, 例如URL為:http://192.162.1.1:8080/web/lenver.exe,
		 * 那麼他的名字就是lenver.exe
		 */
		String filename = this.downloadUrl.substring(this.downloadUrl
						.lastIndexOf('/') + 1);
		if (filename == null|| "".equals(filename.trim()))
		{// 如果獲取不到文件名稱
			for (int i = 0;; i++)
			{
				String mine = conn
						.getHeaderField(i);
				if (mine == null)
					break;
				/*
				 * 當所請求的路徑所得的name不合法的shih
				 * Content-disposition其實可以控制用戶請求所得的內容存為一個文件的時候提供一個默認的文件名,
				 * 文件直接在浏覽器上顯示或者在訪問時彈出文件下載對話框。
				 */
				if ("content-disposition"
						.equals(conn
								.getHeaderFieldKey(
										i)
								.toLowerCase()))
				{
					Matcher m = Pattern
							.compile(
									".*filename=(.*)")
							.matcher(
									mine.toLowerCase());
					if (m.find())
						return m.group(1);
				}
			}
			filename = UUID.randomUUID() + ".tmp";// 默認取一個文件名
		}
		return filename;
	}

	/**
	 * 開始下載文件
	 * 
	 * @param listener
	 *            監聽下載數量的變化,如果不需要了解實時下載的數量,可以設置為null
	 * @return 已下載文件大小
	 * @throws Exception
	 */
	public int download(DownloadProgressListener listener) throws Exception
	{
		try
		{
			RandomAccessFile randOut = new RandomAccessFile(
					this.saveFile, "rw");
			if (this.fileSize > 0)
				randOut.setLength(this.fileSize);
			randOut.close();
			URL url = new URL(this.downloadUrl);
			if (this.data.size() != this.threads.length)
			{// 如果原先未曾下載或者原先的下載線程數與現在的線程數不一致,這裡都是三
				this.data.clear();
				for (int i = 0; i < this.threads.length; i++)
				{
					this.data.put(i + 1, 0);// 初始化每條線程已經下載的數據長度為0
				}
				this.downloadSize = 0;
			}
			for (int i = 0; i < this.threads.length; i++)
			{// 開啟線程進行下載
				 int downLength = this.data.get(i + 1);// 從數據庫中取出某一條線程下載的長度
				if (downLength < this.block && this.downloadSize < this.fileSize)
				{// 判斷線程是否已經完成下載,否則繼續下載
					this.threads[i] = new DownloadThread(
							this, url,
							this.saveFile,
							this.block,
							this.data.get(i + 1),
							i + 1);
					this.threads[i]
							.setPriority(7);// 設置優先級
					this.threads[i].start();// 啟動線程
				} else
				{
					this.threads[i] = null;
				}
			}
			fileService.delete(this.downloadUrl);// 如果存在下載記錄,刪除它們,然後重新添加
			fileService.save(this.downloadUrl,
					this.data);
			boolean notFinish = true;// 下載未完成
			while (notFinish)//這個循環很關鍵,他是可以維持他的調用者也就是DownloadTask這個線程一直運行下去,然後還就可以不斷的發消息給UI線程
			{// 循環判斷所有線程是否完成下載
				Thread.sleep(900);
				notFinish = false;// 假定全部線程下載完成
				for (int i = 0; i < this.threads.length; i++)
				{
					if (this.threads[i] != null
							&& !this.threads[i]
									.isFinish())
					{// 如果發現線程未完成下載
						notFinish = true;// 設置標志為下載沒有完成
						if (this.threads[i]
								.getDownLength() == -1)
						{// 如果下載失敗,再重新下載
							this.threads[i] = new DownloadThread(
									this,
									url,
									this.saveFile,
									this.block,
									this.data
											.get(i + 1),
									i + 1);
							this.threads[i]
									.setPriority(7);
							this.threads[i]
									.start();
						}
					}
				}
				if (listener != null)
					listener.onDownloadSize(this.downloadSize);// 通知目前已經下載完成的數據長度
			}
			if (downloadSize == this.fileSize)
				fileService
						.delete(this.downloadUrl);// 下載完成刪除記錄
		} catch (Exception e)
		{
			throw new Exception(
					"file download error");
		}
		return this.downloadSize;
	}

	/**
	 * 獲取Http響應頭字段
	 * 
	 * @param http
	 * @return
	 */
	public static Map getHttpResponseHeader(
			HttpURLConnection http)
	{
		Map header = new LinkedHashMap();
		for (int i = 0;; i++)
		{
			String mine = http.getHeaderField(i);
			if (mine == null)
				break;
			header.put(http.getHeaderFieldKey(i),
					mine);
		}
		return header;
	}
}

 

其中FileService類是操作數據庫的一個類,下下一篇blog講到,就是對數據庫各種增刪操作。

關鍵代碼分析:

其中private Map data = new ConcurrentHashMap();這個參數,他的作用是每一次都從數據庫中讀取每一條線程的停止所下載時候的值,當下載完成之後就會刪除該記錄。

我們是把下載文件的位置存放到了SD卡的根目錄地下,大家可以看到我們是把SD卡的位置作為參數傳進FIleDownloader中,然後通過獲取URL中最後'\'的字符串作為文件名稱。文件名稱不存在的時候則使用服務器提供給我們的默認名稱,大家可以看getName()那個方法。

然後,在MainActivity中調用了FileDownlaoder中的download方法,可以看到他是開了三條線程去完成下載功能的。然後大家可以看到每一次下載之前都會從數據庫中讀取相對應線程的下載已經下載的長度,然後去跟他本應該下載的長度去對比,如果該線程完成任務,即從數據庫中讀取到的長度等於他應該下載的長度,則不用去下載,否則繼續。然後會有一個循環,默認是死循環,他的作用是沒經過900毫秒去檢測下載是否完成和下載是否失敗,以及發送最新的下載進度給UI線程。大家可以把那個sleep時間設置為任意值,但是要合適,因為用戶一般需要經過某一段時間就要查看下載進度,我們盡可能及時的更新UI給用戶一個更好的體驗,但是又不能太【頻繁,因為這樣執行的次數太多有損性能。大家可以看自己下載的文件的大小去衡量,大一點的可以久一點時間在去更新Ui,但是小的就快一點更新UI。

然後,FIleDownloader實際上是一個控制真正去下載文件的線程的一個類。而真正下載的類是DownloadThread。下一篇blog就跟大家分享他的使用。

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