Android 多线程断点下载文件

效果图

首先先上一下效果图 如果有需要的朋友可以往下看我的具体实现:
在这里插入图片描述
整个下载的过程都在后台服务中进行,因此实现了一个activity将任务加入下载 在另一个activity中显示进度并能控制暂停,同时结合了sqlite进行缓存,将任务列表缓存到本地,下次打开app任务依然存在并能继续

实现的思路

1.创建一个sqlite的表 将任务列表中的 例如文件名 url 等信息进行缓存
2. 封装下载工具类
3. 创建后台服务 将下载的开始暂停等方法写到服务中

代码具体实现

一 创建DBHelper 并封装一些方法
public class DBHelper extends SQLiteOpenHelper {
    private static final String DB_NAME = "download.db";
    private static final int VERSION = 1;
    //state 1 是正在下载 2 是暂停  3是已经完成
    //thread_id(与文件id相同) 下载url 文件总长度 下载状态 title文件标题
    private static final String SQL_CREATE = "create table if not exists thread_info(" +
            "thread_id integer,url text,filelength integer,state integer,title text)";

    private static final String SQL_DROP = "drop table if exists thread_info";



    public DBHelper(Context context) {
        super(context, DB_NAME, null, VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL(SQL_DROP);
        db.execSQL(SQL_CREATE);
    }
}

封装增删改查的方法

public interface ThreadDao {
    //插入线程信息
    void insertThread(ThreadInfo threadInfo);

    //删除线程
    void deleteThread(String url, int thread_id);

    //更新线程信息
    void updateThread(String url, int thread_id,int state);


    //根据url查询文件的线程信息
    ThreadInfo getThreadsByUrl(String url);

    List<ThreadInfo> getAllThreads();

    //系统被杀死的时候没有完成的任务
    List<ThreadInfo> getAllContinueThreads();

    //线程信息是否存在
    boolean isExists(String url, int thread_id);
}

具体的sql 语句实现

public class ThreadDaoImpl implements ThreadDao {
    private DBHelper mHelper = null;

    public ThreadDaoImpl(Context context) {
        mHelper = new DBHelper(context);
    }

    @Override
    public synchronized void insertThread(ThreadInfo threadInfo) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        db.execSQL("insert into thread_info(thread_id,url,filelength,state,title) values (?,?,?,?,?)",
                new Object[]{threadInfo.getId(), threadInfo.getUrl(), threadInfo.getFilelength(), threadInfo.getState(), threadInfo.getTitle()});
        db.close();
    }

    @Override
    public synchronized void deleteThread(String url, int thread_id) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        db.execSQL("delete from thread_info where thread_id = ? and url = ?",
                new Object[]{thread_id, url});
        db.close();
    }

    @Override
    public synchronized void updateThread(String url, int thread_id, int state) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        db.execSQL("update thread_info set  state = ? where thread_id = ? and url = ?",
                new Object[]{state,thread_id, url});
        db.close();
    }

    @Override
    public synchronized ThreadInfo getThreadsByUrl(String url) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info where url = ?", new String[]{url});
        ThreadInfo threadInfo = null;
        while (cursor.moveToNext()) {
            threadInfo = new ThreadInfo();
            threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
            threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
            threadInfo.setFilelength(cursor.getInt(cursor.getColumnIndex("filelength")));
            threadInfo.setState(cursor.getInt(cursor.getColumnIndex("state")));
            threadInfo.setTitle(cursor.getString(cursor.getColumnIndex("title")));
        }
        cursor.close();
        db.close();
        return threadInfo;
    }

    @Override
    public List<ThreadInfo> getAllThreads() {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info", new String[]{});
        ThreadInfo threadInfo = null;
        List<ThreadInfo> list = new ArrayList<>();
        while (cursor.moveToNext()) {
            threadInfo = new ThreadInfo();
            threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
            threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
            threadInfo.setFilelength(cursor.getInt(cursor.getColumnIndex("filelength")));
            threadInfo.setState(cursor.getInt(cursor.getColumnIndex("state")));
            threadInfo.setTitle(cursor.getString(cursor.getColumnIndex("title")));
            list.add(threadInfo);
        }
        cursor.close();
        db.close();
        return list;
    }

    @Override
    public List<ThreadInfo> getAllContinueThreads() {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info where state = ?", new String[]{"1"});
        ThreadInfo threadInfo = null;
        List<ThreadInfo> list = new ArrayList<>();
        while (cursor.moveToNext()) {
            threadInfo = new ThreadInfo();
            threadInfo.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
            threadInfo.setUrl(cursor.getString(cursor.getColumnIndex("url")));
            threadInfo.setFilelength(cursor.getInt(cursor.getColumnIndex("filelength")));
            threadInfo.setState(cursor.getInt(cursor.getColumnIndex("state")));
            threadInfo.setTitle(cursor.getString(cursor.getColumnIndex("title")));
            list.add(threadInfo);
        }
        cursor.close();
        db.close();
        return list;
    }

    @Override
    public synchronized boolean isExists(String url, int thread_id) {
        SQLiteDatabase db = mHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info where url = ? and thread_id = ?",
                new String[]{url, thread_id + ""});
        while (cursor.moveToNext()) {
            cursor.close();
            db.close();
            return true;
        }
        return false;
    }
}
二 创建实体类

