Android:使用 HttpURLConnection 实现多线程下载
使用HttpURLConnection
实现多线程下载
这个小程序是根据《疯狂Android讲义》(第三版)第13章 Android网络应用,13.3节 使用HTTP访问网络 扩展而来。
使用多线程下载文件可以更快地完成下载任务,因为客户端启动多条线程进行下载就意味着服务器也需要为该客户端提供相应的服务。假设服务器同时最多服务100个用户,在服务器中一个线程对应一个用户,100条线程在计算机内并发执行,也就是由CPU划分时间轮片流执行,如果A应用使用了99条线程下载文件,那么相当于占用了99个用户的资源,自然就拥有了较快的下载速度。不考虑网络因素
实际上并不是客户端并发的下载线程越多下载速度就越快,因为当开启太多并发线程后,应用程序需要维护每条线程的开销,线程同步的开销,这些开销可能反而使下载速度减慢
先看看最终效果
1.这个小程序中负责开启多线程下载的核心类是:DownloadUtil.java
这个类主要做了如下的事情:
- 创建URL对象(本程序使用的是
HttpURLConnection
) - 获取URL指向资源的大小(由
HttpURLConnection.getContentLength()
方法完成) - 在本地磁盘上创建一个与网络资源相同大小的空文件
(RandomAccessFile.setLength()
) - 计算每条线程应该下载网络资源的哪一部分
(从哪个字节开始,到哪个字节结束) - 依次创建,启动多条线程来下载网络资源的指定部分
######下面来看具体代码:
- 先来看构造函数
|
|
OnDownloadFinish
是下载完成后的回调
|
|
- 开启下载任务的入口方法
下载前的计算和开启线程下载都在这个方法中完成
|
|
- 创建HttpURLConnection对象:
对于一次下载该对象指向的URL资源地址应是同一个,封装成一个方法:
该方法在两个地方被调用到
- 获得URL指向资源大小时。获得大小后关闭该网络连接
- 启动线程下载该线程对应的那部分资源时
|
|
- 下载前的计算和开启多个线程进行下载:
获得大小->本地磁盘创建同大小的空文件->求得每一个线程应下载多少字节->开启线程进行下载
|
|
- 负责下载的线程类
private class
DownLoadThread
extends Thread
来看run方法
在构造函数中写了该句currentPart = new RandomAccessFile(targetFilePathAndName, "rw");
RandomAccessFile
: RandomAccessFile是用来访问那些保存数据记录的文件的,你可以用seek( )方法来访问记录,并进行读写了。这些记录的大小不必相同;但是其大小和位置必须是可知的。但是该类仅限于操作文件。——Java RandomAccessFile用法
RandomAccessFile的对象包含一个记录指针,用于标识当前流的读写位置,这个位置可以向前移动,也可以向后移动。RandomAccessFile包含两个方法来操作文件记录指针。
- long getFilePoint():记录文件指针的当前位置。
- void seek(long pos):将文件记录指针定位到pos位置
在finally
里判断当前还有几个线程还在下载,当没有线程在下载了(下载结束了),就回调onComplete
方法。
|
|
其中用到一个跳过指定字节数的方法
由于最新的Android平台上调用InputStream的skip方法时,并不能总是准确地跳过指定的字节数,因此程序实现了一个skipFully方法。
参考此处:
Java.IO.InputStream.skip() 错误(跳过字节数和预想的不等)
|
|
好了,以上就时进行多线程下载的核心类,源码已上传到GitHub,你可以在这里找到:
2. Activity部分
布局部分用到的控件主要有:TextView
,EditText
,ProgressBar
,CardView
,FloatingActionButton
和ListView
.
看起来是这样子的:
解释一下:
- 最上面的
任务详情
:
1.当点击了下载
之后当前正在下载任务的信息会显示在这里。
2.当已完成多个下载任务后,已下载的文件名会显示在ListView
处,ListView
的onItemClick
时间会获得已下载文件对应的下载信息显示在这里。 进度
下面有个ProgressBar
用于实时显示当前的下载进度下载速度
处显示实时的下载速度(两行byte/s kb/s
)下载速度
右边也会显示实时的下载进度,对应ProgressBar
的Max
和当前进度
.已下载
上边有个TextView
显示文件保存的路径FloatingActionButton
控制输入URL输入框,线程数输入框,下载按钮所在的CardView
的显示和隐藏。
代码部分
- 进入onCreate关联布局后其他几乎都在
init
方法里搞定了>.<
|
|
init()
方法里做了这么些事情:
- 初始化控件
- 初始化用于保存已下载文件名和对应任务信息的
ArrayList
ListView
绑定adapter
,item点击事件- 从
anim
和animator
中加载动画资源并绑定给FloatingActionButton
和CardView
,FloatingActionButton
的点击事件(控制动画播放) new
一个DownloadUtil
实例并定义回调方法onComplete
,在这里遇到了不少问题[^1] [^2](亦可在源码注释中查看)下载
按钮的点击事件,在点击事件里调用DownloadUtil.start
方法,因为start
方法里调用了URLConnection.openConnection
方法,所以不能在UI线程里调用此方法。同时在这里开
启Timer
实时更新UI。注意^3
- 控制
CardView
动画的代码
|
|
- 下载完成后的回调和下载按钮的点击事件
代码有点多就直接上张图吧! - 还有动画部分的代码
控制cardView隐藏/res/animator/input_hide.xml
|
|
控制cardView显示/res/animator/input_show.xml
|
|
FloatingActionButton
的动画直接定义在java代码中了
|
|
- 界面上有
EditText
所以一打开程序就会把软键盘调出来效果不好,所以在style
里加了下面两句:
|
|
- 要访问网络和读写磁盘,所以加入如下三条权限:
|
|
[^1]: 定义全局的MainActivity.this.file
来获得onComplete
返回的File
引用 或 在此处(onComplete
内)定义一个final的File来获得file的引用。不能使用第二种方法 ——假设使用第二种方法:当第一次下载(打开程序进行的第一次下载任务)成功回调该方法时file被赋值,赋值后dialog初始化,此时dialog持有file的引用,onComplete方法执结束时file被GC回收,但dialog初始时在onclick方法里使用了他的值,使每次调用dialog里onclick方法时使用的file都是第一次调用onComplete时的值,从而使File.reNameTo方法在调用两次之后就出错无法正常重命名。而每次调用editText.setText();时用的却是该次新的final File对象使对话框能正确显示文件名。因此应使用全局的file对象。
[^2]: onComplete
方法在DownloadUtil$DownLoadThread
中调用(非UI
线程),因此在onComplete
中更新UI要使用Handler
,Activity.runOnUiThread
或View.post
。