Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 多線程下載器(不含數據庫部分)

多線程下載器(不含數據庫部分)

編輯:關於Android編程

1、寫在前面:

雖然demo中程序框架已搭建完成,但是由於筆者時間原因,暫時只完成了核心部分:多線程下載的部分,其他數據庫、服務通知、暫停部分還未添加到項目中。

2、相關知識點:

(1)Java線程及停止線程的方式

(2)Java RandomAccessFile文件操作

(3)HttpURLConnection相關range字段的配置

(4)Sqlite同步操作

2、核心思想:

(1)通過HttpURLConnection判斷服務器是否支持斷電續傳:

<1>否->直接開啟普通的多線程下載(遇到斷網等情況便會重新下載)

<2>是->開啟普通的多線程下載,但是每個線程都含有自己的下載進度信息,以便斷網或用戶暫停開始重新下載重新開啟下載。筆者在針對不同的下載尺寸智能的分配不同的線程數量去下載資源,通過設置緩沖區大小來提高下載速度。

3、核心技術:

(1)HttpURLConnection的配置

(2)RandomAccessFile隨機文件的讀取以及緩沖區的設置

(3)線程的暫停與啟動

4、分析結果:

(1)將功能劃分為三大部分:下載器(統一的外部接口)、存儲器(內部的存儲實現)、通知服務(用戶交互部分)。

(2)項目結構:

\

5、核心代碼:

 

package com.jx.downloader;

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

import android.content.Context;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;

import com.jx.dbhelper.DownloadRecordDB;
import com.jx.model.DownloadModel;

/**
 * 自定義下載器: 1、根據即將下載的內容大小智能的分配下載線程數量,每個線程通過Downloader攜帶自身線程的下載信息(下載起點、終點、線程名稱)
 * 2、如果服務器支持斷電續傳則開啟數據庫
 * ,在下載處於暫停的狀態時(導致下載暫停的原因可能是手動暫停或者網絡不佳),自動保存下載信息到數據庫,在取消下載的時候自動清空數據庫信息,
 * 重新開始下載的時候,讀取內容重新下載(一般情況本地存儲變量還未被回收,不必從數據庫重新讀取) 2、使用RandomAccessFile存儲下載的內容
 * 
 * @author J_X 2016年3月19日09:58:00
 */
public class JX_Downloader {

	/**
	 * 0~3M的下載范圍默認開啟1個線程
	 */
	private final static long BELOW_Three_M_SIZE = 5 * 1024 * 1024;
	/**
	 * 3~6M的下載范圍默認開啟2個線程
	 */
	private final static long BELOW_SIX_M_SIZE = 6 * 1024 * 1024;
	/**
	 * 10~18M的下載范圍默認開啟4個線程
	 */
	private final static long BELOW_EIGHTEEN_M_SIZE = 18 * 1024 * 1024;
	/**
	 * 線程是否暫停的標示
	 */
	private static volatile boolean isStopDownloading = false;
	/**
	 * 下載器的標示id,以為可以創建多個下載器
	 */
	private long downloaderID;
	/**
	 * 需要下載的資源鏈接
	 */
	private String resourceUrl;
	/**
	 * 下載鏈接的遵守URL協議的對象
	 */
	private URL resourceURL;
	/**
	 * 需要下載的資源的總字節數
	 */
	private long resourceSize;
	/**
	 * 需要的下載線程的總數量
	 */
	private int totalTreadNum;
	/**
	 * 資源所在服務器是否支持斷點續傳功能
	 */
	private boolean isSupportLoadingAndSaving;

	/**
	 * 下載資源存儲的數據庫
	 */
	private DownloadRecordDB downlaodDB;
	/**
	 * 文件存儲目錄
	 */
	private String savaFileName;

	/**
	 * 構造器暫無子類所以沒有提供默認構造函數
	 * 
	 * @param context
	 *            上下文
	 * @param downloadUrl
	 *            將要被下載的鏈接
	 * @param saveFileName
	 *            帶後綴的下載的文件存儲名稱
	 */
	public JX_Downloader(Context context, String downloadUrl,
			String saveFileName) throws MalformedURLException {
		// TODO Auto-generated constructor stub
		resourceUrl = downloadUrl;
		this.resourceURL = new URL(resourceUrl);
		this.downlaodDB = new DownloadRecordDB(context);
		// 如果用戶沒有設置下載後綴,提供默認存儲文件夾
		if (TextUtils.isEmpty(saveFileName)) {
			this.savaFileName = "JX_DownLoader.txt";
		} else {
			this.savaFileName = saveFileName;
		}
	}

	/**
	 * 開啟下載任務,對外提供方便下載方法
	 */
	public void startDownload() {
		new Thread(new Runnable() {
			@Override
			public void run() {
				// TODO Auto-generated method stub
					try {
						readyDownload();
					} catch (IOException e) {
						// TODO Auto-generated catch block
						Log.e("Debug", "downloading fail!!!");
					}
			}
		}).start();
	}

