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<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;
	}

	/**
	 * 获取文件大小
	 * @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<Integer, Integer> logdata = fileService.getData(downloadUrl);// 获取下载记录
				if (logdata.size() > 0)
				{
					// 如果存在下载记录,则把各条线程已经下载的数据长度放入data中
					//这一步很重要,比如当用户由于某一种原因关闭退出了应用,
					//那么他再次进入应用的时候,应该是从上次未完成的地方开始下载而不是从新开始下载
					for (Map.Entry<Integer, Integer> 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<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;
	}
}

其中FileService类是操作数据库的一个类,下下一篇blog讲到,就是对数据库各种增删操作。

关键代码分析:

其中private Map<Integer, Integer> data = new ConcurrentHashMap<Integer, Integer>();这个参数,他的作用是每一次都从数据库中读取每一条线程的停止所下载时候的值,当下载完成之后就会删除该记录。

我们是把下载文件的位置存放到了SD卡的根目录地下,大家可以看到我们是把SD卡的位置作为参数传进FIleDownloader中,然后通过获取URL中最后‘\‘的字符串作为文件名称。文件名称不存在的时候则使用服务器提供给我们的默认名称,大家可以看getName()那个方法。

然后,在MainActivity中调用了FileDownlaoder中的download方法,可以看到他是开了三条线程去完成下载功能的。然后大家可以看到每一次下载之前都会从数据库中读取相对应线程的下载已经下载的长度,然后去跟他本应该下载的长度去对比,如果该线程完成任务,即从数据库中读取到的长度等于他应该下载的长度,则不用去下载,否则继续。然后会有一个循环,默认是死循环,他的作用是没经过900毫秒去检测下载是否完成和下载是否失败,以及发送最新的下载进度给UI线程。大家可以把那个sleep时间设置为任意值,但是要合适,因为用户一般需要经过某一段时间就要查看下载进度,我们尽可能及时的更新UI给用户一个更好的体验,但是又不能太【频繁,因为这样执行的次数太多有损性能。大家可以看自己下载的文件的大小去衡量,大一点的可以久一点时间在去更新Ui,但是小的就快一点更新UI。

然后,FIleDownloader实际上是一个控制真正去下载文件的线程的一个类。而真正下载的类是DownloadThread。下一篇blog就跟大家分享他的使用。

时间: 2024-08-08 13:58:10

Android的断点下载详细分析二的相关文章

Android 多线程断点下载源码

源码下载地址   http://download.csdn.net/detail/kiduo08/7709391 Android 多线程断点下载源码

Android之——断点下载示例

转载请注明出处:http://blog.csdn.net/l1028386804/article/details/46897641 在上一篇博文<Android之--多线程下载示例>中,我们讲解了如何实现Android的多线程下载功能,通过将整个文件分成多个数据块,开启多个线程,让每个线程分别下载一个相应的数据块来实现多线程下载的功能.多线程下载中,可以将下载这个耗时的操作放在子线程中执行,即不阻塞主线程,又符合Android开发的设计规范. 但是当下载的过程当中突然出现手机卡死,或者网络中断

Android 学习之--android多线程断点下载

我们平时都用"迅雷"下载软件,当下载到一半的时候突然断网,下次开启的时候能够从上次下载的地方继续下载,而且下载速度很快,那么这是怎么做到的呢! 其实它的“快”其实就是多线程的下载实现的,断点下载的原理是将每次下载的字节数存取下来,保证存取的子节点跟下载的同步,并在用户下次下载的时候自动读取 存储点,并以存储点为开始值继续下载.那么android里面如何实现这么断点的下载呢? 在res的布局文件里面先画一个带有进度条的下载界面 activity_main.xml <LinearLa

Android 多线程断点下载(非原创)

1.服务器的CPU分配给每条线程的时间片相同,服务器带宽平均分配给每条线程,所以客户端开启的线程越多,就能抢占到更多的服务器资源,这里在客户端开启多个线程来从服务器下载资源 2.fragment_main.xml <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" andro

android关于AndroidManifest.xml详细分析

转:http://www.cnblogs.com/zady/archive/2013/10/14/3368385.html 一.关于AndroidManifest.xmlAndroidManifest.xml 是每个android程序中必须的文件.它位于整个项目的根目录,描述了package中暴露的组件(activities, services, 等等),他们各自的实现类,各种能被处理的数据和启动位置. 除了能声明程序中的Activities, ContentProviders, Service

Android 多线程断点下载

package com.itheima.mutiledownloader; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader;

android 文件断点下载

实现原理:使用HttpURLConnection(兼容android 所有api)获取网络文件流 写入文件到sdcard中,用sqlite实时保存下载信息,如果中断下载,在下一次下载的时候读取下载信息 使用RandomAccessFile移动文件,实现续传功能. 主要分为:初始化模块.请求模块.响应模块.数据库模块. 流程图如下: 代码下次上.

android 文件断点下载库使用

类库地址:https://github.com/sungerk/android-downloader 1.首先加入连接网络权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission> 2.文件读写操作权限 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAG

Android多线程断点下载的代码流程解析

Step 1:创建一个用来记录线程下载信息的表 创建数据库表,于是乎我们创建一个数据库的管理器类,继承SQLiteOpenHelper类 重写onCreate()与onUpgrade()方法 DBOpenHelper.java: public class DBOpenHelper extends SQLiteOpenHelper { public DBOpenHelper(Context context) { super(context, "downs.db", null, 1); }