我的Android进阶之旅------>Android基于HTTP协议的多线程断点下载器的实现

简介: 一、首先写这篇文章之前,要了解实现该Android多线程断点下载器的几个知识点  1.多线程下载的原理,如下图所示 注意:由于Android移动设备和PC机的处理器还是不能相比,所以开辟的子线程建议不要多于5条。

一、首先写这篇文章之前,要了解实现该Android多线程断点下载器的几个知识点

 1.多线程下载的原理,如下图所示


注意:由于Android移动设备和PC机的处理器还是不能相比,所以开辟的子线程建议不要多于5条。当然现在某些高端机子的处理器能力比较强了,就可以多开辟几条子线程。

2、为了实现断点下载,采用数据库方式记录下载的进度,这样当你将该应用退出后,下次点击下载的时候,程序会去查看该下载链接是否存在下载记录,如果存在下载记录就会判断下载的进度,如何从上次下载的进度继续开始下载。

3、特别注意在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户的输入事件,导致“应用无响应”错误的出现。耗时的工作应该在子线程里执行。

4、UI控件画面的重绘(更新)是由主线程负责处理的,不能在子线程中更新UI控件的值。可以采用Handler机制,在主线程创建Handler对象,在子线程发送消息给主线程所绑定的消息队列,从消息中获取UI控件的值,然后在主线程中进行UI控件的重绘(更新)工作。
5、了解HTTP协议各个头字段的含义


二、将该下载器的具体实现代码展现出来

step1、首先查看整个Android项目的结构图

                                           

step2:设计应用的UI界面   /layout/activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:orientation="vertical" android:layout_width="fill_parent"
	android:layout_height="fill_parent">

    <TextView
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="@string/path" />

    <EditText
        android:id="@+id/path"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:text="http://192.168.1.100:8080/Hello/a.mp4" />

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal" >

        <Button
            android:id="@+id/downloadbutton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/startbutton" />

        <Button
            android:id="@+id/stopbutton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:enabled="false"
            android:text="@string/stopbutton" />
    </LinearLayout>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="fill_parent"
        android:layout_height="18dp" />

    <TextView
        android:id="@+id/resultView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:gravity="center" />
</LinearLayout>


/values/string.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="action_settings">Settings</string>
    <string name="hello_world">Hello world!</string>
    <string name="app_name">多线程断点下载器_欧阳鹏编写</string>
    <string name="path">下载路径</string>
    <string name="startbutton">开始下载</string>
    <string name="success">下载完成</string>
    <string name="error">下载失败</string>
    <string name="stopbutton">停止下载</string>
    <string name="sdcarderror">SDCard不存在或者写保护</string>
</resources>


step3、程序主应用 cn.oyp.download.MainActivity.java文件

package cn.oyp.download;

import java.io.File;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import cn.oyp.download.downloader.DownloadProgressListener;
import cn.oyp.download.downloader.FileDownloader;

public class MainActivity extends Activity {

	/** 下载路径文本框 **/
	private EditText pathText;
	/** 下载按钮 **/
	private Button downloadButton;
	/** 停止下载按钮 **/
	private Button stopbutton;
	/** 下载进度条 **/
	private ProgressBar progressBar;
	/** 下载结果文本框,显示下载的进度值 **/
	private TextView resultView;

	/** Hanlder的作用是用于往创建Hander对象所在的线程所绑定的消息队列发送消息 **/
	private Handler handler = new UIHander();

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

