Android 实现文件的单线程断点续传下载

来源:互联网 发布:百度算法调整2017 编辑:程序博客网 时间:2024/05/17 07:43

1.实际效果:

效果图
开始界面


2.代码实现

完成这个小项目需要:

  • 基础网络知识(Http)
  • 了解android界面处理机制
  • Service的绑定与解绑
  • BroadCastReceiver的注册与消息的处理
  • 本地文件的I/O处理
  • 数据库基础
  • 事件回调原理

在这里我采用了数据库框架GreenDao,方便实现想要的效果,自己独立写几个类来操作SQLite数据库也是可以的。关于怎么使用GreenDao,这里不做叙述了,网上一大堆教程,这里给个教程链接http://m.blog.csdn.net/article/details?id=51893092。

下面是项目框架
这里写图片描述

主要有服务类DownloadService,广播类ProgressReceiver,下载类DownloadTask,线程信息类ThreadInfo,和MainActivity与App(继承Application,用于初始化数据库)。

首先来看看我的布局文件acitivty_main.xml:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/activity_main"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:paddingBottom="@dimen/activity_vertical_margin"    android:paddingLeft="@dimen/activity_horizontal_margin"    android:paddingRight="@dimen/activity_horizontal_margin"    android:paddingTop="@dimen/activity_vertical_margin"    tools:context="com.chen.capton.filedownload.MainActivity">    <TextView        android:id="@+id/info"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="文件信息:"        android:layout_alignEnd="@+id/pause" />    <ProgressBar        style="?android:attr/progressBarStyleHorizontal"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_below="@+id/info"        android:layout_alignParentStart="true"        android:id="@+id/progressBar" />    <Button        android:text="开始"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_below="@+id/progressBar"        android:layout_alignParentStart="true"        android:id="@+id/start" />    <Button        android:text="暂停"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:enabled="false"        android:id="@+id/pause"        android:layout_below="@+id/progressBar"        android:layout_toEndOf="@+id/start" />    <TextView        android:text="进度"        android:gravity="right"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_alignParentTop="true"        android:id="@+id/progressText"        android:layout_alignParentEnd="true"        android:layout_toEndOf="@+id/info" /></RelativeLayout>

配置文件menifests.xml:

<?xml version="1.0" encoding="utf-8"?><manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.chen.capton.filedownload">    <uses-permission android:name="android.permission.INTERNET"/>    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>    <application        android:name=".App"        android:allowBackup="true"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:supportsRtl="true"        android:theme="@style/AppTheme">        <activity android:name=".MainActivity">            <intent-filter>                <action android:name="android.intent.action.MAIN" />                <category android:name="android.intent.category.LAUNCHER" />            </intent-filter>        </activity>        <service            android:name=".DownloadService"            android:enabled="true"            android:exported="true">        </service>    </application></manifest>

最主要的MainActivity:
洋洋洒洒100多行

