Android下载文件(一)下载进度&断点续传
索引
- Android下载文件(一)下载进度&断点续传
- Android下载文件(二)多线程并发&断点续传(待续)
- Android下载文件(三)自定义进度条(待续)
- Android下载文件(四)任务信息持久化储存(待续)
- Android下载文件(五)IPC(待续)
- Android下载文件(六)XDownloader(待续)
前言
从接触Android开发至今也快两年了,一路走过来可以说是站在巨人的肩膀上前进,真的很感激为开源世界作出贡献的人。话说回来,搞了这么久的开发却一直在用别人的劳动成果也不是回事,所以我决定写几篇文章分享我对Android下载文件的理解,并在最后整合并开源一个框架,也是对我在Android之旅中的一个小小的总结。
注意:本人能力有限,如有错误、不合理、可优化的地方 请务必告知我!
实现效果
本节主要讲解Android下载文件的进度获取和断点续传,效果如下
录像-2017-09-17-00-45-57.gif所需知识点
- volatile
- RandomAccessFile
- HttpURLConnection
- Handler
volatile
volatile是java中修饰变量的关键字,在这里重点讲下其特性,后面会用到。
如需深入理解请参考 《深入理解Java虚拟机》12.3.3 对于volatile型变量的特殊规则
1. 保证可见性
根据JVM内存模型得知,JVM将内存分为主内存与工作内存两个部分,所有的变量都存放在主内存中。而每条线程有自己的工作内存,其存放部分主存中变量的拷贝,线程对变量的操作必须在工作内存中完成,然后更新到主存中。
当一个共享变量被volatile修饰,它会保证修改的值立即更新到主存中,其他线程访问时会去主存中读取新的值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时主存中可能还是原来的旧值,因此无法保证可见性。
2. 禁止指令重排
当代码编译时JVM会对指令执行的顺序进行优化,但volatile不会,如下所示
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
语句3必定在语句1/2后执行,但语句1/2顺序不做保证,同理,语句3也必定在语句4/5前面执行,语句4/5执行的顺序也不做保证。
3. 非原子性
volatile变量是不保证原子性的,但是需要注意的是 volatile关键字对long/double类型的get/set操作保证了原子性,详见这里 。
HttpURLConnection
Android基本网络请求类,这个不必多说,接触过Android开发的同学也一定会了解,如果是Android新同学请点我 。至于为什么我用HttpURLConnection而不用OKhttp或者Retrofit,因为最终我会开源一个Android下载文件的框架,所以不做过多的外部依赖。
RandomAccessFile
这个类很特殊,虽然是java.io包下的,但是只实现了DataOutput, DataInput, Closeable这三个接口,唯一父类是Object。其功能是随机读写文件,换句话说就是可以在一个文件的任何位置读取或者写入。在本文中用它来实现文件下载的断点续传。
Handler
Android开发必然涉及到的东西,新同学请点我 。
准备好了,开始撸代码
1.首先下载文件需要下载链接/下载路径/文件名等属性,所以我们写一个JavaBean,这里用到了volatile关键字,详见注释
public class TaskInfo {
private String name;//文件名
private String path;//文件路径
private String url;//链接
private long contentLen;//文件总长度
/**
* 迄今为止java虚拟机都是以32位作为原子操作,而long与double为64位,当某线程
* 将long/double类型变量读到寄存器时需要两次32位的操作,如果在第一次32位操作
* 时变量值改变,其结果会发生错误,简而言之,long/double是非线程安全的,volatile
* 关键字修饰的long/double的get/set方法具有原子性。
*/
private volatile long completedLen;//已完成长度
getter/setter省略
2.下载文件需要在子线程中进行,所以我们写一个类,实现Runnable接口,方便任务的创建
public class DownloadRunnable implements Runnable {
private TaskInfo info;//下载信息JavaBean
private boolean isStop;//是否暂停
/**
* 构造器
* @param info 任务信息
*/
public DownloadRunnable(TaskInfo info) {
this.info = info;
}
/**
* 停止下载
*/
public void stop() {
isStop = true;
}
/**
* Runnable的run方法,进行文件下载
*/
@Override
public void run() {
HttpURLConnection conn;//http连接对象
BufferedInputStream bis;//缓冲输入流,从服务器获取
RandomAccessFile raf;//随机读写器,用于写入文件,实现断点续传
int len = 0;//每次读取的数组长度
byte[] buffer = new byte[1024 * 8];//流读写的缓冲区
try {
//通过文件路径和文件名实例化File
File file = new File(info.getPath() + info.getName());
//实例化RandomAccessFile,rwd模式
raf = new RandomAccessFile(file, "rwd");
conn = (HttpURLConnection) new URL(info.getUrl()).openConnection();
conn.setConnectTimeout(120000);//连接超时时间
conn.setReadTimeout(120000);//读取超时时间
conn.setRequestMethod("GET");//请求类型为GET
if (info.getContentLen() == 0) {//如果文件长度为0,说明是新任务需要从头下载
//获取文件长度
info.setContentLen(Long.parseLong(conn.getHeaderField("content-length")));
} else {//否则设置请求属性,请求制定范围的文件流
conn.setRequestProperty("Range", "bytes=" + info.getCompletedLen() + "-" + info.getContentLen());
}
raf.seek(info.getCompletedLen());//移动RandomAccessFile写入位置,从上次完成的位置开始
conn.connect();//连接
bis = new BufferedInputStream(conn.getInputStream());//获取输入流并且包装为缓冲流
//从流读取字节数组到缓冲区
while (!isStop && -1 != (len = bis.read(buffer))) {
//把字节数组写入到文件
raf.write(buffer, 0, len);
//更新任务信息中的完成的文件长度属性
info.setCompletedLen(info.getCompletedLen() + len);
}
if (len == -1) {//如果读取到文件末尾则下载完成
Log.i("tag", "下载完了");
} else {//否则下载系手动停止
Log.i("tag", "下载停止了");
}
} catch (IOException e) {
e.printStackTrace();
Log.i("tag",e.toString());
}
}
}
3.任务开始/停止和进度回调
public class MainActivity3 extends AppCompatActivity {
private ProgressBar bar;//进度条
private TaskInfo info;//任务信息
private DownloadRunnable runnable;//下载任务
//用于更新进度的Handler
private Handler handler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
//使用Handler制造一个200毫秒为周期的循环
handler.sendEmptyMessageDelayed(1, 200);
//计算下载进度
int l = (int) ((float) info.getCompletedLen() / (float) info.getContentLen() * 100);
//设置进度条进度
bar.setProgress(l);
if (l>=100) {//当进度>=100时,取消Handler循环
handler.removeCallbacksAndMessages(null);
}
return true;
}
});
@Override
protected void onDestroy() {
//在Activity销毁时移除回调和msg,并置空,防止内存泄露
if(handler != null){
handler.removeCallbacksAndMessages(null);
handler = null;
}
super.onDestroy();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main3);
//实例化任务信息对象
info = new TaskInfo("aa.apk"
, Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/Download/"
, "https://download.alicdn.com/wireless/taobao4android/latest/702757.apk");
bar = (ProgressBar) findViewById(R.id.bar);
//设置进度条的最大值
bar.setMax(100);
}
/**
* 开始下载按钮监听
* @param view
*/
public void start(View view) {
//创建下载任务
runnable = new DownloadRunnable(info);
//开始下载任务
new Thread(runnable).start();
//开始Handler循环
handler.sendEmptyMessageDelayed(1, 200);
}
/**
* 停止下载按钮监听
* @param view
*/
public void stop(View view) {
//调用DownloadRunnable中的stop方法,停止下载
runnable.stop();
runnable = null;//强迫症,不用的对象手动置空
}
}
Q:为什么进度信息不用handler发送到主线程,而是直接从主内存中的TaskInfo获取下载进度?
A:单个线程任务确实可以用handler携带下载信息进行线程切换,但是我们过后会涉及到多线程下载,一个下载任务甚至可以达到128线程并发,这么多子线程“同时”向主线程传递消息,主线程压力太大会造成“掉帧”,也就是我们所说的卡顿,并且TaskInfo中所有属性的均具有原子性,不会出现线程安全问题。
Q:Handler是非静态的不会造成内存泄露吗?
A:不会,造成内存泄露的原因是Message持有Handler,Handler持有Activity,造成Message-Handler-Activity的引用链,导致在Activity销毁时无法被GC回收。但在Activity销毁时移除未处理的Message,这样就从源头上解决了内存泄露。
后记
再次强调,本人能力有限,难免有知识上的空缺或者疏漏,如有不足之处请告知!我会用业余时间继续更新,感谢您的阅读。