	/**
	 * 核心下載程序(采用類似門面模式方法處理,方法有嚴格的執行順序要求)
	 * 
	 * @throws IOException
	 * 
	 */
	private void readyDownload() throws IOException {
		// 判斷當前連接所在服務器是否支持斷點續傳下載
		// this.isSupportLoadingAndSaving = false;
		// /** 不支持的給出友好提示,只進行多線程下載的任務 */
		// if (!isSupportLoadingAndSaving) {
		// Log.e("Debug", "Server don't support pause_save download!");
		// } else {
		// /** 支持的情況:1、開啟數據庫存儲各個線程進度 */
		// }

		/* 多線程下載流程 */
		// 1、計算需要下載的資源大小
		HttpURLConnection httpURLConnection = settingRequestHttp(null);
		if (httpURLConnection.getResponseCode() == 200) {
			// TODO: 需要進一步優化網絡
			Log.e("Debug", "Coonected sucessfully");
		}
		resourceSize = httpURLConnection.getContentLength();
		if (resourceSize <= 0) {
			Log.e("Debug", "unkown file Length and return");
			return;
		} else {
			Log.e("Debug", "file length: " + resourceSize + "bytes");
		}
		// 2、按照用戶設置或者資源大小智能的設置下載線程總數量
		int tempThreadNum = setTotalTreadNum(resourceSize);
		// 3、根據分配的線程數量,來將資源“等分”,並設置header
		ArrayList downloadArray = new ArrayList();
		if (isSupportLoadingAndSaving) {
			// 從數據庫讀取
			downloadArray = downlaodDB.getAllInfo();
		} else {
			// 從本地方法讀取
			downloadArray = initResoureSize(tempThreadNum, resourceSize);
		}
		// 4、劃分線程進行下載
		for (int i = 0; i < downloadArray.size(); ++i) {
			DownloadModel tempModel = downloadArray.get(i);
			MyRunnable runnable = new MyRunnable(tempModel);
			Thread thread = new Thread(runnable);
			thread.start();
		}
	}

