«

Android 断点续传的实现

时间:2024-3-2 18:17     作者:韩俊     分类: Android


这两天用了下android5.1下载功能,简直鸡肋。无奈自己研究了半天的断点续传,总结一下。本文参考了慕课网XRay_Chen大神的视频,在此表示衷心的感谢!顺便说明,作者本身还只是个菜鸟,因此我希望能写一篇能让菜鸟看得懂的博客

基础知识

断点续传指从文件上次中断的地方开始传送数据,而并非是从文件开头传送。

  因此我们可以从上面一句话得出这样一个结论,如果想要实现断点续传,必须保存断点。于是,我们可以定义这样一个普通类,用来保存文件的网络地址url,断点的位置finished(初始为0),以及结束位置end,简单代码如下

public class ThreadInfo {
    private int id;
    private String url;
    private long end;
    private long finished;

    public ThreadInfo() {
    }

    public ThreadInfo(int id, String url, long end, long finished) {
        this.id = id;
        this.url = url;
        this.end = end;
        this.finished = finished;
    }
    //set get方法

其次我们还需要定义一个普通的文件类,保存基本的文件信息,如文件大小,URL,名字等。

//注意,此类实线了Serializable接口,表明可也被序列化,主要是为了在Intent中传送此类
public class FileInfo implements Serializable {
    private String name;
    private String url;
    private long length;

    public FileInfo() {
    }

    public FileInfo(String name, String url, long length) {
        this.name = name;
        this.url = url;
        this.length = length;
    }
    //set get方法

  同样我们需要知道一些简单的http协议的内容

Header
作用
示例

Accept-Ranges
可以请求网页实体的一个或者多个子范围字段
Accept-Ranges: bytes

翻译成大白话来讲就是,你可以通过设置Range来下载文件的一部分内容
注意:其返回的状态码不是200(HttpURLConnection.HTTP_OK),而是206(HttpURLConnection.HTTP_PARTIAL),因此在安卓中我们这样使用这个Header,代码实例

    connection = (HttpURLConnection) new URL("Address").openConnection();
    connection.setConnectTimeout(5000);
    //使用GET
    connection.setRequestMethod("GET");
    connection.setRequestProperty("Range", "bytes="+start+"-"+end");
    //start与end表示你要下载的范围
    //判断返回的状态码
    if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
       doSomething();
    }

除了安卓的基本使用外,基础知识大概也就这么多.

页面布局

大致就是如下的样子,相当的简单
一个TextView 一个ProgressBar,两个Button

实现过程

定义普通的常量

public class Variable {
    public static final String ACTION_START = "action_start";
    public static final String ACTION_PAUSE = "action_pause";
    public static final String ACTION_UPDATE = "action_update";

    public static final String DB_NAME = "download";
    public static final String TABLE_NAME = "thread_info";

    public static final String URL = "http://openbox.mobilem.360.cn/index/d/sid/2147683";
    public static final String PATH = Environment.getExternalStorageDirectory().toString();
}

Service部分

我们的Activity实现的功能是向Service发送下载与暂停请求,Intent需要传递下载文件的对象,此时文件长度临时设置为0,当从网络获取到文件长度是再设置

fileInfo = new FileInfo("csu.apk", Variable.URL, 0);

然后通过按钮设置响应事件,向Service发送下载与暂停的请求

  start.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, DownloadService.class);
                //发送Variable.ACTION_START即开始下载请求
                intent.setAction(Variable.ACTION_START);
                intent.putExtra("file_info", fileInfo);
                startService(intent);
            }
        });

        pause.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, DownloadService.class);
                //发送Variable.ACTION_PAUSE,即暂停
                intent.setAction(Variable.ACTION_PAUSE);
                intent.putExtra("file_info", fileInfo);
                startService(intent);
            }
        });

从Service的生命周期可以看出,第一次启动服务时,运行 onCreate()->onStartCommand(),再次启动时只会调用onStartCommand(),因此我们可以重写onStartCommand()

  //如何解决开始与暂停,自定义DownloadTask类,之后有具体实现
    private DownloadTask downloadTask;
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (intent == null) return super.onStartCommand(null, flags, startId);
        //比较Intent.getAction的与Variable.ACTION_START是否一致,请求是否相同
        fileInfo = (FileInfo) intent.getSerializableExtra("file_info");
        if (intent.getAction().equals(Variable.ACTION_START)) {
            //开始下载准备
            //下载文件的基本信息
             download(fileInfo);//下载文件的基本信息
            //下载基本信息之后在下载下载文件内容
        } else if (intent.getAction().equals(Variable.ACTION_PAUSE)) {
           //暂停下载
           if (downloadTask != null) downloadTask.pause();
        }
        return super.onStartCommand(intent, flags, startId);
    }

下载基本的文件信息,与网络有关,开线程下载

    private void download(FileInfo fileInfo) {
        new InitThread(fileInfo, this).start();
    }

    private class InitThread extends Thread {

        FileInfo fileInfo;
        Context context;

        public InitThread(FileInfo fileInfo, Context context) {
            this.fileInfo = fileInfo;
            this.context = context;
        }

        @Override
        public void run() {
            HttpURLConnection connection;
            //RandomAccessFile利用文件指针,可以从给定的位置开始读写数据
            RandomAccessFile raf;
            try {
                connection = (HttpURLConnection) new URL(fileInfo.getUrl()).openConnection();
                connection.setConnectTimeout(3000);
                //获取文件文件长度
                long length = -1;
                if (connection.getResponseCode() == HttpURLConnection.HTTP_OK)
                    length = connection.getContentLength();
                if (length <= 0) return;
                //在SD创建一个与带下载文件相同大小的文件
                File file = new File(Variable.PATH, fileInfo.getName());
                raf = new RandomAccessFile(file, "rwd");
                raf.setLength(length);
                //设置文件长度
                fileInfo.setLength(length);
                //向handler发送开始下载的请求,下载开始
                handler.obtainMessage(MSG_INT, fileInfo).sendToTarget();
            } catch (IOException e) {
                e.printStackTrace();
            }
            super.run();
        }
    }