package com.chen.capton.filedownload;import android.content.ComponentName;import android.content.Intent;import android.content.IntentFilter;import android.content.ServiceConnection;import android.os.Handler;import android.os.IBinder;import android.os.Message;import android.support.v7.app.AppCompatActivity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.ProgressBar;import android.widget.TextView;public class MainActivity extends AppCompatActivity {    private TextView fileInfoText,progressText;    private Button startBtn,pauseBtn;    private ProgressBar mProgressBar;    private ProgressReceiver mReceiver;    private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //设置Action,与ProgressReceiver,DownloadService中的Action都一致    private final String url="http://192.168.1.103/app.zip";  //设置url    private final int maxProgress=100;                        //设置进度条最大进度    private boolean isContinue;                               //是否暂停的标识    private Handler handler=new Handler(){        public void handleMessage(Message msg){              mProgressBar.setProgress(msg.what);//由于发送的是空消息,直接用What作为进度参数使用              progressText.setText("完成度:"+msg.what+"%");         }    }; //用于界面更新的handler。将它作为参数传递至ProgressReceiver,供其发送Message,然后根据Message更新进度    private DownloadService mService;    private ServiceConnection conn=new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            //获取绑定好的DownloadService对象           mService= ((DownloadService.MyBinder)service).getService();        }        @Override        public void onServiceDisconnected(ComponentName name) {}    };    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        initComponent(); //初始化ProgressReceiver,DownloadService        setContentView(R.layout.activity_main);        initView(); //初始化视图控件        setListener(); //设置点击事件    }    private void initView() {        fileInfoText= (TextView) findViewById(R.id.info);        progressText= (TextView) findViewById(R.id.progressText);        startBtn= (Button) findViewById(R.id.start);        pauseBtn= (Button) findViewById(R.id.pause);        mProgressBar= (ProgressBar) findViewById(R.id.progressBar);        mProgressBar.setMax(maxProgress);    }    /*    * 初始化ProgressReceiver,DownloadService    * 绑定DownloadService,注册ProgressReceiver    * */    private void initComponent() {        Intent intent=new Intent(this,DownloadService.class);        bindService(intent,conn,BIND_AUTO_CREATE);        mReceiver=new ProgressReceiver(handler);        IntentFilter filter=new IntentFilter();        filter.addAction(REFRESH_PROGRESS);        registerReceiver(mReceiver,filter);    }    private void setListener() {       startBtn.setOnClickListener(new View.OnClickListener() {           @Override           public void onClick(View v) {               if(!isContinue) {                   mService.startMission(url,maxProgress);                   v.setEnabled(false);                   pauseBtn.setEnabled(true);                   fileInfoText.setText(getFileName(url));               }else {                   mService.continueMission();                   v.setEnabled(false);                   pauseBtn.setEnabled(true);               }           }       });       pauseBtn.setOnClickListener(new View.OnClickListener() {           @Override           public void onClick(View v) {               mService.pauseMission();                   v.setEnabled(false);               startBtn.setEnabled(true);               isContinue=true;               startBtn.setText("继续");           }       });    }    /*    * 获取文件名    * */    private String getFileName(String url){        int start=url.lastIndexOf("/")+1;        int end=url.length();        return url.substring(start,end);    }    @Override    protected void onDestroy() {        /*        * 解绑定DownloadService,注销ProgressReceiver        * */        unregisterReceiver(mReceiver);        unbindService(conn);        super.onDestroy();    }}

用于初始化数据库的App类:
当然也可以在MainActivity中初始化数据库,为了代码简洁和更快地初始化数据库,就写着这里了。

package com.chen.capton.filedownload;import android.app.Application;import android.database.sqlite.SQLiteDatabase;/** * Created by CAPTON on 2017/1/9. */public class App extends Application {    public static App instances;    @Override    public void onCreate() {        super.onCreate();        setDatabase();        instances = this;    }    public static App getInstances(){        return instances;    }    /**     * 设置greenDao     */    private DaoMaster.DevOpenHelper mHelper;    private SQLiteDatabase db;    private DaoMaster mDaoMaster;    private DaoSession mDaoSession;    private void setDatabase() {        mHelper = new DaoMaster.DevOpenHelper(this, "Dishes-db", null);        db = mHelper.getWritableDatabase();        mDaoMaster = new DaoMaster(db);        mDaoSession = mDaoMaster.newSession();    }    public DaoSession getDaoSession() {        return mDaoSession;    }    public SQLiteDatabase getDb() {        return db;    }}

接下来就是贴各个类的代码了

服务类DownloadService:

package com.chen.capton.filedownload;import android.app.Service;import android.content.Intent;import android.os.Binder;import android.os.IBinder;/** 实现DownloadTask.RefreshProgressListener接口* */public class DownloadService extends Service implements DownloadTask.RefreshProgressListener{    private final String REFRESH_PROGRESS="REFRESH_PROGRESS"; //设置Action    private DownloadTask task;     private Intent intent;    @Override    public IBinder onBind(Intent intent) {        return new MyBinder();    }    class MyBinder extends Binder {        public DownloadService getService(){            intent=new Intent();            intent.setAction(REFRESH_PROGRESS);            return DownloadService.this;        }    }    /*    * 供DownloadTask回调的方法,用于发送刷新进度的广播    * */    @Override    public void refressProgress(int progress) {        intent.putExtra("progress",progress); //将进度值写入intent        sendBroadcast(intent);  //发送广播    }    public void startMission(String url,int maxProgress){        task=new DownloadTask(url,maxProgress);        task.setRefreshProgressListener(this);        task.startMission();    };    public void pauseMission(){        task.pauseMission();    }    public void continueMission(){        task.continueMission();    }}

广播类ProgressReceiver:

package com.chen.capton.filedownload;import android.content.BroadcastReceiver;import android.content.Context;import android.content.Intent;import android.os.Handler;/** * Created by CAPTON on 2017/1/9. */public class ProgressReceiver extends BroadcastReceiver {    private Handler handler;  //保存从MainActivity传来的handler;    private final String REFRESH_PROGRESS="REFRESH_PROGRESS";//设置Action    public ProgressReceiver(Handler handler) {        this.handler=handler;    }    @Override    public void onReceive(Context context, Intent intent) {        //判断Action是否一致         if(intent.getAction().equals(REFRESH_PROGRESS)){             //从传来的intent中获取进度值             int progress=intent.getIntExtra("progress",0);             //将带有进度值的intent发送出去,交与MainActivity中的handler处理             handler.sendEmptyMessage(progress);          }    }}

下载类(重点)DownloadTask:

package com.chen.capton.filedownload;import android.os.Environment;import android.util.Log;import java.io.BufferedReader;import java.io.DataInputStream;import java.io.File;import java.io.FileNotFoundException;import java.io.IOException;import java.io.InputStream;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;/** * Created by CAPTON on 2017/1/9. */public class DownloadTask {    private String url;           //下载地址    private ThreadInfo threadInfo;//线程信息    private ThreadInfoDao dao; //数据库入口对象    private DownloadThread thread; //下载线程    public boolean isPause;//是否断开连接的标志位,很关键,呵呵    private int maxProgress; //最大进度    public int filedLength;  //文件长度    public int finishedLength; //完成的文件长度(待保存的文件进度)    private RefreshProgressListener listener;//进度刷新的监听器用于调用Service的更新方法    public DownloadTask(String url,int maxProgress) {        this.url = url;        this.maxProgress=maxProgress;        threadInfo=new ThreadInfo();        DaoSession session=App.getInstances().getDaoSession();  //从App中获取初始化好的DaoSession对象        dao=session.getThreadInfoDao();        thread=new DownloadThread(url,threadInfo,maxProgress);    }    /*    * 开始下载线程    * */    public void startMission(){          thread.start();    }    /*    * 暂停任务,即跳出while循环,将线程信息保存到数据库    * */    public void pauseMission(){         isPause=true;        ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique();        //第一次保存进度时插入纪录,之后更新纪录即可        if(info==null) {            info=new ThreadInfo(null, filedLength, finishedLength);            dao.insert(info);        }else {            info.setFileLength(filedLength);            info.setFinishedLength(finishedLength);            dao.update(info);        }    }    /*    *  从数据库读取上次保存的线程信息,新建线程从指定位置下载剩下的部分    * */    public void continueMission(){        isPause=false;        ThreadInfo info=dao.queryBuilder().where(ThreadInfoDao.Properties.Id.eq(1)).build().unique();        if(info!=null){            thread=new DownloadThread(url,info,maxProgress);            thread.start();        }    }    /*    * 下载线程,核心    * */    class DownloadThread extends Thread{       private String url;        private ThreadInfo threadInfo;        private HttpURLConnection conn; //httpUrl连接        private InputStream is;   //输入流        private File fileDir; //建立一个文件夹存放文件        private File file;        private RandomAccessFile raFile;  //可随机读写的File类,实际又不是继承File,呵呵,断点续传必用。        private int maxProgress;        public DownloadThread(String url, ThreadInfo threadInfo, int maxProgress) {            this.url = url;            this.threadInfo=threadInfo;            this.maxProgress=maxProgress;            fileDir=new File(Environment.getExternalStorageDirectory(),"test");            if(!fileDir.exists()){                fileDir.mkdir(); //第一次下载时,应该没有text目录,新建一个            }            file=new File(fileDir,getFileName(url));            try {                raFile=new RandomAccessFile(file,"rw"); //设置文件读写模式,"rw"为可读可写            } catch (FileNotFoundException e) {                e.printStackTrace();            }        }        public void run(){            /*            * 第一次连接,先获取文件长度,供后续设置文件的传输范围(byte)            * */                    try {                        URL Url = new URL(url);                        HttpURLConnection conn= (HttpURLConnection) Url.openConnection();                        conn.setRequestMethod("GET");                        conn.setReadTimeout(3000);                        filedLength=conn.getContentLength();                        threadInfo.setFileLength(filedLength);                        conn.disconnect();                    } catch (IOException e) {                        e.printStackTrace();                    }            /*            * 第二次连接,根据文件长度,上次保存的进度,设置传输范围,建立连接开始下载            * */                    try {                        URL Url=new URL(url);                        conn= (HttpURLConnection) Url.openConnection();                        conn.setRequestMethod("GET");  //设置连接方式                        conn.setReadTimeout(5000);  //设置连接超时                        //设置传输的范围,例如"bytes=0-1231540",‘-’后面没写说明结束端为传输文件的最后一字节                        conn.setRequestProperty("Range","bytes="+threadInfo.getFinishedLength()+"-"+threadInfo.getFileLength());                        conn.connect();                        is=conn.getInputStream();  //从连接对象中获取输入流                        //数据输入流,也可以用BufferedInputStream;                        DataInputStream dis=new DataInputStream(is);                        try {                            //设置一定的延时,等待服务器响应报文                            Thread.sleep(1800);                        } catch (InterruptedException e) {                            e.printStackTrace();                        }                        /*                        * 根据响应码判断是否成功连接服务器                        * */                        if(conn.getResponseCode()==HttpURLConnection.HTTP_OK||                                conn.getResponseCode()==HttpURLConnection.HTTP_PARTIAL) {                            raFile.seek(threadInfo.getFinishedLength());//跳转至上一次暂停时保存的位置                            byte[] b = new byte[1024];  //设置byte数组,大小适度即可;                            int len;    //每次写入b中的实际字节数                            long now=System.currentTimeMillis(); //设置循环初始时间                            while ((len = dis.read(b)) != -1) {                                raFile.write(b, 0, len); //将保存在b中的数据写入文件                                finishedLength += len;   //累加下载长度                                 /*                                 * 判断文件(下载)写入消耗的时间是否大于100ms,若是才跟新进度,不设置的话,                                 * 刷新频率=文件长度(很大的数)/1kb,会明显地限制传输速度                                 * */                                if(System.currentTimeMillis()-now>=100) {                                    now=System.currentTimeMillis();            //根据公式算出实际进度大小,然后调用DownloadService的实现方法refressProgress(int progress);                                    listener.refressProgress((int) ((long) finishedLength * maxProgress / threadInfo.getFileLength()));                                }else {        /*当文件(下载)写入消耗的时间小于100ms时,判断是否下载完成,若是则把进度设置为最大这个判断存在的意义在于,当文件下载完全时,消耗时间又小于100ms,进度显示为100%, 若不设置,则进度显示会卡在90%-100%之间,文件越小,显示误差越大*/                                    if (finishedLength>=threadInfo.getFileLength()){                                        listener.refressProgress(maxProgress);                                    }                                }                                if (isPause) {                                    break;                                }                            }                            threadInfo.setFinishedLength(finishedLength); //保存下载信息                            //写入完毕或者暂停则断开连接                            is.close();                            conn.disconnect();                        }                    } catch (IOException e) {                        e.printStackTrace();                    }        }    };    /*    * 设置回调方法,和回调接口,让DownloadService实现接口用于刷新进度。    * */    public void setRefreshProgressListener(RefreshProgressListener listener){         this.listener=listener;    }    public interface RefreshProgressListener{        void refressProgress(int progress);    }    private String getFileName(String url){        int start=url.lastIndexOf("/")+1;        int end=url.length();        return url.substring(start,end);    }}

线程信息类ThreadInfo:

package com.chen.capton.filedownload;import org.greenrobot.greendao.annotation.Entity;import org.greenrobot.greendao.annotation.Id;import org.greenrobot.greendao.annotation.Generated;/** * Created by CAPTON on 2017/1/9. *///声明此类是GreenDao框架的Entity实体类@Entitypublic class ThreadInfo {    //声明这是自增的唯一键    @Id    private Long id;    private int fileLength;    private int finishedLength;    /*    *用Android Studio 点击Build选项下的"Make Project",后面的代码会自动生成    */    @Generated(hash = 956576157)    public ThreadInfo(Long id, int fileLength, int finishedLength) {        this.id = id;        this.fileLength = fileLength;        this.finishedLength = finishedLength;    }    @Generated(hash = 930225280)    public ThreadInfo() {    }    public Long getId() {        return this.id;    }    public void setId(Long id) {        this.id = id;    }    public int getFileLength() {        return this.fileLength;    }    public void setFileLength(int fileLength) {        this.fileLength = fileLength;    }    public int getFinishedLength() {        return this.finishedLength;    }    public void setFinishedLength(int finishedLength) {        this.finishedLength = finishedLength;    }}

代码就全部贴完了,至于思路是怎么屡清楚的,主要看你对各个知识点掌握的熟练程度。


3.设计思路

写这个小demo,我的思路是:
❶,先要明确目标:文件的单线程断点续传,首先单线程先不管,后面还有多线程呢,断点续传是重点,说道断点续传你就应该明白要保存暂停时的进度了,保存进度需要用什么途径?SharePreference,SQLite(这里采用的途径),文件保存?如果是单线程,只需要保存一个进度信息,SharePreference是最方便了,把文件长度,已下载长度两个参数写进xml文件里就行了,需要续传文件时从xml里读就行了,当然这只是最简单的情况;如果是多线程下载时,比如10个线程,你就需要至少20个命名空间来保存参数,这样很麻烦,读写信息都很麻烦,不如数据库来的方便了。至于用文件存储信息就不要去想了,更麻烦,可以自行琢磨。
❷,信息的保存方式确定了,接下来是考虑如何下载文件了,当然不要去用什么框架来下载了,自己手写,从http连接开始写,到文件写入,关闭http连接为止,都自己写。这里Http连接用到的是HttpUrlConnection,也可以用HttpClient。文件的来源弄懂了,然后是文件输出,断点续传要用到RandomAccessFile这个类,可以实现文件的随机位置的读写,当然这个类并不是继承File类,而是实现 DataOutput, DataInput, Closeable这几个接口,所以我们在导入数据的时候用DataInputStream比较好,BufferdInputStream也是可以的。
❸文件如何下载写入和保存搞定了,接下来是进度刷新的问题,这个就涉及到Service,BroadCastReceiver,Handler这三大块的知识了,把这些基础知识先掌握好。其实我刚开始学的时候没用Service,BroadCastReceiver,直接在MainActivity里写,当然代码量就吓人了,结构也看起来很复杂,不过感觉下载速度确实最快(估计是Service不用一直发送消息给BroadCastReceiver,发送一个消息虽然很快,但是进度是不断刷新的,积少成多,我们的设备要处理的消息就多了,效率就下降了,从而影响文件传输速度了)。
❹各大类的调用关系
(1)MainActivity(点击”开始”,”暂停”按钮)调用DownloadService内的方法
(2)DownloadService继续调用DownloadTask的相应方法
(3)DownloadTask开始其中的下载线程,线程下载一定字节的后,回调DownloadService的refressProgress(int progress)方法
(4)refressProgress方法发送广播给ProgressReceiver,ProgressReceiver根据发来的信息通过Handler转发到MainActivity的Handler中
(5)MainActivity中的Handler收到最终消息,更新UI。
若是点击”暂停”按钮,“开始”按钮变为“继续”按钮,->(3)中跳出文件读取的循环,并把进度写入数据库,再次点击“继续”按钮续传文件,DownloadTask读取数据库数据,重新开始(3)(4)(5)。

前面的是大致思路,具体细节要深入到代码里去剖析,如果你是大神,余光一瞥就能理解每一行代码的用意;如果你对各个知识掌握的还不够熟练,可能就卡在某处了。几乎所有重要的方法和类我都一一注释了其用意了,剩下一些繁文缛节就没有叙述了,希望大家都能明白。

建议:
❶最好在个人电脑上构建一个局域网服务器,通过局域网来测试下载任务(不用流量),我用的是WAMP,简单粗暴,把文件丢进根目录下的“www”目录即可通过类似“http://192.168.1.10x/xxx.xxx“的url找到你的文件。建好服务器启动后发现手机无法访问地址,可以试试关闭电脑的防火墙(百试不厌),当然用完最好恢复回去。
❷文件最好选择一些设备需要检查其完整性的格式,如zip,rar,apk, 尝试打开文件来检验是否下载完整
❸手机最好已经root,方便查看数据库的信息(注意:GreenDao输出的数据库格式可能不是以db结尾(受初始化时的命名决定),手动改成”.db”结尾就可以打开了)。这里推荐“RE文件管理器”这款软件来查看root后的手机文件夹。当然不root的话,如果是数据出错,就手动写代码检查数据库喽。


4.相关文件

单线程断点续传 demo apk 链接:http://pan.baidu.com/s/1sltoHrB 密码:v4tj
完整项目demo 链接:http://pan.baidu.com/s/1kVBbTcj 密码:qcb7

1 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 汽车充电口坏了怎么办 如果手机充不了电怎么办 淘宝买的家电坏了怎么办 衣服皱了没有熨斗怎么办 油烟机油盒坏了怎么办 实体店不给换货怎么办 台式电脑鼠标不动了怎么办 电脑开机一直长鸣报警怎么办 国外电话卡网速太慢怎么办 滴滴提现忘记登录密码怎么办 微信提现支付密码忘记了怎么办 小米手机前置摄像头打不开怎么办 mp4视频屏幕好小怎么办 投了简历没回应怎么办 借壳上市原股票怎么办 红米note3指纹解锁失灵怎么办 来分期刷脸失败怎么办 买到假货商家不承认怎么办 买到假货翻新货工商局怎么办 三星note8笔丢了怎么办 夜跑结束后该怎么办 lol客户端正在运行中怎么办 瑞和宝终端锁定怎么办 骊爽摩托车动力弱怎么办? 换了手机跟点微信怎么办 手机送点插头换了怎么办 校园网总是显示有代理软件怎么办 电脑连接无线网络需要登录怎么办 智校园密码忘了怎么办 ivvi手机开不开机怎么办 跑鞋子大了一码怎么办 洗浴搓澡出汗多该怎么办 联华超市卡丢了怎么办 飞利浦电视的应用打不开怎么办 贵阳市下岗职工办理门面补贴怎么办 陌陌上被骗了色怎么办 七位数的座机要怎么办 开通米粉卡店铺预留电话号码怎么办 办信用卡没有单位电话怎么办 qq账号永久封停怎么办 qq号给冻结了怎么办