上篇 UML
UML是面向对象分析与设计时的行业标准,谈面向对象的分析、设计时就不能不谈UML。
对软件工程师而言,使用UML规范整个程序的开发,是最佳的选择。
第1章 项目分析
1.1 为什么要选择Android多线程断点续传下载器作为本书讲解UML时的项目
Android多线程断点续传下载器涉及了Android应用开发大部分的核心知识点和难点:
(1)Android中主线程和非主线通信机制:Handler、Looper、Message、MessageQueue。
(2)多线程的编程、管理。
(3)Android网络编程。
(4)IoC技术,自己动手实现设计模式中的Listener模式。
(5)Activity、Service、数据库编程等。
(6)文件系统。
(7)缓存。
本章用一个项目去贯穿整个Android项目的学习,理论和实践相结合,设计和编码并举。
1.2 细致剖析Android多线程断点续传下载器
首先看一下多线程断点续传下载器的运行效果图,如下图所示。
其实上面运行效果的基本原理如下图所示。
1.Android多线程的实现思想
(1)可以根据记录当前的下载位置,实现断点下载。
如果现在需要下载一个大小为29MB的文件,当下载到5MB时,临时有事情,关闭之后普通的下载器不能帮助我们继续下载,而是必须重新开始,而多线程下载器(如迅雷)可以帮助我们记录下上次下载的位置,当再次下载时可以从记录的位置继续下载。
(2)下载速度快。
使用多线程下载文件可以更快地完成文件的下载,多线程下载文件之所以快,是因为其抢占的服务器资源多。假设服务器同时最多服务100个用户,在服务器中一条线程对应一个用户,100条线程在计算机中并非并发执行,而是由CPU划分时间片轮流执行,如果A应用使用了99条线程下载文件,那么相当于占用了99个用户的资源。假设一秒内CPU分配给每条线程的平均执行时间是10ms, A应用在服务器中一秒内就得到了990ms的执行时间,而其他应用在一秒内只有10ms的执行时间。就如同一个水龙头,在每秒出水量相等的情况下,放990ms的水肯定比放10ms的水要多。
实现多线程操作的可以分为:
● 取得网络连接;
● 初始化多线程下载信息;
● “开辟”硬盘空间;
● 将网络数据放入已申请的空间中;
● 关闭资源。
通过下图来加深对多线程的理解。
此文件的大小是6MB,共有三条线程同时进行下载,实现过程如下所示。
(1)首先要根据要访问的URL路径去调用openConnection()方法,得到HttpUrlConnection对象。HttpUrlConnection调用它的方法得到下载文件的长度,然后设置本地文件的长度。
Int filesize = HttpURLConnection.getContentLength(); RandomAccessFile file = new RandomAccessFile("QQWubiSetup.exe", "rw");
可以使用RandomAccessFile随机访问类。
RandomAccessFile和File的区别:
RandomAccessFile将FileInputStream和FileOutputStream整合到一起,而且支持将从文件的任意字节处读或写数据,File类只是将文件作为整体来处理文件的,不能读写文件。
file.setLength(filesize);
调用setLength(filesize)方法设置文件的长度,file可以达到下载文件的长度,但是它的内部不存在我们要下载文件的的数据,而是File类的特有的一些初始化的数据。
(2)根据文件长度和线程数计算每条线程下载的数据长度和下载位置。例如,文件的长度为6MB,线程数为3,那么,每条线程下载的数据长度为2MB,每条线程开始下载的位置如上图所示。
需要计算每一条线程需要下载的长度、每条线程下载的开始位置。例如,如果每一条线程是2MB,那么第一条线程就是从0开始,第二条就是从2开始,以此类推。
但是这样就引出了一个问题:在下载时,怎样去指定这些线程开始下载的位置呢?
HTTP协议已经为我们解决了这个问题,它可以给我们提供一个Range头。
(3)使用HTTP的Range头字段指定每条线程从文件的什么位置开始下载,例如,指定从文件的2MB位置开始下载文件,代码如下所示。
HttpURLConnection.setRequestProperty("Range", "bytes=2097152-");
我们设置的请求头Range字段,就是bytes=2097152,2MB的字节,比如说指定了上图的线程2,在下载的过程中只要设置了这个头,那么它就会从文件的A→B开始下载。
每条线程在各自的文件下载完之后,需要将下载完的文件保存到一定的位置。这样就引入了RandomAccessFile类。
(4)保存文件,使用RandomAccessFile类指定每条线程从本地文件的一定位置开始写入数据。
下面的代码就可以指定从文件的什么位置开始写入数据。
RandomAccessFile threadfile = new RandomAccessFile("QQWubiSetup.exe ", "rw"); threadfile.seek(2097152);
用时序图演示在此工程中实现多线程、断点下载的思路,每条线程负责写文件的某一段的数据,如下图所示。
整个工程的结构图如下图所示。
多线程、断点实现过程如下:
(1)首先设计main.xml页面,当在DownLoadActivity.java中单击下载按钮时,就会触发其单击事件,在单击事件内部中调用download()方法用于实现下载功能。
(2)编写实现下载的download()方法,并在方法内开启一个线程,在其run()方法中“new”FileDownloader类。返回下载文件的大小和已经下载的数量。
● 在FileDownloader类中构造线程下载器。
● 在download方法中调用FileService类中操作线程的下载记录业务方法,得到各个线程的最后下载位置。
● 将DownloadProgressListener接口以对象的形式当做参数传入download方法中。
(3)在FileDownloader类中的download方法中“new”DownloadThread类实现断点多线程下载,并存储在指定的文件中。DownloadThread线程返回文件是否下载完成。
(4)在FileDownloader类中的download方法中完成未下载完的补救方案。
(5)如果已经下载完成,则删除数据库中的文件,并通过DownloadProgressListener接口中的onDownloadSize方法得到已经下载文件的数量。
(6)将下载文件的大小和已经下载的数量已经返回给新开启的线程中,通过Handle异步通信实现页面重绘。将文件的下载进度显示在UI界面上。
完成此功能需要解决的技术要点:
● 完成页面UI和布局文件;
● 数据库中记录各线程已经下载的信息,对各个线程的下载记录进行操作;
● 构造下载器;
● 实现下载功能,并同时可以得到实时的各个线程的下载数量;
● 完成下载的进度的实时更新;
● 得到下载文件的名字;
● 完成页面的实时更新。
因为多线程文件下载涉及Web服务端和Android客户端,所以需要分别建立这两部分工程。
2.Android多线程断点续传下载之服务器端
服务端的核心功能是提供一个相对比较大的文件以供Android客户端下载使用,具体建立过程如下所示。
(1)建立一个动态的Web工程“ServerForMultipleThreadDownloader”,如下图所示。
一切采用默认设置,单击“Finish”按钮完成工程的创建。此时的工程视图如下图所示。
(2)把一个相对比较大的音频文件加入到WebContent的根目录下,例如,笔者这里加入的是笔者自己的CNN录音文件,文件名为“CNNRecordingFromWangjialin.mp3”,加入后的工程视图如下图所示。
(3)发布服务端Web工程,如下图所示。
发布后的Eclipse内置的浏览器显示如下图所示。
此时在该浏览器中输入http://localhost:8080/ServerForMultipleThreadDownloader/CNNReco-rdingFromWangjialin.mp3,出现如下图所示的页面。
因为安装了浏览器的播放器的缘故,此时笔者的计算机上正在播放自己模仿的CNN新闻播音。
至此,Web服务器端实现并发布成功。
3.Android多线程断点续传下载之Android客户端
(1)新建Android工程,工程的名字为“MultipleThreadContinuableDownloaderForAndroid4”,如下图所示。
单击“Next”按钮,选择默认的Android 4.0平台,如下图所示。
单击“Next”按钮,把包名设为“com.wangjialin.internet.multipleThreadContinuableDown-loaderForAndroid4”,单击“Finish”按钮完成。此时的工程视图如下图所示。
(2)完成主界面main.xml,其具体内容如下所示。
<? xml version="1.0" encoding="utf-8"? > <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:layout_width="fill_parent" android:layout_height="wrap_content" android:text="http://192.168.1.100:8080/ServerForMultipleThread Downloader/CNNRecordingFromWangjialin.mp3" android:id="@+id/path" /> <! -- 水平LinearLayout布局,包括下载按钮和暂停按钮 --> <LinearLayout android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="wrap_content" > <! -- 下载按钮,用于触发下载事件 --> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/button" android:id="@+id/downloadbutton" /> <! -- 暂停按钮,在初始状态下为不可用 --> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/stopbutton" android:enabled="false" android:id="@+id/stopbutton" /> </LinearLayout> <! -- 水平进度条,用图形化的方式实时显示进步信息 --> <ProgressBar android:layout_width="fill_parent" android:layout_height="18dp" style="? android:attr/progressBarStyleHorizontal" android:id="@+id/progressBar" /> <! -- 文本框,用于显示实时下载的百分比 --> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:id="@+id/resultView" /> </LinearLayout>
在main.xml中我们用到了水平进度条ProgressBar,这是Android所定义的,我们如果要访问Android系统中定义的样式,就必须加一个“? ”,也就是说要引用Android系统中的样式。
style="? android:attr/progressBarStyleHorizontal"这里面的样式和以前接触过的css样式很相似,主要定义页面显示的一个风格。我们在下载的过程中,还用到了百分率,我们用textview来表示,用到了Andorid的居中显示android:gravity="center",内容可以居中对齐。
此时打开main.xml的Graphical Layout视图,如下图所示。
从上图中可以看到,需要定义path、button、stopbutton等几个字符串资源,具体的strings.xml内容如下所示。
<? xml version="1.0" encoding="utf-8"? > <resources> <string name="hello">Hello World, MultipleThreadContinuableDownloader ForAndroid4Activity! </string> <string name="app_name">MultipleThreadContinuableDownloaderFor Android4</string> <string name="path">Download URL</string> <string name="button">Start Downloading</string> <string name="success">Downloading Completed Successfully</string> <string name="error">Downloading Error</string> <string name="stopbutton">Pause Downloading</string> <string name="sdcarderror">There is no SDCard or it is not allowed to write</string> </resources>
此时再看main.xml的Graphical Layout视图就不会有显示问题了,如下图所示。
(3)完成数据库的设计、实现以及对数据库的操作。
数据库中表的字段有id, downpath, threadid, downlength。
● id:代表数据记录的主键。
● threadid:代表线程的id。
● downlength:代表线程下载的最后位置。
● downpath:代表当前线程下载的资源,因为一个下载器可能会同时下载很多资源。
此时建立自己的数据库管理类,负责数据库和数据表的创建、升级、初始化等工作。
创建数据库管理类DBOpenHelper,该类需要继承“android.database.sqlite.SQLiteOpen Helper”,如下图所示。
DBOpenHelper的具体内容如下所示。
package com.wangjialin.internet.service; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; /** * SQLite管理器,实现创建数据库和表,但版本变化时实现对表的数据库表的操作 * @author think * */ public class DBOpenHelper extends SQLiteOpenHelper { private static final String DBNAME = "eric.db"; //设置数据库的名称 private static final int VERSION = 1; //设置数据库的版本 /** * 通过构造方法 * @param context应用程序的上、下文对象 */ 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); //调用onCreate方法重新创建数据表,也可以自己根据业务需onCreate(db); //调用onCreate方法重新创建数据表,也可以自己根据业务需要创建新的的数据表 } }
接下来建立数据库业务操作类,如下图所示。
单击“Finish”按钮完成创建,FileService的具体内容如下所示。
package com.wangjialin.internet.service; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; /** * 业务Bean,实现对数据的操作 * @author Wang Jialin * */ public class FileService { private DBOpenHelper openHelper; //声明数据库管理器 public FileService(Context context){ openHelper = new DBOpenHelper(context); //根据上、下文对象实例化数据库管理器 } /** * 获取特定URI的每条线程已经下载的文件长度 * @param path * @return */ public Map<Integer, Integer> getData(String path){Cursor cursor = db.rawQuery("select threadid, downlength from SQLiteDatabase db = openHelper.getReadableDatabase(); //获取可读SQLiteDatabase db = openHelper.getReadableDatabase(); //获取可读的数据库句柄,一般情况下在该操作的内部实现中,其返回的其实是可写的数据库句柄filedownlog where downpath=? ", new String[]{path}); //根据下载路径查询所有线程下载数据,返回的Cursor指向第一条记录之前Map<Integer, Integer> data = new HashMap<Integer, Integer>();//建立一个哈希表用于存放每条线程的已经下载的文件长度while(cursor.moveToNext()){//从第一条记录开始开始遍历Cursor对象 data.put(cursor.getInt(0), cursor.getInt(1)); //把线程ID和该线程已下载的长度设置进data哈希表中 data.put(cursor.getInt(cursor.getColumnIndexOrThrow ("threadid")), cursor.getInt(cursor.getColumnIndexOrThrow ("downlength"))); } cursor.close(); //关闭cursor,释放资源 db.close(); //关闭数据库 return data; //返回获得的每条线程和每条线程的下载长度 } /** * 保存每条线程已经下载的文件长度 * @param path 下载的路径 * @param map现在的ID和已经下载的长度的集合 */ public void save(String path, Map<Integer, Integer> map){ SQLiteDatabase db = openHelper.getWritableDatabase(); //获取可写的数据库句柄 db.beginTransaction(); //开始事务,因为此处要插入多批数据 try{ for(Map.Entry<Integer, Integer> entry : map.entrySet()){ //采用For-Each的方式遍历数据集合 db.execSQL("insert into filedownlog(downpath, threadid, downlength)values(? , ? , ?)", new Object[]{path, entry.getKey(), entry.getValue ()}); //插入特定下载路径,特定线程ID,已经下载的数据 } 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(); //关闭数据库,释放资源 } }
(4)实现文件下载类FileDownloader、具体线程DownloadThread以及进度监听器接口DownloadProgressListener。
FileDownloader的建立如下图所示。
FileDownloaderd的具体内容如下所示。
package com.wangjialin.internet.service.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 android.content.Context; import android.util.Log; import com.wangjialin.internet.service.FileService; public class FileDownloader { private static final String TAG = "FileDownloader"; //设置标签,方便Logcat日志记录 private static final int RESPONSEOK = 200; //响应码为200,即访问成功 private Context context; //应用程序的上、下文对象 private FileService fileService; //获取本地数据库的业务Bean private boolean exited; //停止下载标志 private int downloadedSize = 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.exited = true; //设置退出标志为true } public boolean getExited(){ return this.exited; } /** * 获取文件大小 * @return */ public int getFileSize(){ return fileSize; //从类成员变量中获取下载文件的大小 } /** * 累计已下载大小 * @param size */ protected synchronized void append(int size){ //使用同步关键字解决并发访问问题 downloadedSize += size; //把实时下载的长度加入到总下载长度中 } /** * 更新指定线程最后下载的位置 * @param threadId线程ID * @param pos最后下载的位置 */ protected synchronized void update(int threadId, int pos){ this.data.put(threadId, pos); //把制定线程ID的线程赋予最新的下载长度,以前的值会被覆盖掉 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); //实例化数据操 作业务Bean,此处需要使用Context,因为此处的数据库是应用程序私有 URL url = new URL(this.downloadUrl); //根据下载路径实例化URL if(! fileSaveDir.exists())fileSaveDir.mkdirs(); //如果指定的文 件不存在,则创建目录,此处可以创建多层目录 this.threads = new DownloadThread[threadNum]; //根据下载的线 程数创建下载线程池 HttpURLConnection conn =(HttpURLConnection)url. openConnection(); //建立一个远程连接句柄,此时尚未真正连接 conn.setConnectTimeout(5*1000); //设置连接超时时间为5秒 conn.setRequestMethod("GET"); //设置请求方式为GET 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"); //设置Connection的方式 conn.connect(); //和远程资源建立真正的连接,但尚无返回的数据流 printResponseHeader(conn); //答应返回的HTTP头字段集合 if(conn.getResponseCode()==RESPONSEOK){ //此处的请求会打开 返回流并获取返回的状态码,用于检查是否请求成功,当返回码为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.downloadedSize += this.data.get(i+1); //计算已经下载的数据之和 } print("已经下载的长度"+ this.downloadedSize + "个字节"); //打印出已经下载的数据总和 } this.block =(this.fileSize % this.threads.length)==0? this.fileSize / this.threads.length : this.fileSize / this.threads.length + 1; //计算每条线程下载的数据长度 }else{ print("服务器响应错误:" + conn.getResponseCode()+ conn. getResponseMessage()); //打印错误 throw new RuntimeException("server response error "); //抛出运行时服务器返回异常 } } catch(Exception e){ print(e.toString()); //打印错误 throw new RuntimeException("Can't connection this url"); //抛出运行时无法连接的异常 } } /** * 获取文件名 */ private String getFileName(HttpURLConnection conn){ 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; //如果遍历到了返回头末尾处,退出循环 if("content-disposition".equals(conn.getHeaderField Key(i).toLowerCase())){//获取content-disposition返回头字段,里面可能会包含文件名 Matcher m = Pattern.compile(".*filename=(.*)").matcher (mine.toLowerCase()); //使用正则表达式查询文件名 if(m.find())return m.group(1); //如果有符合正则表达规则的字符串 } } filename = UUID.randomUUID()+ ".tmp"; //由网卡上的标识数字(每个网卡都有唯一的标识号)及CPU时钟的唯一数字生成的一个16字节的二进制数作为文件名 } return filename; } /** * 开始下载文件 * @param listener监听下载数量的变化,如果不需要了解实时下载的数量,可以设置为 null * @return已下载文件大小 * @throws Exception */ public int download(DownloadProgressListener listener)throws Exception{ //进行下载,并抛出异常给调用者,如果有异常的话 try { RandomAccessFile randOut = new RandomAccessFile(this.saveFile, "rwd"); //The file is opened for reading and writing. Every change of the file's content must be written synchronously to the target device. if(this.fileSize>0)randOut.setLength(this.fileSize); //设置文件的大小 randOut.close(); //关闭该文件,使设置生效 URL url = new URL(this.downloadUrl); //A URL instance specifies the location of a resource on the internet as specified by RFC 1738 if(this.data.size()! = this.threads.length){ //如果原先未曾下载或者原先的下载线程数与现在的线程数不一致 this.data.clear(); //Removes all elements from this Map, leaving it empty. for(int i = 0; i < this.threads.length; i++){//遍历线程池 this.data.put(i+1, 0); //初始化每条线程已经下载的数据长度为0 } this.downloadedSize = 0; //设置已经下载的长度为0 } for(int i = 0; i < this.threads.length; i++){//开启线程进行下载 int downloadedLength = this.data.get(i+1); //通过特定的线程ID获取该线程已经下载的数据长度 if(downloadedLength < this.block && this.downloadedSize < this.fileSize){//判断线程是否已经完成下载,否则继续下载 this.threads[i] = new DownloadThread(this, url, this.saveFile, this.block, this.data.get(i+1), i+1); //初始化特定ID的线程 this.threads[i].setPriority(7); //设置线程的优先级, Thread.NORM_PRIORITY = 5 Thread.MIN_PRIORITY = 1 Thread. MAX_PRIORITY = 10 this.threads[i].start(); //启动线程 }else{ this.threads[i] = null; //表明在线程已经完成下载任务 } } fileService.delete(this.downloadUrl); //如果存在下载记录,删除它们,然后重新添加 fileService.save(this.downloadUrl, this.data); //把已经下载的实时数据写入数据库 boolean notFinished = true; //下载未完成 while(notFinished){// 循环判断所有线程是否完成下载 Thread.sleep(900); notFinished = false; //假定全部线程下载完成 for(int i = 0; i < this.threads.length; i++){ if(this.threads[i] ! = null && ! this.threads[i]. isFinished()){//如果发现线程未完成下载 notFinished = true; //设置标志为下载没有完成 if(this.threads[i].getDownloadedLength()== -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. downloadedSize); //通知目前已经下载完成的数据长度 } if(downloadedSize == this.fileSize)fileService.delete (this.downloadUrl); //下载完成删除记录 } catch(Exception e){ print(e.toString()); //打印错误 throw new Exception("File downloads error"); //抛出文件下载异常 } return this.downloadedSize; } /** * 获取HTTP响应头字段 * @param http HttpURLConnection对象 * @return 返回头字段的LinkedHashMap */ public static Map<String, String> getHttpResponseHeader(HttpURL Connection http){ Map<String, String> header = new LinkedHashMap<String, String>(); //使用LinkedHashMap保证写入和遍历的时候的顺序相同,而且允许空值存在 for(int i = 0; ; i++){//此处为无限循环,因为不知道头字段的数量 String fieldValue = http.getHeaderField(i); //getHeaderField(int n)用于返回第n个头字段的值。 if(fieldValue == null)break; //如果第i个字段没有值了,则表明头字段部分已经循环完毕,此处使用Break退出循环 header.put(http.getHeaderFieldKey(i), fieldValue); //getHeaderFieldKey(int n)用于返回第n个头字段的键。 } return header; } /** * 打印HTTP头字段 * @param http HttpURLConnection对象 */ public static void printResponseHeader(HttpURLConnection http){ Map<String, String> header = getHttpResponseHeader(http); //获取HTTP响应头字段 for(Map.Entry<String, String> entry : header.entrySet()){ //使用For-Each循环的方式遍历获取的头字段的值,此时遍历的循序和输入的顺序相同 String key = entry.getKey()! =null ? entry.getKey()+ ":" : ""; //当有键的时候截获取键,如果没有则为空字符串 print(key+ entry.getValue()); //答应键和值的组合 } } /** * 打印信息 * @param msg 信息字符串 */ private static void print(String msg){ Log.i(TAG, msg); //使用LogCat的Information方式打印信息 } }
可以看出FileDownloader是使用了DownloadThread来执行具体的下载工作的,Download Thread的建立如下图所示。
单击“Finish”按钮完成创建,DownloadThread的具体内容如下所示。
package com.wangjialin.internet.service.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; /** * 下载线程,根据具体下载地址、数据保存到的文件、下载块的大小、已经下载的数据大小等信息进行下载 * @author Wang Jialin * */ public class DownloadThread extends Thread { private static final String TAG = "DownloadThread"; //定义TAG,方便日期的打印输出 private File saveFile; //下载的数据保存到的文件 private URL downUrl; //下载的URL private int block; //每条线程下载的大小 private int threadId = -1; //初始化线程ID设置 private int downloadedLength; //该线程已经下载的数据长度 private boolean finished = false; //该线程是否完成下载的标志 private FileDownloader downloader; //文件下载器 public DownloadThread(FileDownloader downloader, URL downUrl, File saveFile, int block, int downloadedLength, int threadId){ this.downUrl = downUrl; this.saveFile = saveFile; this.block = block; this.downloader = downloader; this.threadId = threadId; this.downloadedLength = downloadedLength; } @Override public void run(){ if(downloadedLength < block){//未下载完成 try { HttpURLConnection http =(HttpURLConnection)downUrl. openConnection(); //开启HttpURLConnection连接 http.setConnectTimeout(5 * 1000); //设置连接超时时间为5秒钟 http.setRequestMethod("GET"); //设置请求的方法为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"); //设置通信编码为UTF-8 int startPos = block *(threadId -1)+ downloadedLength; //开始位置 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]; //设置本地数据缓存的大小为1MB int offset = 0; //设置每次读取的数据量 print("Thread " + this.threadId + " starts to download from position "+ startPos); //打印该线程开始下载的位置 RandomAccessFile threadFile = new RandomAccessFile (this.saveFile, "rwd"); //If the file does not already exist then an attempt will be made to create it and it require that every update to the file's content be written synchronously to the underlying storage device. threadFile.seek(startPos); //文件指针指向开始下载的位置 while(! downloader.getExited()&&(offset = inStream. read(buffer, 0, 1024))! = -1){//但用户没有要求停止下载,同时没有到达请求数据的末尾时候会一直循环读取数据 threadFile.write(buffer, 0, offset); //直接把数据写到文件中 downloadedLength += offset; //把新下载的已经写到文件中的数据加入到下载长度中 downloader.update(this.threadId, downloadedLength); //把该线程已经下载的数据长度更新到数据库和内存哈希表中 downloader.append(offset); //把新下载的数据长度加入到已经下载的数据总长度中 }//该线程下载数据完毕或者下载被用户停止 threadFile.close(); //Closes this random access file stream and releases any system resources associated with the stream. inStream.close(); //Concrete implementations of this class should free any resources during close if(downloader.getExited()) { print("Thread " + this.threadId + " has been paused"); } else { print("Thread " + this.threadId + " download finish"); } this.finished = true; //设置完成标志为true,无论是下载完成还是用户主动中断下载 } catch(Exception e){//出现异常 this.downloadedLength = -1; //设置该线程已经下载的长度为-1 print("Thread "+ this.threadId+ ":"+ e); //打印出异常信息 } } } /** * 打印信息 * @param msg 信息 */ private static void print(String msg){ Log.i(TAG, msg); //使用Logcat的Information方式打印信息 } /** * 下载是否完成 * @return */ public boolean isFinished(){ return finished; } /** * 已经下载的内容大小 * @return如果返回值为-1,代表下载失败 */ public long getDownloadedLength(){ return downloadedLength; } }
同时在FileDownloader中需要使用DownloadProgressListener进行进度监听,这是一个接口,如下图所示。
DownloadProgressListener的具体内容如下所示。
package com.wangjialin.internet.service.downloader; /** * 下载进度监听器 * @author Wang Jialin * */ public interface DownloadProgressListener { /** * 下载进度监听方法,获取和处理下载点数据的大小 * @param size数据大小 */ public void onDownloadSize(int size); }
(5)实现主Activity类。
MultipleThreadContinuableDownloaderForAndroid4Activity具体的内容如下所示。
package com.wangjialin.internet.multipleThreadContinuableDownloaderFor Android4; 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 com.eric.net.download.DownloadProgressListener; import com.eric.net.download.FileDownloader; /** * 主界面,负责下载界面的显示、与用户交互、响应用户事件等 * @author Wang Jialin * */ public class MultipleThreadContinuableDownloaderForAndroid4Activity extends Activity { private static final int PROCESSING = 1; //正在下载实时数据传输Message标志 private static final int FAILURE = -1; //下载失败时的Message标志 private EditText pathText; //下载输入文本框 private TextView resultView; //现在进度显示百分比文本框 private Button downloadButton; //下载按钮,可以触发下载事件 private Button stopbutton; //停止按钮,可以停止下载 private ProgressBar progressBar; //下载进度条,实时图形化的显示进度信息 //hanlder对象的作用是向创建Hander对象所在的线程所绑定的消息队列发送消息并处理消息 private Handler handler = new UIHander(); private final class UIHander extends Handler{ /** * 系统会自动调用的回调方法,用于处理消息事件 * Mesaage一般会包含消息的标志和消息的内容以及消息的处理器Handler */ public void handleMessage(Message msg){ switch(msg.what){ case PROCESSING: //下载时 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, Toast.LENGTH_LONG).show(); //使用Toast技术,提示用户下载完成 } break; case -1: //下载失败时 Toast.makeText(getApplicationContext(), R.string.error, Toast.LENGTH_LONG).show(); //提示用户下载失败 break; } } } @Override public void onCreate(Bundle savedInstanceState){ //应用程序启动时会首先调用且在应用程序整个生命周期中只会调用一次,适合于初始化工作 super.onCreate(savedInstanceState); //使用父类的onCreate用做屏幕主界面的底层和基本绘制工作 setContentView(R.layout.main); //根据XML界面文件设置主界面 pathText =(EditText)this.findViewById(R.id.path); //获取下载URL 的文本输入框对象 resultView =(TextView)this.findViewById(R.id.resultView); //获取显示下载百分比文本控件对象 downloadButton =(Button)this.findViewById(R.id.downloadbutton); //获取下载按钮对象 stopbutton =(Button)this.findViewById(R.id.stopbutton); //获取停止下载按钮对象 progressBar =(ProgressBar)this.findViewById(R.id.progressBar); //获取进度条对象 ButtonClickListener listener = new ButtonClickListener(); //声明并定义按钮监听器对象 downloadButton.setOnClickListener(listener); //设置下载按钮的监听器对象 stopbutton.setOnClickListener(listener); //设置停止下载按钮的监听器对象 } /** * 按钮监听器实现类 * @author Wang Jialin * */ private final class ButtonClickListener implements View. OnClickListener{ public void onClick(View v){ //该方法在注册了该按钮监听器的对象被单 击时会自动调用,用于响应单击事件 switch(v.getId()){ //获取单击对象的ID case R.id.downloadbutton: //当单击下载按钮时 String path = pathText.getText().toString(); //获取下载路径 if(Environment.getExternalStorageState().equals (Environment.MEDIA_MOUNTED)){ //获取SDCard是否存在,当SDCard存在时 File saveDir = Environment.getExternalStorage Directory(); //获取SDCard根目录文件 File saveDir = Environment.getExternalStoragePublic Directory( Environment.DIRECTORY_MOVIES); /*File saveDir = Environment.getExternalStoragePublic Directory( Environment.DIRECTORY_MUSIC); */ File saveDir = getApplicationContext().getExternal FilesDir(Environment.DIRECTORY_MOVIES); download(path, saveDir); //下载文件 }else{ //当SDCard不存在时 Toast.makeText(getApplicationContext(), R.string. sdcarderror, Toast.LENGTH_LONG).show(); //提示用户SDCard 不存在 } downloadButton.setEnabled(false); //设置下载按钮不可用 stopbutton.setEnabled(true); //设置停止下载按钮可用 break; case R.id.stopbutton: //当单击停止下载按钮时 exit(); //停止下载 downloadButton.setEnabled(true); //设置下载按钮可用 stopbutton.setEnabled(false); //设置停止按钮不可用 break; } } /////////////////////////////////////////////////////////////// //由于用户的输入事件(单击button,触摸屏幕…)是由主线程负责处理的,如果主线程 处于工作状态 //此时用户产生的输入事件如果没能在5秒内得到处理,系统就会报"应用无响应"错误 //所以在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户 的输入事件 //导致"应用无响应"错误的出现,耗时的工作应该在子线程里执行 /////////////////////////////////////////////////////////////// private DownloadTask task; //声明下载执行者 /** * 退出下载 */ public void exit(){ if(task! =null)task.exit(); //如果有下载对象时,退出下载 } /** * 下载资源,生命下载执行者并开辟线程开始现在 * @param path 下载的路径 * @param saveDir 保存文件 */ private void download(String path, File saveDir){//此方法运行在主线程 task = new DownloadTask(path, saveDir); //实例化下载任务 new Thread(task).start(); //开始下载 } /* * UI控件画面的重绘(更新)是由主线程负责处理的,如果在子线程中更新UI控件的 值,更新后的值不会重绘到屏幕上 * 一定要在主线程里更新UI控件的值,这样才能在屏幕上显示出来,不能在子线程中 更新UI控件的值 */ private final class DownloadTask implements Runnable{ private String path; //下载路径 private File saveDir; //下载到保存到的文件 private FileDownloader loader; //文件下载器(下载线程的容器) /** * 构造方法,实现变量初始化 * @param path 下载路径 * @param saveDir 下载要保存到的文件 */ public DownloadTask(String path, File saveDir){ this.path = path; this.saveDir = saveDir; } /** * 退出下载 */ public void exit(){ if(loader! =null)loader.exit(); //如果下载器存在的话则退出下载 } DownloadProgressListener downloadProgressListener = new DownloadProgressListener(){ //开始下载,并设置下载的监听器 V /** * 下载的文件长度会不断的被传入该回调方法 */ public void onDownloadSize(int size){ Message msg = new Message(); //新建立一个Message对象 msg.what = PROCESSING; //设置ID为1; msg.getData().putInt("size", size); //把文件下载的size设置进Message对象 handler.sendMessage(msg); //通过handler发送消息到消息队列 } }; /** * 下载线程的执行方法,会被系统自动调用 */ public void run(){ try { loader = new FileDownloader(getApplicationContext(), path, saveDir, 3); //初始化下载 progressBar.setMax(loader.getFileSize()); //设置进度条的最大刻度 loader.download(downloadProgressListener); } catch(Exception e){ e.printStackTrace(); handler.sendMessage(handler.obtainMessage(FAILURE)); //下载失败时向消息队列发送消息 /*Message message = handler.obtainMessage(); message.what = FAILURE; */ } } } } }
(6)因为要访问网络,同时要在SDCard中创建文件,所示要在AndroidManifest.xml中加入如下权限信息。
<! -- 访问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"/>
此时的AndroidManifest.xml的具体内容如下所示。
<? xml version="1.0" encoding="utf-8"? > <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.wangjialin.internet.multipleThreadContinuableDownloader ForAndroid4" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="8" /> V <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" > <! -- 主Acitivity,提供用户操作的界面和接口 --> <activity android:label="@string/app_name" android:name=".MultipleThreadContinuableDownloaderForAndroid4 Activity" > <intent-filter > <! -- 应用启动时启动入口Activity --> <action android:name="android.intent.action.MAIN" /> <! -- 应用显示在应用程序列表--> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> <! -- 访问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"/> </manifest>
(7)安装并运行Android多线程断点续传下载程序,安装并运行后的初始界面如下图(左)所示。单击“Start Downloading”开始进行下载,此时的状态如下图(右)所示。
当下载完成时,出现如下图所示的视图。
至此,Android多线程断点续传下载项目完成。