		/** 初始化各控件 **/
		pathText = (EditText) this.findViewById(R.id.path);
		downloadButton = (Button) this.findViewById(R.id.downloadbutton);
		stopbutton = (Button) this.findViewById(R.id.stopbutton);
		progressBar = (ProgressBar) this.findViewById(R.id.progressBar);
		resultView = (TextView) this.findViewById(R.id.resultView);
		/** 设置按钮的监听 **/
		ButtonClickListener listener = new ButtonClickListener();
		downloadButton.setOnClickListener(listener);
		stopbutton.setOnClickListener(listener);
	}

	/**
	 * Hanlder的作用是用于往创建Hander对象所在的线程所绑定的消息队列发送消息
	 */
	private final class UIHander extends Handler {
		public void handleMessage(Message msg) {
			switch (msg.what) {
			case 1:
				int size = msg.getData().getInt("size"); // 获取下载的进度值
				progressBar.setProgress(size); // 实时更新,设置下载进度值
				/** 计算下载的进度百分比 */
				float num = (float) progressBar.getProgress()
						/ (float) progressBar.getMax();
				int result = (int) (num * 100);
				resultView.setText(result + "%"); // 设置下载结果文本框显示下载的进度值
				// 如果进度达到了进度最大值,即下载完毕
				if (progressBar.getProgress() == progressBar.getMax()) {
					Toast.makeText(getApplicationContext(), R.string.success, 1)
							.show();// 下载成功
				}
				break;
			case -1:
				Toast.makeText(getApplicationContext(), R.string.error, 1)
						.show();// 下载出错
				break;
			}
		}
	}
	
	/**
	 * 按钮监听类
	 */
	private final class ButtonClickListener implements View.OnClickListener {
		public void onClick(View v) {
			switch (v.getId()) {
			/** 如果是下载按钮 */
			case R.id.downloadbutton:
				String path = pathText.getText().toString();// 获取下载路径
				// 判断SD卡是否存在并且可写
				if (Environment.getExternalStorageState().equals(
						Environment.MEDIA_MOUNTED)) {
					// 获取SD卡的路径
					File saveDir = Environment.getExternalStorageDirectory();
					// 开始下载的相关操作
					download(path, saveDir);
				} else {
					Toast.makeText(getApplicationContext(),
							R.string.sdcarderror, 1).show();
				}
				downloadButton.setEnabled(false);
				stopbutton.setEnabled(true);
				break;
			/** 如果是停止下载按钮 */
			case R.id.stopbutton:
				exit();// 退出下载
				downloadButton.setEnabled(true);
				stopbutton.setEnabled(false);
				break;
			}
		}

		/**
		 * UI控件画面的重绘(更新)是由主线程负责处理的,如果在子线程中更新UI控件的值,更新后的值不会重绘到屏幕上
		 * 一定要在主线程里更新UI控件的值,这样才能在屏幕上显示出来,不能在子线程中更新UI控件的值
		 * 借用Handler来传送UI控件的值到主线程去,在主线程更新UI控件的值
		 */
		private final class DownloadTask implements Runnable {
			/** 下载路径 */
			private String path;
			/** 保存路径 */
			private File saveDir;
			/** 文件下载器 */
			private FileDownloader loader;

			/**
			 * DownloadTask的构造函数
			 * 
			 * @param path
			 *            下载路径
			 * @param saveDir
			 *            保存路径
			 */
			public DownloadTask(String path, File saveDir) {
				this.path = path;
				this.saveDir = saveDir;
			}

			/**
			 * 线程主方法
			 */
			public void run() {
				try {
					/**
					 * 构建文件下载器 将下载路径,文件保存目录,下载线程数指定好
					 */
					loader = new FileDownloader(getApplicationContext(), path,
							saveDir, 5);
					progressBar.setMax(loader.getFileSize());// 设置进度条的最大刻度(即文件的总长度)
					/**
					 * DownloadProgressListener是一个接口,onDownloadSize()为未实现的方法。
					 * onDownloadSize()方法会在download方法内部被动态赋值
					 * 监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为null
					 */
					loader.download(new DownloadProgressListener() {
						public void onDownloadSize(int size) {
							// 借用Handler来传送UI控件的值到主线程去,在主线程更新UI控件的值
							Message msg = new Message();
							msg.what = 1; // 对应UIHander 获得的msg.what
							msg.getData().putInt("size", size); // 将获取的值发送给handler,用于动态更新进度
							handler.sendMessage(msg);
						}
					});
				} catch (Exception e) {
					e.printStackTrace();
					// 对应UIHander 获得的msg.what
					handler.sendMessage(handler.obtainMessage(-1)); 
				}
			}

			/**
			 * 退出下载
			 */
			public void exit() {
				if (loader != null)
					loader.exit();
			}
		}

		/** end of DownloadTask */

		/**
		 * 由于用户的输入事件(点击button, 触摸屏幕....)是由主线程负责处理的,如果主线程处于工作状态,
		 * 此时用户产生的输入事件如果没能在5秒内得到处理,系统就会报“应用无响应”错误。
		 * 所以在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户的输入事件,
		 * 导致“应用无响应”错误的出现。耗时的工作应该在子线程里执行。
		 */
		private DownloadTask task;

		/**
		 * 退出下载
		 */
		public void exit() {
			if (task != null)
				task.exit();
		}

		/**
		 * 下载方法,运行在主线程,负责开辟子线程完成下载操作,这操作耗时不超过1秒
		 * 
		 * @param path
		 *            下载路径
		 * @param saveDir
		 *            保存路径
		 */
		private void download(String path, File saveDir) {
			task = new DownloadTask(path, saveDir);
			new Thread(task).start();// 开辟子线程完成下载操作
		}
	}
	/** end of ButtonClickListener **/
}


