美文网首页需要使用Android开发经验谈Android开发
Android下载文件(一)下载进度&断点续传

Android下载文件(一)下载进度&断点续传

作者: GLee9507 | 来源:发表于2017-09-17 15:54 被阅读4086次

索引

  • 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,这样就从源头上解决了内存泄露。

后记

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

相关文章

网友评论

  • TokyoZ:写的很好,把关键知识点都指出并解释了,真一点很用心,收益良多。

本文标题:Android下载文件(一)下载进度&断点续传

本文链接:https://www.haomeiwen.com/subject/bnxmsxtx.html