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

后记

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,015评论 11 349
  • 今天是休产假后上班的第一天。昨天晚上,我就想把今天要穿的衣服准备好,但两个孩子太调皮,哄睡哄得自己跟着睡了。半夜起...
    金牛勤阅读 384评论 6 15
  • 光伏支架厂家都会收集哪些数据?这些数据又有着哪些强大的数据吸引力?光伏支架厂家哪家好?——诚智泰,带来测量制作光伏...
    花革阅读 708评论 0 0
  • 机缘巧合之下,我通过微信加入了写作治懒小组,被要求连续三十天,每天写一篇文章,主题不限,字数700字以上。...
    周言欣文阅读 258评论 0 0