文件下载器cn.oyp.download.downloader.FileDownloader.java文件

package cn.oyp.download.downloader;

import java.io.File;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import cn.oyp.download.service.FileService;

import android.content.Context;
import android.util.Log;

/**
 * 文件下载器
 */
public class FileDownloader {
	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<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();
	/** 每条线程下载的长度 */
	private int block;
	/** 下载路径 */
	private String downloadUrl;

	/**
	 * 获取线程数
	 */
	public int getThreadSize() {
		return threads.length;
	}

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

	/**
	 * 是否退出下载
	 */
	public boolean getExit() {
		return this.exit;
	}

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

	/**
	 * 累计已下载大小
	 * 该方法在具体某个线程下载的时候会被调用
	 */
	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 url = new URL(this.downloadUrl);
			if (!fileSaveDir.exists())
				fileSaveDir.mkdirs();// 如果保存路径不存在,则新建一个目录
			// 根据指定的线程数来新建线程数组
			this.threads = new DownloadThread[threadNum];
			// 打开HttpURLConnection
			HttpURLConnection conn = (HttpURLConnection) url.openConnection();
			// 设置 HttpURLConnection的断开时间
			conn.setConnectTimeout(5 * 1000);
			// 设置 HttpURLConnection的请求方式
			conn.setRequestMethod("GET");
			// 设置 HttpURLConnection的接收的文件类型
			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, */*");
			// 设置 HttpURLConnection的接收语音
			conn.setRequestProperty("Accept-Language", "zh-CN");
			// 指定请求uri的源资源地址
			conn.setRequestProperty("Referer", downloadUrl);
			// 设置 HttpURLConnection的字符编码
			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");
			conn.connect();
			// 打印Http协议头
			printResponseHeader(conn);
			// 如果返回的状态码为200表示正常
			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<Integer, Integer> logdata = fileService
						.getData(downloadUrl);// 获取下载记录
				if (logdata.size() > 0) {// 如果存在下载记录
					for (Map.Entry<Integer, Integer> entry : logdata.entrySet())
						data.put(entry.getKey(), entry.getValue());// 把各条线程已经下载的数据长度放入data中
				}
				if (this.data.size() == this.threads.length) {// 下面计算所有线程已经下载的数据总长度
					for (int i = 0; i < this.threads.length; i++) {
						this.downloadSize += this.data.get(i + 1);
					}
					print("已经下载的长度" + this.downloadSize);
				}
				// 计算每条线程下载的数据长度
				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) {
			print(e.toString());
			throw new RuntimeException("don't connection this url");
		}
	}

	/**
	 * 获取文件名
	 * 
	 * @param conn
	 *            Http连接
	 */
	private String getFileName(HttpURLConnection conn) {
		String filename = this.downloadUrl.substring(this.downloadUrl
				.lastIndexOf('/') + 1);// 截取下载路径中的文件名
		// 如果获取不到文件名称
		if (filename == null || "".equals(filename.trim())) {
			// 通过截取Http协议头分析下载的文件名
			for (int i = 0;; i++) {
				String mine = conn.getHeaderField(i);
				if (mine == null)
					break;
				/**
				 * Content-disposition 是 MIME 协议的扩展,MIME 协议指示 MIME
				 * 用户代理如何显示附加的文件。
				 * Content-Disposition就是当用户想把请求所得的内容存为一个文件的时候提供一个默认的文件名
				 * 协议头中的Content-Disposition格式如下:
				 * Content-Disposition","attachment;filename=FileName.txt");
				 */
				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) {// 循环判断所有线程是否完成下载
				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) {
			print(e.toString());
			throw new Exception("file download error");
		}
		return this.downloadSize;
	}

	/**
	 * 获取Http响应头字段
	 * @param http
	 * @return
	 */
	public static Map<String, String> getHttpResponseHeader(
			HttpURLConnection http) {
		Map<String, String> header = new LinkedHashMap<String, String>();
		for (int i = 0;; i++) {
			String mine = http.getHeaderField(i);
			if (mine == null)
				break;
			header.put(http.getHeaderFieldKey(i), mine);
		}
		return header;
	}

	/**
	 * 打印Http头字段
	 * 
	 * @param http
	 */
	public static void printResponseHeader(HttpURLConnection http) {
		Map<String, String> header = getHttpResponseHeader(http);
		for (Map.Entry<String, String> entry : header.entrySet()) {
			String key = entry.getKey() != null ? entry.getKey() + ":" : "";
			print(key + entry.getValue());
		}
	}
	/**
	 * 打印信息
	 * @param msg  信息
	 */
	private static void print(String msg) {
		Log.i(TAG, msg);
	}
}

文件下载线程 cn.oyp.download.downloader.DownloadThread.java文件

package cn.oyp.download.downloader;


import java.io.File;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

import android.util.Log;

public class DownloadThread extends Thread {
	private static final String TAG = "DownloadThread";
	/** 本地保存文件 */
	private File saveFile;
	/** 下载路径 */
	private URL downUrl;
	/** 该线程要下载的长度 */
	private int block;
	/** 线程ID */
	private int threadId = -1;
	/** 该线程已经下载的长度 */
	private int downLength;
	/** 是否下载完成*/
	private boolean finish = false;
	/** 文件下载器 */
	private FileDownloader downloader;
	/***
	 *  构造方法
	 */
	public DownloadThread(FileDownloader downloader, URL downUrl,
			File saveFile, int block, int downLength, int threadId) {
		this.downUrl = downUrl;
		this.saveFile = saveFile;
		this.block = block;
		this.downloader = downloader;
		this.threadId = threadId;
		this.downLength = downLength;
	}
	/**
	 * 线程主方法
	 */
	@Override
	public void run() {
		if (downLength < block) {// 未下载完成
			try {
				HttpURLConnection http = (HttpURLConnection) downUrl
						.openConnection();
				http.setConnectTimeout(5 * 1000);
				http.setRequestMethod("GET");
				http.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, */*");
				http.setRequestProperty("Accept-Language", "zh-CN");
				http.setRequestProperty("Referer", downUrl.toString());
				http.setRequestProperty("Charset", "UTF-8");
				// 该线程开始下载位置
				int startPos = block * (threadId - 1) + downLength;
				// 该线程下载结束位置
				int endPos = block * threadId - 1;
				// 设置获取实体数据的范围
				http.setRequestProperty("Range", "bytes=" + startPos + "-"
						+ endPos);
				http.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)");
				http.setRequestProperty("Connection", "Keep-Alive");
				//获取输入流
				InputStream inStream = http.getInputStream();
				byte[] buffer = new byte[1024];
				int offset = 0;
				print("Thread " + this.threadId
						+ " start download from position " + startPos);
				/**
				 * rwd: 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到基础存储设备。
				 * 对于Android移动设备一定要注意同步,否则当移动设备断电的话会丢失数据
				 */
				RandomAccessFile threadfile = new RandomAccessFile(
						this.saveFile, "rwd");
				//直接移动到文件开始位置下载的
				threadfile.seek(startPos);
				while (!downloader.getExit()
						&& (offset = inStream.read(buffer, 0, 1024)) != -1) {
					threadfile.write(buffer, 0, offset);//开始写入数据到文件
					downLength += offset;	//该线程以及下载的长度增加
					downloader.update(this.threadId, downLength);//修改数据库中该线程已经下载的数据长度
					downloader.append(offset);//文件下载器已经下载的总长度增加
				}
				threadfile.close();
				inStream.close();
				print("Thread " + this.threadId + " download finish");
				this.finish = true;
			} catch (Exception e) {
				this.downLength = -1;
				print("Thread " + this.threadId + ":" + e);
			}
		}
	}