我们需要两个实体类 第一个实体类是做文件列表假数据用的 不需要state(是暂停还是正在下载) 第二个实体类是线程下载用的

文件的实体类

public class FileInfo implements Serializable {
    private int id;
    private String url;
    private String fileName;
    private int length;
    
    public FileInfo() {
    
    }

    public FileInfo(int id, String url, String fileName, int length) {
        this.id = id;
        this.url = url;
        this.fileName = fileName;
        this.length = length;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public int getLength() {
        return length;
    }

    public void setLength(int length) {
        this.length = length;
    }
    
}

线程的实体类

public class ThreadInfo {
    private int id;
    private String url;
    private long filelength;
    private int state;
    private String title;

    public ThreadInfo() {

    }

    public ThreadInfo(int id, String url, long filelength, int state, String title) {
        this.id = id;
        this.url = url;
        this.filelength = filelength;
        this.state = state;
        this.title = title;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public long getFilelength() {
        return filelength;
    }

    public void setFilelength(long filelength) {
        this.filelength = filelength;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}
三 创建下载工具类

下面我们创建一个下载的工具类 DownloadTask 断点下载的核心就是RandomAccessFile 这个类 通过seek() 这个方法能跳过已经下载的部分 另一个重点就是在请求头中加上range 下面看一下具体的代码

//下载任务类
public class DownloadTask {
    private Context context = null;
    private FileInfo fileInfo = null;
    private ThreadDao dao = null;
    public boolean isPause = false;

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

    public void download() {
        //读取数据库的线程任务信息
        ThreadInfo threadInfo = dao.getThreadsByUrl(fileInfo.getUrl());
        if (threadInfo == null) {
            //如果是空的初始化线程任务对象
            threadInfo = new ThreadInfo(fileInfo.getId(), fileInfo.getUrl(),  fileInfo.getLength(), 1, fileInfo.getFileName());
        }
        //创建子线程开始下载
        new DownloadThread(threadInfo).start();
    }

    //下载线程
    class DownloadThread extends Thread {
        ThreadInfo threadInfo = null;
        private File file;

        public DownloadThread(ThreadInfo threadInfo) {
            this.threadInfo = threadInfo;
        }

        @Override
        public void run() {
            super.run();
            //向数据库插入线程任务信息
            if (!dao.isExists(threadInfo.getUrl(), threadInfo.getId())) {
                //之前不存在这个线程任务就插入到数据库
                dao.insertThread(threadInfo);
            }
            HttpURLConnection httpURLConnection = null;
            RandomAccessFile raf = null;
            InputStream inputStream = null;
            try {
                Intent intent = new Intent(DownloadService.ACTION_UPDATA);

                //设置文件写入位置 同时判断手机中是否已经有下载的相同文件
                file = new File(DownloadService.DOWNLOAD_PATH, fileInfo.getFileName());

                if (file.length() == fileInfo.getLength()){
                    //已经有这个文件了
                    //下载完成之后发送广播
                    intent.putExtra("finished", 100 + "");
                    intent.putExtra("fileId", fileInfo.getId());
                    context.sendBroadcast(intent);
                    //下载完成之后更新线程任务信息
                    dao.updateThread(threadInfo.getUrl(), threadInfo.getId(), 3);
                    return;
                }

                raf = new RandomAccessFile(file, "rwd");
                raf.seek(file.length());

                //开始下载
                URL url = new URL(threadInfo.getUrl());
                httpURLConnection = (HttpURLConnection) url.openConnection();
                httpURLConnection.setConnectTimeout(3000);
                httpURLConnection.setRequestMethod("GET");
                httpURLConnection.setRequestProperty("RANGE", "bytes=" + file.length() + "-" );
                inputStream = httpURLConnection.getInputStream();
                byte[] bytes = new byte[1024 * 10];
                int len;
                long time = System.currentTimeMillis();

                while ((len = inputStream.read(bytes)) != -1) {
                    //写入文件
                    raf.write(bytes, 0, len);
                    //下载进度发送广播给activity
                    if (System.currentTimeMillis() - time > 300) {
                        time = System.currentTimeMillis();
                        intent.putExtra("finished", file.length() * 100 / fileInfo.getLength() + "");
                        intent.putExtra("fileId", fileInfo.getId());
                        context.sendBroadcast(intent);
                    }
                    //下载暂停保存下载进度
                    if (isPause) {
                        dao.updateThread(threadInfo.getUrl(), threadInfo.getId() , 2);
                        return;
                    }
                }
                //下载完成之后发送广播
                intent.putExtra("finished", 100 + "");
                intent.putExtra("fileId", fileInfo.getId());
                context.sendBroadcast(intent);
                //下载完成之后更新线程任务信息
                dao.updateThread(threadInfo.getUrl(), threadInfo.getId(), 3);
            } catch (Exception e) {
                e.printStackTrace();
                isPause = true;
                dao.updateThread(threadInfo.getUrl(), threadInfo.getId(), 2);
                Intent intent = new Intent(DownloadService.ACTION_ERRO);
                intent.putExtra("fileId", fileInfo.getId());
                context.sendBroadcast(intent);
            } finally {
                if (httpURLConnection != null) {
                    httpURLConnection.disconnect();
                }
                if (raf != null) {
                    try {
                        raf.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }

                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }


    }
}

在这个类的构造方法中需要传FileInfo,因为我们在往sql表中插入数据的时候需要从FileInfo中拿到一些属性进行插入 或者查询 下面将会在服务中进行传入

四 创建后台服务

在后台服务中 我们控制DownloadTask的开始和暂停 将每一个任务存在一个map中,同事在这个服务中 我们同时加入了一个请求下载文件的长度

public class DownloadService extends Service {

    public static final String ACTION_START = "ACTION_START";
    public static final String ACTION_STOP = "ACTION_STOP";
    public static final String ACTION_UPDATA = "ACTION_UPDATA";
    public static final String ACTION_ERRO = "ACTION_ERRO";
    public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/ADEMO";
    public static final int MSG_INIT = 0;
    private DownloadTask downloadTask;
    //下载任务的集合
    public static Map<Integer,DownloadTask> mTasks = new LinkedHashMap<>();
    
    Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_INIT:
                    FileInfo fileInfo = (FileInfo) msg.obj;
                    Log.d("DownloadService", "MSG_INIT:" + fileInfo);
                    //启动下载任务
                    downloadTask = new DownloadTask(DownloadService.this,fileInfo);
                    downloadTask.download();
                    mTasks.put(fileInfo.getId(),downloadTask);
                    break;
            }
        }
    };

    @Override
    public 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");
            //从集合中取出下载任务
            DownloadTask downloadTask = mTasks.get(fileInfo.getId());
            if (downloadTask != null){
                downloadTask.isPause = true;
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    class InitThread extends Thread {
        private FileInfo mFileInfo = null;

        public InitThread(FileInfo mFileInfo) {
            this.mFileInfo = mFileInfo;
        }

        @Override
        public void run() {
            HttpURLConnection httpURLConnection = null;
            RandomAccessFile raf = null;
            try {
                URL url = new URL(mFileInfo.getUrl());
                httpURLConnection = (HttpURLConnection) url.openConnection();
                httpURLConnection.setConnectTimeout(3000);
                httpURLConnection.setRequestMethod("GET");
                int length = -1;
                if (httpURLConnection.getResponseCode() == 200) {
                    length = httpURLConnection.getContentLength();
                }
                if (length <= 0) {
                    return;
                }
                File dir = new File(DOWNLOAD_PATH);
                if (!dir.exists()) {
                    dir.mkdir();
                }
                mFileInfo.setLength(length);
                Message message = handler.obtainMessage(MSG_INIT, mFileInfo);
                message.sendToTarget();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (httpURLConnection != null) {
                    httpURLConnection.disconnect();
                }
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }
}

主要的核心代码就展示完了 具体的一些细节 我会在文末贴上源码的下载地址

五 一些补充

因为我们是在后台服务中进行下载 如何处理用户手动强行杀死这个进程,当进程杀死的时候也许还有线程正在进行 线程并未执行完毕,因此 我们可以通过我们sql中的一个属性state进行判断 首先判断服务中的Map是否为空,如果为空的话再去sql中查询哪些是state为正在下载的任务,那么我们将这些任务继续下载

     //强行杀死进程重启之后筛选未完成的任务继续下载
        if (DownloadService.mTasks.size() == 0){
            List<ThreadInfo> allContinueThreads = dao.getAllContinueThreads();
            for (ThreadInfo allContinueThread : allContinueThreads) {
                Intent intent = new Intent(this, DownloadService.class);
                FileInfo fileInfo = new FileInfo();
                fileInfo.setFileName(allContinueThread.getTitle());
                fileInfo.setId(allContinueThread.getId());
                fileInfo.setUrl(allContinueThread.getUrl());
                intent.putExtra("fileInfo", fileInfo);
                intent.setAction(DownloadService.ACTION_START);
                startService(intent);
            }
        }

上面的具体效果为下面的效果图
在这里插入图片描述

源码地址:
https://github.com/louyulin/DownTask

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值