索引

  • 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;        //语句1y = 0;        //语句2flag = true;  //语句3x = 4;        //语句4y = -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,这样就从源头上解决了内存泄露。

后记

再次强调,本人能力有限,难免有知识上的空缺或者疏漏,如有不足之处请告知!我会用业余时间继续更新,感谢您的阅读。

更多相关文章

  1. 【读书笔记《Android游戏编程之从零开始》】1.Android(安卓)平台
  2. 自定义android开机动画
  3. 【读书笔记-《Android游戏编程之从零开始》】1.Android(安卓)平
  4. 创建android文件系统(Root file system)
  5. 【安卓学习之开发工具】 Android(安卓)Studio学习 6 - Android(
  6. android 的一些小知识
  7. Android更新Ui进阶精解(二)
  8. Android更新Ui进阶精解(一)
  9. Android有用代码片段(四)

随机推荐

  1. kNN之改进约会网站配对效果(附源码)
  2. 2行代码实现小程序直接分享到微信朋友圈
  3. 前端工程师和后端工程师的区别?
  4. 在windows系统下安装linux虚拟机(VMware)
  5. 更改sqlplus命令提示符的样式
  6. Numpy
  7. Solaris10下安装Oracle11g
  8. WARNING swSocket_bind: bind(0.0.0.0:95
  9. Linux下安装linux tar.gz包
  10. Azure解决方案:订单系统的构建和Azure Ser