	private static void print(String msg) {
		Log.i(TAG, msg);
	}

	/**
	 * 下载是否完成
	 * 
	 * @return
	 */
	public boolean isFinish() {
		return finish;
	}

	/**
	 * 已经下载的内容大小
	 * 
	 * @return 如果返回值为-1,代表下载失败
	 */
	public long getDownLength() {
		return downLength;
	}
}

下载进度监听接口cn.oyp.download.downloader.DownloadProgressListener.java文件

package cn.oyp.download.downloader;

/**
 * 下载进度监听接口
 */
public interface DownloadProgressListener {
	/**
	 *下载的进度 
	 */
	public void onDownloadSize(int size);
}

数据库操作类 cn.oyp.download.service.DBOpenHelper.java类

package cn.oyp.download.service;

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

public class DBOpenHelper extends SQLiteOpenHelper {
	// 数据库文件的文件名
	private static final String DBNAME = "download.db";
	// 数据库的版本号
	private static final int VERSION = 1;

	public DBOpenHelper(Context context) {
		super(context, DBNAME, null, VERSION);
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
		db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog (id integer primary key autoincrement, downpath varchar(100), threadid INTEGER, downlength INTEGER)");
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		db.execSQL("DROP TABLE IF EXISTS filedownlog");
		onCreate(db);
	}
}

文件下载服务类cn.oyp.download.service.FileService

package cn.oyp.download.service;

import java.util.HashMap;
import java.util.Map;

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

/**
 * 文件下载服务类
 */
public class FileService {
	private DBOpenHelper openHelper;

