多线程下载是加快下载速度的一种方式,通过开启多个线程去执行一个任务,可以使任务的执行速度变快。多线程的任务下载时常都会使用得到断点续传下载,就是我们在一次下载未结束时退出下载,第二次下载时会接着第一次下载的进度继续下载。对于android中的下载,我想分多个部分去讲解分析。今天,我们就首先开始android中下载断点续传代码的实现。源码下载:java多线程断点续传(一) 。关于多线程下载单个文件的实现,请参见博客:android程序---->android多线程下载(二)

目录导航

  1. android中断点续传的思路
  2. android断点续传基本的UI
  3. android断点续传的工具类
  4. 下载暂停取消的具体流程
  5. 友情链接

android中断点续传的思路

一、 断点续传的实现步骤:

第一步: 我们要获得下载资源的的长度,用http请求中HttpURLConnection的getContentLength()方法

第二步:在本地创建一个文件,设计其长度。File file = new File()

第三步:从数据库中获得上次下载的进度,当暂停下载时,存储下载的状态,用到数据库的知识

第四步:从上次下载的位置下载数据,同时保存进度到数据库:RandomAccessFile的seek方法与HttpURLConnection的setRequestProperty方法

第五步:将下载进度回传到Activity,可以通过Intent将数据广播到Activity中

第六步:下载完成后删除下载信息,在数据库中删除相应的信息

二、 断点续传实现的流程图:

android断点续传基本的UI编写

明白了上述的实现流程,现在我们开始一个android项目,开始断点续传代码的编写,项目结构如下:

运行的截图如下:

一、 编写基本的UI,三个TextView,分别显示文件名、下载进度和下载速度,一个ProgressBar。二个Button,分别用于开始下载、暂停下载和取消下载。

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical"    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.example.linux.continuedownload.MainActivity">    <LinearLayout        android:layout_width="match_parent"        android:layout_height="wrap_content">    <TextView        android:id="@+id/textView"        android:layout_width="wrap_content"        android:layout_height="wrap_content" />    <TextView        android:layout_marginLeft="80dp"        android:id="@+id/progress"        android:layout_width="wrap_content"        android:layout_height="wrap_content" />    <TextView        android:layout_marginLeft="80dp"        android:id="@+id/speed"        android:layout_width="wrap_content"        android:layout_height="wrap_content" />    </LinearLayout>    <ProgressBar        android:visibility="invisible"        android:id="@+id/progressBar"        style="?android:attr/progressBarStyleHorizontal"        android:layout_width="match_parent"        android:layout_height="wrap_content" />    <LinearLayout        android:layout_width="match_parent"        android:layout_height="wrap_content">        <Button            android:id="@+id/start"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="开始下载" />        <Button            android:layout_marginLeft="20dp"            android:id="@+id/stop"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="暂停下载" />        <Button            android:layout_marginLeft="20dp"            android:id="@+id/cancel"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="取消下载" />    </LinearLayout></LinearLayout>

二、 在MainActivity中初始化一些组件,绑定按钮的事件:

在onCreate方法中初始化一些组件:

// 初始化组件textView = (TextView) findViewById(R.id.textView);progressView = (TextView) findViewById(R.id.progress);speedView = (TextView) findViewById(R.id.speed);progressBar = (ProgressBar) findViewById(R.id.progressBar);progressBar.setMax(100);startButton = (Button) findViewById(R.id.start);stopButton = (Button) findViewById(R.id.stop);cancelButton = (Button) findViewById(R.id.cancel);// 创建一个文件信息对象final FileInfo fileInfo = new FileInfo(0, fileUrl, "huhx.apk", 0, 0);

在onCreate方法中绑定开始下载按钮事件:点击start按钮,设置进度条可见,并且设置start的Action,启动服务。