Service中定义handler开始下载

    private Handler handler = new Handler() {
        @Override
        //处理发送消息
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == MSG_INT) {
                FileInfo fileInfo = (FileInfo) msg.obj;
                //开始下载fileInfo
              downloadTask = new DownloadTask(DownloadService.this, fileInfo);
               downloadTask.download();
            }
        }
    };

恩,现在Service已经实现.

DownloadTask部分

主要实现文件的下载与暂停的控制,以及定时的发给主线程完成进度的消息.

下载/暂停部分

这一部分涉及到断点类ThreadInfo的基本操作,由于中断,所以需要我们及时的把当前的断点保存起来,这里选择使用数据库,当然你也可以选择其他方式,在此不在赘述数据库的实现,只是提供一个基本的接口

public interface ThreadDB {
    public void insert(ThreadInfo info);

    public void update(int id, String url, long finished);

    public void delete(int id, String url);

    public List<ThreadInfo> getThreadInfo(String url);

    public boolean isExists(String url, int id);

}

下载/暂停的基本实现

public class DownloadTask {
    private Context context;
    private FileInfo fileInfo;
    //数据库的操作
    private ThreadDB impl;

    //已完成
    private long finished = 0;

    //是否暂停
    public boolean isPause = false;
    private ThreadInfo thread_info;

    public DownloadTask(Context context, FileInfo fileInfo) {
        this.context = context;
        this.fileInfo = fileInfo;
        impl = new ThreadDBImpl(context);
    }

    public void download() {
        isPause = false;
        List<ThreadInfo> thread_infos = impl.getThreadInfo(fileInfo.getUrl());
        //从数据库获取断点线程信息
        if (thread_infos.size() == 0)
            thread_info = new ThreadInfo(0, fileInfo.getUrl(), fileInfo.getLength(), 0);
        else
            thread_info = thread_infos.get(0);
        //下载,仍旧开线程
        new DownloadThread(thread_info).start();
    }

    //暂停方法
    public void pause() {
        isPause = true;
    }
    //下载线程

}

下载线程私有类实现

private class DownloadThread extends Thread {
        ThreadInfo thread_info;
        Intent intent;

        public DownloadThread(ThreadInfo thread_info) {
          //从数据库活取的最新断点信息
            this.thread_info = thread_info;
            //发送广播的intent
            intent = new Intent(Variable.ACTION_UPDATE);
        }

        @Override
        public void run() {
            if (!impl.isExists(thread_info.getUrl(), thread_info.getId()))
                impl.insert(thread_info);
            HttpURLConnection connection = null;
            RandomAccessFile raf = null;
            InputStream input = null;

            try {
                connection = (HttpURLConnection) new URL(thread_info.getUrl()).openConnection();
                connection.setConnectTimeout(5000);
                connection.setRequestMethod("GET");
                //获取start与end
                long start = thread_info.getFinished();
                long end = thread_info.getEnd();
                //http获取部分内容
                connection.setRequestProperty("Range", "bytes=" + start + "-" + end);

                File file = new File(Variable.PATH, fileInfo.getName());
                raf = new RandomAccessFile(file, "rwd");
                raf.seek(start);
                finished = thread_info.getFinished();

                if (connection.getResponseCode() == HttpStatus.SC_PARTIAL_CONTENT) {
                    input = connection.getInputStream();
                    byte[] buffer = new byte[1024 * 4];
                    int len = -1;
                    long time = System.currentTimeMillis();
                    while ((len = input.read(buffer)) != -1) {
                    //写入数据
                        raf.write(buffer, 0, len);
                        finished += len;
                        //当前进度
                        long progress = finished * 100 / fileInfo.getLength();
                        //定时发送广播
                        ...
                        ...
                        //判断是否停止下载
                        if (isPause) {
                          //停止下载,保存当前最新断点信息
                            impl.update(thread_info.getId(), thread_info.getUrl(), finished);
                            return;
                        }
                    }
                    //下载完成后,删除数据库中的信息
                    impl.delete(thread_info.getId(), thread_info.getUrl());
                }

            } catch (IOException ignored) {
            } finally {
                try {
                    if (connection != null) connection.disconnect();
                    if (raf != null) raf.close();
                    if (input != null) input.close();
                } catch (Exception ignored) {
                }
            }
            super.run();
        }
    }

广播部分

发送广播

     //每隔500毫秒发送进度,避免发送频繁导致UI线程崩溃
     if (System.currentTimeMillis() - time > 500 || progress == 100) {
            time = System.currentTimeMillis();
            intent.putExtra("value", progress);
            context.sendBroadcast(intent);
        }

Activity部分

Activity部分除了控制UI还有接受广播,更新下载进度

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //省略UI代码
        //定义IntentFilter,注册广播
        IntentFilter filter = new IntentFilter();
        filter.addAction(Variable.ACTION_UPDATE);
        registerReceiver(receiver, filter);

    }

    BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Variable.ACTION_UPDATE)) {
            //当接收到广播,更新进度
                long finished = intent.getLongExtra("value", 0);
                progressBar.setProgress((int) finished);
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //
        unregisterReceiver(receiver);
    }

总结

终于写完了,由于篇幅过长,在此,总结一下
大致过程

当然,楼主也提供了响应的源代码 github

2015年5月24日 于418

标签: android

热门推荐