	public FileService(Context context) {
		openHelper = new DBOpenHelper(context);
	}

	/**
	 * 获取每条线程已经下载的文件长度
	 * 
	 * @param path
	 * @return
	 */
	public Map<Integer, Integer> getData(String path) {
		SQLiteDatabase db = openHelper.getReadableDatabase();
		Cursor cursor = db
				.rawQuery(
						"select threadid, downlength from filedownlog where downpath=?",
						new String[] { path });
		Map<Integer, Integer> data = new HashMap<Integer, Integer>();
		while (cursor.moveToNext()) {
			data.put(cursor.getInt(0), cursor.getInt(1));
		}
		cursor.close();
		db.close();
		return data;
	}

	/**
	 * 保存每条线程已经下载的文件长度
	 * 
	 * @param path
	 * @param map
	 */
	public void save(String path, Map<Integer, Integer> map) {// int threadid,
																// int position
		SQLiteDatabase db = openHelper.getWritableDatabase();
		db.beginTransaction();
		try {
			for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
				db.execSQL(
						"insert into filedownlog(downpath, threadid, downlength) values(?,?,?)",
						new Object[] { path, entry.getKey(), entry.getValue() });
			}
			db.setTransactionSuccessful();
		} finally {
			db.endTransaction();
		}
		db.close();
	}

	/**
	 * 实时更新每条线程已经下载的文件长度
	 * 
	 * @param path
	 * @param map
	 */
	public void update(String path, int threadId, int pos) {
		SQLiteDatabase db = openHelper.getWritableDatabase();
		db.execSQL(
				"update filedownlog set downlength=? where downpath=? and threadid=?",
				new Object[] { pos, path, threadId });
		db.close();
	}

	/**
	 * 当文件下载完成后,删除对应的下载记录
	 * 
	 * @param path
	 */
	public void delete(String path) {
		SQLiteDatabase db = openHelper.getWritableDatabase();
		db.execSQL("delete from filedownlog where downpath=?",
				new Object[] { path });
		db.close();
	}
}

step4:AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.oyp.download"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="17" />
    <!-- 访问Internet权限 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <!-- 在SDCard中创建与删除文件权限 -->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <!-- 往SDCard写入数据权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/icon"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="cn.oyp.download.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


step5:由于便于本项目的展示,所以新建一个JSP项目,部署到Tomcat服务器上,以供下载。


step6:部署应用,观看运行效果

1、打开应用


2、点击“开始下载”


3.点击“停止下载”


4.点击“开始下载”   会继续上一次的下载进度继续下载


5.退出应用,再进应用


6、点击“开始下载”,会继续上一次退出应用的时候的下载进度继续下载,完成断点下载



7.当下载完成的时候



==================================================================================================

  作者:欧阳鹏  欢迎转载,与人分享是进步的源泉!

  转载请保留原文地址http://blog.csdn.net/ouyang_peng

==================================================================================================




读者下载源码后,会发现下载速度特别慢,有以下两种原因:

1、由于本身的网络速度的原因,不会特别快。

2、由于使用RandomAccessFile的原因,对IO操作太过于频繁。因此,我修改了DownloadThread类,修改代码如下,修改之后对速度有了点提升。在此特别感谢 pobi 读者的意见

package cn.oyp.download.downloader;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

import android.util.Log;