startButton.setOnClickListener(new View.OnClickListener() {    @Override    public void onClick(View v) {        textView.setText(fileInfo.getFileName());        progressBar.setVisibility(View.VISIBLE);        // 通过Intent传递参数给service        Intent intent = new Intent(MainActivity.this, DownloadService.class);        intent.setAction(DownloadService.ACTION_START);        intent.putExtra("fileInfo", fileInfo);        startService(intent);    }});

在onCreate方法中绑定暂停下载按钮事件:点击stop按钮,设置stop的Action,启动服务。

stopButton.setOnClickListener(new View.OnClickListener() {    @Override    public void onClick(View v) {        // 通过Intent传递参数给service        Intent intent = new Intent(MainActivity.this, DownloadService.class);        intent.setAction(DownloadService.ACTION_STOP);        intent.putExtra("fileInfo", fileInfo);        startService(intent);    }});

在onCreate方法中绑定取消下载按钮事件:点击cancel按钮,设置cancel的Action,启动服务,之后更新UI。

cancelButton.setOnClickListener(new View.OnClickListener() {    @Override    public void onClick(View v) {        // 通过Intent传递参数给service        Intent intent = new Intent(MainActivity.this, DownloadService.class);        intent.setAction(DownloadService.ACTION_CANCEL);        intent.putExtra("fileInfo", fileInfo);        startService(intent);        // 更新textView和progressBar的显示UI        textView.setText("");        progressBar.setVisibility(View.INVISIBLE);        progressView.setText("");        speedView.setText("");    }});

注册广播,用于Service向Activity传递一些下载进度信息:

// 静态注册广播IntentFilter intentFilter = new IntentFilter();intentFilter.addAction(DownloadService.ACTION_UPDATE);registerReceiver(broadcastReceiver, intentFilter);/** * 更新UI */BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {    @Override    public void onReceive(Context context, Intent intent) {        if (DownloadService.ACTION_UPDATE.equals(intent.getAction())) {            int finished = intent.getIntExtra("finished", 0);            int speed = intent.getIntExtra("speed", 0);            Log.i("Main", finished + "");            progressBar.setProgress(finished);            progressView.setText(finished + "%");            speedView.setText(speed + "KB/s");        }    }};

三、 在AndroidManifest.xm文件中声明权限,定义服务

<service android:name="com.huhx.services.DownloadService" android:exported="true" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

android断点续传的工具类

二、 我们定义一些实体类,用于断点续传过程的信息的良好封装:

下载文件信息: 省略了get和set方法,以及toString和构造方法

public class FileInfo implements Serializable{    // 文件Id,用于标识文件    private int fileId;    // 文件的下载地址    private String url;    // 文件的名称    private String fileName;    // 文件的长度,也就是大小    private int length;    // 文件已经的下载量    private int finished;}

下载资源的线程信息:省略同上

public class ThreadInfo {    // 线程ID    private int threadId;    // 下载资源的地址    private String url;    //下载资源的开始处    private int start;    //下载资源的结束处    private int end;    //资源已经的下载量    private int finished;}

三、 我们开始数据库方面的编写,它用于存储更新线程的下载的进度信息

首先我们要创建一个数据库的工具类:

package com.huhx.util;import android.content.Context;import android.database.sqlite.SQLiteDatabase;import android.database.sqlite.SQLiteOpenHelper;/** * Created by huhx on 2016/4/9. */public class SqliteDBHelper extends SQLiteOpenHelper {    private static final String DB_NAME = "download.db";    private static final int version = 1;    private static final String CREATE_THREADINFO = "create table thread_info(_id integer primary key autoincrement, " +            "thread_id integer, url text, start integer, end integer, finished integer)";    private static final String DROP_THREADINFO = "drop table if exists thread_info";    public SqliteDBHelper(Context context) {        super(context, DB_NAME, null, version);    }    @Override    public void onCreate(SQLiteDatabase db) {        db.execSQL(CREATE_THREADINFO);    }    @Override    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {        db.execSQL(DROP_THREADINFO);        db.execSQL(CREATE_THREADINFO);    }}

定义一个Dao接口,用于数据库对线程信息的CRUD操作:

/** * Created by Linux on 2016/4/9. */public interface ThreadDao {    // 插入线程信息    public void insertThread(ThreadInfo threadInfo);    // 删除线程信息    public void deleteThread(String url, int threadId);    // 删除所有关于这个url的线程    public void deleteThread(String url);    // 更新线程信息    public void updateThread(String url, int threadId, int finished);    // 查询线程信息    public List<ThreadInfo> queryThread(String url);    // 线程信息是否存在    public boolean isThreadInfoExist(String url, int threadId);}

具体实现上述Dao的Impl类:

package com.huhx.util;import android.content.Context;import android.database.Cursor;import android.database.sqlite.SQLiteDatabase;import com.huhx.model.ThreadInfo;import java.util.ArrayList;import java.util.List;/** * Created by huhx on 2016/4/9. */public class ThreadDaoImpl implements ThreadDao {    private SqliteDBHelper sqliteDBHelper;    public ThreadDaoImpl(Context context) {        sqliteDBHelper = new SqliteDBHelper(context);    }    @Override    public void insertThread(ThreadInfo threadInfo) {        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();        Object[] objects = new Object[]{                threadInfo.getThreadId(), threadInfo.getUrl(), threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished()        };        database.execSQL("insert into thread_info(thread_id, url, start, end, finished) values(?,?,?,?,?)", objects);        database.close();    }    @Override    public void deleteThread(String url, int threadId) {        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();        Object[] objects = new Object[]{                url, threadId        };        database.execSQL("delete from thread_info where url = ? and thread_id = ?", objects);        database.close();    }    @Override    public void deleteThread(String url) {        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();        Object[] objects = new Object[]{                url        };        database.execSQL("delete from thread_info where url = ?", objects);        database.close();    }    @Override    public void updateThread(String url, int threadId, int finished) {        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();        Object[] objects = new Object[]{                finished, url, threadId        };        database.execSQL("update thread_info set finished = ? where url = ? and thread_id = ?", objects);        database.close();    }    @Override    public List<ThreadInfo> queryThread(String url) {        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();        List<ThreadInfo> threadInfos = new ArrayList<>();        Cursor cursor = database.rawQuery("select * from thread_info where url = ?", new String[]{url});        while (cursor.moveToNext()) {            ThreadInfo threadInfo = new ThreadInfo();            threadInfo.setThreadId(cursor.getInt(cursor.getColumnIndex("thread_id")));            threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));            threadInfo.setStart(cursor.getInt(cursor.getColumnIndex("start")));            threadInfo.setEnd(cursor.getInt(cursor.getColumnIndex("end")));            threadInfo.setFinished(cursor.getInt(cursor.getColumnIndex("finished")));            threadInfos.add(threadInfo);        }        cursor.close();        database.close();        return threadInfos;    }    @Override    public boolean isThreadInfoExist(String url, int threadId) {        SQLiteDatabase database = sqliteDBHelper.getWritableDatabase();        Cursor cursor = database.rawQuery("select * from thread_info where url = ? and thread_id = ?", new String[]{url, threadId+""});        boolean isExist = cursor.moveToNext();        cursor.close();        database.close();        return isExist;    }}

下载暂停取消的具体流程

四、 最后我们开始最重要的Service以及核心的下载代码的编写,我们按照上述的开始、暂停、取消的顺序,来讲解断点续传的实现过程。

我们在DownloadService中onStartCommand方法中接收的Intent,关于Service的使用请参见:android基础---->service的生命周期

@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {    // 获得Activity传过来的参数    if (ACTION_START.equals(intent.getAction())) {        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");        // 启动初始化线程        new InitThread(fileInfo).start();    } else if (ACTION_STOP.equals(intent.getAction())) {        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");        if (downloadTask != null) {            downloadTask.isPause = true;        }    } else if (ACTION_CANCEL.equals(intent.getAction())) {        FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");        if (downloadTask != null) {            downloadTask.isPause = true;        }        // 删除本地文件        File file = new File(DOWNLOAD_PATH, fileInfo.getFileName());        if (file.exists()) {            file.delete();        }        handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();    }    return super.onStartCommand(intent, flags, startId);}

五、 文件的开始下载流程:

开始下载时,启动一个初始化线程,并把文件信息传递给线程,该线程通过Http请求得到文件的长度,在本地创建下载文件的载体,设置大小并发送下载的消息给Handler:

/** * 初始化子线程 */class InitThread extends Thread {    private FileInfo fileInfo = null;    public InitThread(FileInfo fileInfo) {        this.fileInfo = fileInfo;    }    @Override    public void run() {        // 连接网络文件        HttpURLConnection connection = null;        RandomAccessFile randomAccessFile = null;        try {            URL url = new URL(fileInfo.getUrl());            connection = (HttpURLConnection) url.openConnection();            connection.setConnectTimeout(3000);            connection.setRequestMethod("GET");            connection.connect();            int length = -1;            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {                // 获取文件的长度                length = connection.getContentLength();            }            if (length <= 0) {                return;            }            // 在本地创建文件            File dir = new File(DOWNLOAD_PATH);            if (dir.exists()) {                dir.mkdir();            }            File file = new File(dir, fileInfo.getFileName());            // 设置文件长度            randomAccessFile = new RandomAccessFile(file, "rwd");            randomAccessFile.setLength(length);            fileInfo.setLength(length);            handler.obtainMessage(DOWNLOAD_MESSAGE, fileInfo).sendToTarget();        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                randomAccessFile.close();                connection.disconnect();            } catch (IOException e) {                e.printStackTrace();            }        }    }}

handler接收消息,并加以处理:注意这里有两种消息,我们暂时只考虑DOWNLOAD_MESSAGE消息,它启动下载任务

private Handler handler = new Handler() {    @Override    public void handleMessage(Message msg) {        switch (msg.what) {            case DOWNLOAD_MESSAGE:                FileInfo fileInfo = (FileInfo) msg.obj;                // 启动下载任务                downloadTask = new DownloadTask(DownloadService.this, fileInfo);                downloadTask.download();                break;            case DOWNLOAD_CANCEL:                FileInfo fileCancelInfo = (FileInfo) msg.obj;                downloadTask = new DownloadTask(DownloadService.this);                downloadTask.cancelDownload(fileCancelInfo);                break;        }    }};

在download方法中,首先判断是否有线程下载过文件,如果没有就创建一个。有的话,从数据库直接得到。而且开启了下载的任务线程

public void download() {    // 读取数据库的线程信息    List<ThreadInfo> threadInfos = threadDao.queryThread(fileInfo.getUrl());    ThreadInfo threadInfo = null;    if (threadInfos.size() == 0) {        threadInfo = new ThreadInfo(0, fileInfo.getUrl(), 0, fileInfo.getLength(), 0);    } else {        threadInfo = threadInfos.get(0);    }    new DownloadThread(threadInfo).start();}

在下载的线程中,通过Http请求数据并通过字节流的方式存储在本地的文件中。间隔500毫秒,就发送一次更新UI的广播。如果收到了暂停的信号,就暂停下载。在下载完成之后,删除数据库中的线程信息

class DownloadThread extends Thread {    private ThreadInfo threadInfo = null;    public DownloadThread(ThreadInfo threadInfo) {        this.threadInfo = threadInfo;    }    @Override    public void run() {        // 向数据库插入线程信息        if (!threadDao.isThreadInfoExist(threadInfo.getUrl(), threadInfo.getThreadId())) {            threadDao.insertThread(threadInfo);        }        HttpURLConnection connection = null;        RandomAccessFile randomAccessFile = null;        InputStream inputStream = null;        try {            URL url = new URL(threadInfo.getUrl());            connection = (HttpURLConnection) url.openConnection();            connection.setConnectTimeout(5000);            connection.setRequestMethod("GET");            int start = threadInfo.getStart() + threadInfo.getFinished();            connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());            File file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFileName());            randomAccessFile = new RandomAccessFile(file, "rwd");            randomAccessFile.seek(start);            Intent intent = new Intent(DownloadService.ACTION_UPDATE);            // 开始下载            finished += threadInfo.getFinished();            if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {                inputStream = connection.getInputStream();                byte[] buffer = new byte[4 * 1024];                int len = -1;                long time = System.currentTimeMillis();                long time1;                while ((len = inputStream.read(buffer)) != -1) {                    randomAccessFile.write(buffer, 0, len);                    finished += len;                    if ((time1 = System.currentTimeMillis() - time) > 500) {                        time = System.currentTimeMillis();                        intent.putExtra("finished", finished * 100 / fileInfo.getLength());                        intent.putExtra("speed", (int) (len / time1));                        context.sendBroadcast(intent);                    }                    if (isPause) {                        threadDao.updateThread(threadInfo.getUrl(), threadInfo.getThreadId(), finished);                        return;                    }                }                // 删除线程信息,再次发送广播避免上面的广播延迟                intent.putExtra("finished", finished * 100 / fileInfo.getLength());                context.sendBroadcast(intent);                threadDao.deleteThread(threadInfo.getUrl(), threadInfo.getThreadId());                Log.i("Main", "finished: " + finished + ", and file length: " + fileInfo.getLength());            }        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                connection.disconnect();                randomAccessFile.close();                inputStream.close();            } catch (IOException e) {                e.printStackTrace();            }        }    }}

六、 文件的暂停下载流程:如果下载任务在启动,那么设置isPause为true,在上述的讲解中我们知道,此时字节流停止的传输。

if (downloadTask != null) {    downloadTask.isPause = true;}

七、 文件的取消下载流程:

暂停下载的流程,然后删除本地文件,最后发送取消下载的消息:

FileInfo fileInfo = (FileInfo) intent.getSerializableExtra("fileInfo");if (downloadTask != null) {    downloadTask.isPause = true;}// 删除本地文件File file = new File(DOWNLOAD_PATH, fileInfo.getFileName());if (file.exists()) {    file.delete();}handler.obtainMessage(DOWNLOAD_CANCEL, fileInfo).sendToTarget();

handler处理取消下载的消息:调用DownloadTask的cancelDownload方法,并把文件信息传入

case DOWNLOAD_CANCEL:        FileInfo fileCancelInfo = (FileInfo) msg.obj;        downloadTask = new DownloadTask(DownloadService.this);        downloadTask.cancelDownload(fileCancelInfo);        break;

在cancelDownload方法中删除数据库中的线程信息:

// 取消下载任务public void cancelDownload(FileInfo fileInfo) {    threadDao.deleteThread(fileInfo.getUrl());}

最后在MainActivity中更新UI:

// 更新textView和progressBar的显示UItextView.setText("");progressBar.setVisibility(View.INVISIBLE);progressView.setText("");speedView.setText("");

友情链接

  • handler的原理 android高级---->Handler的原理
  • service的生命周期 android基础---->service的生命周期
  • android中广播的使用 android基础---->Broadcast的使用
  • android中数据库的使用 android基础---->SQLite数据库的使用
  • 测试的断点续传源码下载

关于android中多线程的下载,请参见我的博客: android程序---->android多线程下载(二)

更多相关文章

  1. (续)Android热更新:Tinker热修复与Walle多渠道打包
  2. 2011.8.17---2011.8.18 续(android的classloader)
  3. android手动更新demo,当然最好还是用第三方,简单方便
  4. 彻底弄明白Gradle相关配置
  5. 非通话版平板设备在Play Store无法搜索下载百度地图和腾讯地图应
  6. Android(安卓)studio代码混淆
  7. android 实现主线程、子线程双向通信
  8. Android热修复原理总结
  9. 十大技巧优化Android(安卓)App性能

随机推荐

  1. RecyclerView 滚动条的显示与隐藏
  2. Android下雪动画 VS JS下雪动画
  3. Android 设置虚线分割线
  4. 使用 Kotlin 开发 Android 应用 | 8 个最
  5. Android 圆角图片的实现
  6. android GPS定位详解(6)
  7. 使用 Kotlin 开发 Android 应用 | 8 个最
  8. Some thing about android:visibility
  9. android 设定dns
  10. Android Wear Preview- 归档通知(Stacking