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

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实体类
@Entity
public 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

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值