public class DownloadThread extends Thread {
	private static final String TAG = "DownloadThread";
	/** 本地保存文件 */
	private File saveFile;
	/** 下载路径 */
	private URL downUrl;
	/** 该线程要下载的长度 */
	private int block;
	/** 线程ID */
	private int threadId = -1;
	/** 该线程已经下载的长度 */
	private int downLength;
	/** 是否下载完成 */
	private boolean finish = false;
	/** 文件下载器 */
	private FileDownloader downloader;

	/***
	 * 构造方法
	 */
	public DownloadThread(FileDownloader downloader, URL downUrl,
			File saveFile, int block, int downLength, int threadId) {
		this.downUrl = downUrl;
		this.saveFile = saveFile;
		this.block = block;
		this.downloader = downloader;
		this.threadId = threadId;
		this.downLength = downLength;
	}

	/**
	 * 线程主方法
	 */
	@Override
	public void run() {
		if (downLength < block) {// 未下载完成
			try {
				HttpURLConnection http = (HttpURLConnection) downUrl
						.openConnection();
				http.setConnectTimeout(5 * 1000);
				http.setRequestMethod("GET");
				http.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, */*");
				http.setRequestProperty("Accept-Language", "zh-CN");
				http.setRequestProperty("Referer", downUrl.toString());
				http.setRequestProperty("Charset", "UTF-8");
				// 该线程开始下载位置
				int startPos = block * (threadId - 1) + downLength;
				// 该线程下载结束位置
				int endPos = block * threadId - 1;
				// 设置获取实体数据的范围
				http.setRequestProperty("Range", "bytes=" + startPos + "-"
						+ endPos);
				http.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)");
				http.setRequestProperty("Connection", "Keep-Alive");
				/****/
				System.out.println("DownloadThread http.getResponseCode():"
						+ http.getResponseCode());
				if (http.getResponseCode() == 206) {
					/***
					 * //获取输入流 InputStream inStream = http.getInputStream();
					 * byte[] buffer = new byte[1024]; int offset = 0;
					 * print("Thread " + this.threadId +
					 * " start download from position " + startPos);
					 * 
					 * // rwd: 打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到基础存储设备。
					 * //对于Android移动设备一定要注意同步,否则当移动设备断电的话会丢失数据 RandomAccessFile
					 * threadfile = new RandomAccessFile( this.saveFile, "rwd");
					 * //直接移动到文件开始位置下载的 threadfile.seek(startPos); while
					 * (!downloader.getExit() && (offset = inStream.read(buffer,
					 * 0, 1024)) != -1) { threadfile.write(buffer, 0,
					 * offset);//开始写入数据到文件 downLength += offset; //该线程以及下载的长度增加
					 * downloader.update(this.threadId,
					 * downLength);//修改数据库中该线程已经下载的数据长度
					 * downloader.append(offset);//文件下载器已经下载的总长度增加 }
					 * threadfile.close();
					 * 
					 * print("Thread " + this.threadId + " download finish");
					 * this.finish = true;
					 **/
					// 获取输入流
					InputStream inStream = http.getInputStream();
					BufferedInputStream bis = new BufferedInputStream(inStream);
					byte[] buffer = new byte[1024 * 4];
					int offset = 0;
					RandomAccessFile threadfile = new RandomAccessFile(
							this.saveFile, "rwd");
					// 获取RandomAccessFile的FileChannel
					FileChannel outFileChannel = threadfile.getChannel();
					// 直接移动到文件开始位置下载的
					outFileChannel.position(startPos);
					// 分配缓冲区的大小
					while (!downloader.getExit()
							&& (offset = bis.read(buffer)) != -1) {
						outFileChannel
								.write(ByteBuffer.wrap(buffer, 0, offset));// 开始写入数据到文件
						downLength += offset; // 该线程以及下载的长度增加
						downloader.update(this.threadId, downLength);// 修改数据库中该线程已经下载的数据长度
						downloader.append(offset);// 文件下载器已经下载的总长度增加
					}
					outFileChannel.close();
					threadfile.close();
					inStream.close();
					print("Thread " + this.threadId + " download finish");
					this.finish = true;
				}
			} catch (Exception e) {
				this.downLength = -1;
				print("Thread " + this.threadId + ":" + e);
			}
		}
	}

	private static void print(String msg) {
		Log.i(TAG, msg);
	}

	/**
	 * 下载是否完成
	 * 
	 * @return
	 */
	public boolean isFinish() {
		return finish;
	}

	/**
	 * 已经下载的内容大小
	 * 
	 * @return 如果返回值为-1,代表下载失败
	 */
	public long getDownLength() {
		return downLength;
	}
}

   ==================================下面看一个gif动画===========================================

    