	/**
	 * 配置Http信息,准備開始下載數據
	 * 
	 * @throws IOException
	 */
	private HttpURLConnection settingRequestHttp(DownloadModel model)
			throws IOException {
		HttpURLConnection coon = (HttpURLConnection) this.resourceURL
				.openConnection();
		coon.setConnectTimeout(3 * 1000);
		coon.setRequestMethod("GET");
		coon.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, */*");
		coon.setRequestProperty("Accept-Language", "zh-CN");
		coon.setRequestProperty("Referer", resourceUrl);
		coon.setRequestProperty("Charset", "UTF-8");
		coon.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)");
		coon.setRequestProperty("Connection", "Keep-Alive");
		// 斷點續傳的核心代碼,設置下載區間,注意是bytes=在這吃過虧
		if (model != null) {
			coon.setRequestProperty(
					"Range",
					"bytes=" + model.getDownloadedLength() + "-"
							+ model.getDownloadLengthSum());
		}
		return coon;
	}

	/**
	 * 多線程下載共享的run方法
	 * 
	 * @author Administrator
	 * 
	 */
	private class MyRunnable implements Runnable {

		private DownloadModel rModel;

		public MyRunnable(DownloadModel model) {
			this.rModel = model;
		}

		@Override
		public void run() {
			try {
				Log.i("Debug", rModel.getThreadName());
				HttpURLConnection coon = settingRequestHttp(rModel);
				if (coon.getResponseCode() == 200) {
					Log.e("Debug", "Coonected sucessfully");
				}
				resourceSize = coon.getContentLength();
				if (resourceSize <= 0) {
					Log.e("Debug", "unkown file Length and return");
					return;
				} else {
					readResourceAndSave(coon, rModel);
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

	}

	/**
	 * 將網絡數據從網上讀取下來
	 * 
	 * @throws IOException
	 */
	private void readResourceAndSave(HttpURLConnection coon, DownloadModel model)
			throws IOException {
		//創建文件夾
		File savDir = Environment.getExternalStorageDirectory();
		File file = null;
		if (Environment.getExternalStorageState().equals(
				Environment.MEDIA_MOUNTED)) {
			if (!savDir.exists()) {
				savDir.mkdirs();
			}
			file = new File(savDir, this.savaFileName);
		}
		RandomAccessFile rFile = new RandomAccessFile(file, "rwd");
		// 跳轉到當前線程對文件的讀寫起始位置
		rFile.seek(model.getDownloadedLength());
		InputStream finput = coon.getInputStream();
		if (finput != null) {
			Log.e("Debug", "finput=>" + finput.toString());
		} else {
			Log.e("Debug", "finput is null and return");
		}
		Log.e("Debug", "rFile.getFilePointer=>" + rFile.getFilePointer());
		BufferedInputStream bInput = new BufferedInputStream(finput);
		// 設置3KB的緩沖區,加快下載速度,這裡後期改成按字符或者整行讀取方式優化
		int size = 3 * 1024;
		byte[] readBytes = new byte[size];
		int readNum = 0;
		long sum = 0;
		// 當還有數據可讀且線程沒有暫停
		long needReadNum = model.getDownloadLengthSum()
				- model.getDownloadedLength();

		while ((readNum = bInput.read(readBytes, 0, size)) > 0
				&& sum < needReadNum && !isStopDownloading) {
			sum += readNum;
			// 如果請求的尺寸下一次讀取即將超過總需求數量時,修正讀取內容的大小保證不多讀取
			if ((sum + size) > needReadNum) {
				size = (int) (needReadNum - sum);
			}
			// 寫入到文件中
			rFile.write(readBytes, 0, readNum);
		}
		Log.e("Debug",
				model.getThreadName() + "finish downloading:" + rFile.length()
						+ "-bytes");
		rFile.close();
		finput.close();
	}

	/**
	 * 計算需要下載的資源大小
	 * 
	 * @param url
	 *            待下載的資源內容鏈接
	 * @return 返回資源所占的字節數
	 */
	private ArrayList initResoureSize(int tNum,
			long contentLength) {
		long tempLastSize = contentLength;// 剩余的下載內容大小
		ArrayList downloadArray = new ArrayList();
		long commonSize = contentLength / tNum;
		for (int i = 0; i < tNum; ++i) {
			DownloadModel downloadModel = new DownloadModel();
			// 還未開始下載,所以已下載的大小為0byte,他的長度代表下一個線程的下載的起點
			downloadModel.setDownloadedLength(i * commonSize);
			long loadEnding = tempLastSize;
			// 剩下的最後一個下載線程的下載大小=總線程-其他線程下載的大小和
			if (i == tNum - 1) {
				loadEnding = contentLength;
			} else {
				loadEnding = commonSize * i + commonSize;
			}
			tempLastSize = contentLength - commonSize;

			downloadModel.setDownloadLengthSum(loadEnding);
			downloadModel.setThreadName("JX_Download_Thread" + i);
			// 如果支持斷點續傳,就存儲到數據庫中,否則暫時存儲到ArrayList中
			if (isSupportLoadingAndSaving) {
				downlaodDB.insert(downloadModel);
			} else {
				downloadArray.add(downloadModel);
			}
		}
		return downloadArray;
	}

	/**
	 * 根據資源大小智能的設置下載線程總數量
	 * 
	 * @param totalTreadNum
	 *            設置的下載數據量
	 */
	private int setTotalTreadNum(long contentLength) {
		// 如果用戶沒有設置下載的線程數量則根據大小智能設置
		if (0 == this.totalTreadNum) {
			if (contentLength <= 0) {
				Log.i("Debug", "下載的內容太小!");
				return 0;
			} else if (contentLength < BELOW_Three_M_SIZE) {
				this.totalTreadNum = 1;
			} else if (contentLength < BELOW_SIX_M_SIZE) {
				this.totalTreadNum = 2;
			} else if (contentLength < BELOW_EIGHTEEN_M_SIZE) {
				this.totalTreadNum = 4;
			} else {
				this.totalTreadNum = 5;
			}
		}
		return this.totalTreadNum;
	}

	/**
	 * 判斷當前連接所在服務器是否支持斷點續傳下載
	 * 
	 * @return 默認不支持斷電續傳
	 */
	public boolean isSupportLoadingAndSaving() {
		return isSupportLoadingAndSaving;
	}

	public long getDownloaderID() {
		return downloaderID;
	}

	public void setDownloaderID(long downloaderID) {
		this.downloaderID = downloaderID;
	}

	public int getTotalTreadNum() {
		return totalTreadNum;
	}

	public String getResourceUrl() {
		return resourceUrl;
	}

	public long getResourceSize() {
		return resourceSize;
	}
}
下載器使用:

 

 

package com.jx.main;

import java.net.MalformedURLException;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

import com.example.mutilthreaddownloader.R;
import com.jx.downloader.JX_Downloader;

/**
 * 2016年3月19日09:41:00
 * 
 * @author J_X 多線程測試類
 */
public class MainActivity extends Activity {
	JX_Downloader downloader;
    Button btnDownlaod;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		btnDownlaod = (Button)findViewById(R.id.btn_start_downlaoding);
		btnDownlaod.setOnClickListener(new OnClickListener() {
			
			@Override
			public void onClick(View v) {
				// TODO Auto-generated method stub
				try {
					downloader = new JX_Downloader(MainActivity.this,
							"****.apk""****.apk); downloader.startDownload();
				} catch (MalformedURLException e1) {
					// TODO Auto-generated catch block
					e1.printStackTrace();
				}
			}
		});

	}
}
小結:

下載器的速度,出來外部因素網速,服務器傳輸速度,內部因素主要是線程的數量和緩沖區能夠影響下載速度,但是筆者在魅族酷派小米虛擬機上同一wifi相同線程數量以及同樣大小緩沖區下,下載速度小米的下載速度奇慢,還有待考證具體原因。
 

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