可以查看log日志,查看多线程下载的情况

==================================================================================================

  作者:欧阳鹏  欢迎转载,与人分享是进步的源泉!

  转载请保留原文地址http://blog.csdn.net/ouyang_peng

==================================================================================================


    博客写完后,又有读者提出了修改意见,在这儿特别感谢热心的读者给的建议,下面是读者JavaLover00000 给的建议:

修改了部分代码后,具体的优化效果如下所示,修改后下载速度确实变快了很多。

修改前的效果

修改后的效果

具体修改的代码可以到以下地址进行下载:Android基于HTTP协议的多线程断点下载器的实现源码_第二次优化之后

主要是将1、buffer改为8k  
2、因为发现花费在更新数据库的时间比 read和write加起来的时间都要多一点,所以将更新数据库进度改为下载线程出现异常的时候更新单个线程进度和FileDownloader中的exit()中更新所有线程进度


代码修改的地方具体可以查看源代码中的FileDownloader.java和DownloadThread.java

==================================================================================================

  作者:欧阳鹏  欢迎转载,与人分享是进步的源泉!

  转载请保留原文地址http://blog.csdn.net/ouyang_peng

==================================================================================================


相关文章
|
24天前
|
网络协议 Linux iOS开发
推荐:实现RTSP/RTMP/HLS/HTTP协议的轻量级流媒体框架,支持大并发连接请求
推荐:实现RTSP/RTMP/HLS/HTTP协议的轻量级流媒体框架,支持大并发连接请求
55 1
|
8天前
|
缓存 网络协议
【计算机协议】第一章——HTTP协议详解
【计算机协议】第一章——HTTP协议详解
|
9天前
|
网络协议 Java API
深度剖析:Java网络编程中的TCP/IP与HTTP协议实践
【4月更文挑战第17天】Java网络编程重在TCP/IP和HTTP协议的应用。TCP提供可靠数据传输,通过Socket和ServerSocket实现;HTTP用于Web服务,常借助HttpURLConnection或Apache HttpClient。两者结合,构成网络服务基础。Java有多种高级API和框架(如Netty、Spring Boot)简化开发,助力高效、高并发的网络通信。
|
10天前
|
网络协议 安全 API
Android网络和数据交互: 什么是HTTP和HTTPS?在Android中如何进行网络请求?
HTTP和HTTPS是网络数据传输协议,HTTP基于TCP/IP,简单快速,HTTPS则是加密的HTTP,确保数据安全。在Android中,过去常用HttpURLConnection和HttpClient,但HttpClient自Android 6.0起被移除。现在推荐使用支持TLS、流式上传下载、超时配置等特性的HttpsURLConnection进行网络请求。
10 0
|
12天前
|
缓存 安全 网络协议
Http协议是什么
【4月更文挑战第12天】HTTP是用于从WWW服务器传输超文本到浏览器的协议,基于TCP/IP,特点包括无连接、无状态、面向对象、无阻塞和可缓存。它的工作原理是客户端发送请求,服务器处理后返回响应。自1989年创建以来,HTTP已发展支持多媒体内容传输,并通过HTTPS提供安全保护。学习更多可参考计算机网络技术文献。
18 6
|
13天前
|
Java API 调度
安卓多线程和并发处理:提高应用效率
【4月更文挑战第13天】本文探讨了安卓应用中多线程和并发处理的优化方法,包括使用Thread、AsyncTask、Loader、IntentService、JobScheduler、WorkManager以及线程池。此外,还介绍了RxJava和Kotlin协程作为异步编程工具。理解并恰当运用这些技术能提升应用效率,避免UI卡顿,确保良好用户体验。随着安卓技术发展,更高级的异步处理工具将助力开发者构建高性能应用。
|
14天前
|
存储 JSON 前端开发
网络原理(4)HTTP协议(下)
网络原理(4)HTTP协议
26 0
|
24天前
|
安全 Linux API
Android进程与线程
Android进程与线程
18 0
|
24天前
|
XML JSON JavaScript
推荐一个比较好用的c++版本http协议库-cpp-httplib
推荐一个比较好用的c++版本http协议库-cpp-httplib
38 1
|
28天前
解决Error:All flavors must now belong to a named flavor dimension. Learn more at https://d.android.com
解决Error:All flavors must now belong to a named flavor dimension. Learn more at https://d.android.com
22 5