Android-单线程断点续传

断点续传的原理 看上一篇
Android-断点续传

下面的例子是下载的断点续传,断点续传很简单就两点:
1.网络请求的时候,请求指定位置的数据,这个用到了网络请求的Range

conn.setRequestProperty("Range", "bytes=" + 500 + "-" + 1000);

网络请求可以使用httpURLconnection或者OkHttpClient
2.获取到数据之后,将新的数据拼接到目标文件之后,这个需要使用到
RandomAccessFile 。
3.本地数据库保存上一次请求的位置,下载信息,以便下一次继续当前之后请求数据,不需要从头开始请求数据。这里可以使用SQLite GreenDao 等。

手写一个单线程断点续传

ThreadInfo,下载任务信息 保存到数据库
public class ThreadInfo {

    private int id;         // ID
    private String url;     // 下载地址
    private long start;     // 开始长度
    private long end;       // 目标文件的总长度
    private long finished;  // 完成的长度

    public ThreadInfo() {

    }

    public ThreadInfo(int id, String url, long start, long end, long finished) {
        this.id = id;
        this.url = url;
        this.start = start;
        this.end = end;
        this.finished = finished;
    }

    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 getStart() {
        return start;
    }

    public void setStart(long start) {
        this.start = start;
    }

    public long getEnd() {
        return end;
    }

    public void setEnd(long end) {
        this.end = end;
    }

    public long getFinished() {
        return finished;
    }

    public void setFinished(long finished) {
        this.finished = finished;
    }

    @Override
    public String toString() {
        StringBuffer sb = new StringBuffer("ThreadInfo{");
        sb.append("id=").append(id);
        sb.append(", url='").append(url).append('\'');
        sb.append(", start='").append(start).append('\'');
        sb.append(", end='").append(end).append('\'');
        sb.append(", finish=").append(finished);
        sb.append('}');
        return sb.toString();
    }
}

FileInfo ,封装的下载任务信息
public class FileInfo implements Serializable {

    private int id;             // ID
    private String url;         // 下载地址
    private String fileName;    // 文件名
    private long length;        // 文件大小
    private long finish;        // 完成的大小

    public FileInfo() {

    }

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

    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 long getLength() {
        return length;
    }

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

    public long getFinish() {
        return finish;
    }

    public void setFinish(long finish) {
        this.finish = finish;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("FileInfo{");
        sb.append("id=").append(id);
        sb.append(", url='").append(url).append('\'');
        sb.append(", fileName='").append(fileName).append('\'');
        sb.append(", length=").append(length);
        sb.append(", finish=").append(finish);
        sb.append('}');
        return sb.toString();
    }
}

DownLoadService,开启一个服务下载任务
public class DownLoadService extends Service {


    public static final String FILE_INFO = "fileinfo";


    /**
     * action
     */
    private static final int MSG_INIT = 0;                          //初始化
    public static final String ACTION_START = "ACTION_START";       //开始下载
    public static final String ACTION_PAUSE = "ACTION_PAUSE";       //暂停下载
    public static final String ACTION_FINISHED = "ACTION_FINISHED"; //结束下载
    public static final String ACTION_UPDATE = "ACTION_UPDATE";     //更新UI

    /**
     * 下载路径 这里保存在SD卡里面
     */
    public static final String DOWNLOAD_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/downloads/";

    /**
     * 执行下载的那个任务 里面有线程 网络请求 数据保存
     */
    private DownTask mDownloadTask;

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

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // 获得Activity传来的参数
        if (ACTION_START.equals(intent.getAction())) {
            FileInfo fileInfo = (FileInfo) intent.getSerializableExtra(FILE_INFO);
            new InitThread(fileInfo).start();
        } else if (ACTION_PAUSE.equals(intent.getAction())) {
            FileInfo fileInfo = (FileInfo) intent.getSerializableExtra(FILE_INFO);
            if (mDownloadTask != null) {
                mDownloadTask.isPause = true;
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // 资源回收关闭
    }

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG_INIT) {
                FileInfo fileinfo = (FileInfo) msg.obj;
                // TODO 启动下载任务
                mDownloadTask = new DownTask(DownLoadService.this, fileinfo);
                mDownloadTask.startDownTask();
            }
        }
    };


    /**
     * 初始化子线程 这一步的作用是 获取下载目标的信息
     */
    private class InitThread extends Thread {

        private FileInfo tFileInfo;

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

        @Override
        public void run() {
            HttpURLConnection conn = null;
            RandomAccessFile raf = null;
            try {
                //连接网络文件
                URL url = new URL(tFileInfo.getUrl());
                conn = (HttpURLConnection) url.openConnection();
                conn.setConnectTimeout(3000);
                conn.setRequestMethod("GET");
                int length = -1;

                //获取目标文件长度
                if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
                    length = conn.getContentLength();
                }

                if (length < 0) {
                    return;
                }

                File dir = new File(DOWNLOAD_PATH);
                if (!dir.exists()) {
                    dir.mkdir();
                }
                //在本地创建文件
                File file = new File(dir, tFileInfo.getFileName());
                raf = new RandomAccessFile(file, "rwd");

                //设置本地文件长度
                raf.setLength(length);
                tFileInfo.setLength(length);

                // 发消息
                mHandler.obtainMessage(MSG_INIT, tFileInfo).sendToTarget();

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    if (conn != null && raf != null) {
                        raf.close();
                        conn.disconnect();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

DownTask,真正的下载任务在这里 ,当前类主要有两个任务 保存下载的数据,保存下载的进度
public class DownTask {

    private Context mContext = null;
    private FileInfo mFileInfo = null;
    private ThreadDAOImpl mThreadDAOImpe = null;
    private long mFinished = 0;
    public boolean isPause = false;
    private static final String TAG = "DownTask";


    /**
     * 构造函数
     *
     * @param mContext  上下文
     * @param mFileInfo 下载详情
     */
    public DownTask(Context mContext, FileInfo mFileInfo) {
        this.mContext = mContext;
        this.mFileInfo = mFileInfo;
        mThreadDAOImpe = new ThreadDAOImpl(mContext);
    }


    /**
     * 开始任务 开启一个子线程去执行任务
     */
    public void startDownTask() {
        // 本地数据库获取到所有的下载信息
        List threadInfos = mThreadDAOImpe.getThread(mFileInfo.getUrl());
        ThreadInfo info;
        if (threadInfos.size() == 0) {
            info = new ThreadInfo(0, mFileInfo.getUrl(), 0, mFileInfo.getLength(), 0);
        } else {
            info = threadInfos.get(0);
        }

        Thread a = new DownloadThread(info);
        a.start();
    }


    /**
     * 这个是下载的线程
     */
    private class DownloadThread extends Thread {

        private ThreadInfo threadInfo = null;

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

        @Override
        public void run() {
            //如果数据库中,不存在记录就要插入数据
            if (!mThreadDAOImpe.isExists(threadInfo.getUrl(), threadInfo.getId())) {
                mThreadDAOImpe.insertThread(threadInfo);
            }

            HttpURLConnection connection = null;
            RandomAccessFile raf = null;
            InputStream is = null;


            try {
                URL url = new URL(threadInfo.getUrl());
                connection = (HttpURLConnection) url.openConnection();
                connection.setConnectTimeout(3000);
                connection.setRequestMethod("GET");

                //设置下载位置
                long start = threadInfo.getStart() + threadInfo.getFinished();
                Log.e(TAG, "run: 继续下载进度为" + start);

                // TODO 重点 就在这里了 指定位置开始下载
                connection.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());

                //设置文件写入位置
                File file = new File(DownLoadService.DOWNLOAD_PATH, mFileInfo.getFileName());
                raf = new RandomAccessFile(file, "rwd");
                raf.seek(start);

                //设置广播
                Intent intent = new Intent(DownLoadService.ACTION_UPDATE);
                //从上次停止的地方继续下载
                mFinished += threadInfo.getFinished();
                Log.e(TAG, "上次下载的进度:" + mFinished);

                if (connection.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) {
                    is = connection.getInputStream();
                    byte[] buffer = new byte[4096];
                    int len = -1;
                    long time = System.currentTimeMillis();

                    // TODO 保存下载的数据
                    while ((len = is.read(buffer)) != -1) {

                        Log.i(TAG, "run: 一次数据读写 ");

                        //下载暂停时,保存进度
                        if (isPause) {
                            Log.e(TAG, "run: 进度为:" + mFinished);
                            mThreadDAOImpe.updateThread(mFileInfo.getUrl(), threadInfo.getId(), mFinished);
                            // TODO 暂停之后 直接返回 线程结束
                            return;
                        }

                        raf.write(buffer, 0, len);
                        mFinished += len;

                        if (System.currentTimeMillis() - time > 500) {
                            time = System.currentTimeMillis();
                            intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());
                            Log.e(TAG, "run: 这里发送广播了" + mFinished + " -- " + mFileInfo.getLength());
                            mContext.sendBroadcast(intent);
                        }
                    }

                    intent.putExtra("finished", (long) 100);
                    mContext.sendBroadcast(intent);
                    mThreadDAOImpe.deleteThread(mFileInfo.getUrl(), mFileInfo.getId());

                }
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "run: ");
            } finally {
                try {
                    if (is != null)
                        is.close();
                    if (raf != null)
                        raf.close();
                    if (connection != null)
                        connection.disconnect();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

ThreadDAO数据库访问接口
public interface ThreadDAO {

    /**
     * 插入线程信息
     *
     * @param threadInfo 线程信息
     */
    void insertThread(ThreadInfo threadInfo);

    /**
     * 删除线程信息
     *
     * @param url       地址
     * @param thread_id id
     */
    void deleteThread(String url, int thread_id);

    /**
     * /**
     * 更新线程信息
     *
     * @param url       地址
     * @param thread_id id
     * @param finished  完成进度
     */
    void updateThread(String url, int thread_id, long finished);

    /**
     * 查询文件的线程信息
     *
     * @param url 地址
     * @return 信息
     */
    List getThread(String url);

    /**
     * 判断是否存在
     *
     * @param url       地址
     * @param thread_id id
     * @return 是否存在
     */
    boolean isExists(String url, int thread_id);

}

ThreadDAOImpl
public class ThreadDAOImpl implements ThreadDAO{


    private static final String TAG = "ThreadDAOImpl";

    private MyDBHelper myDBHelper;

    public ThreadDAOImpl(Context context) {
        this.myDBHelper =  MyDBHelper.getInstance(context);
    }

    /**
     * 数据库插入数据
     * @param threadInfo 线程信息
     */
    @Override
    public void insertThread(ThreadInfo threadInfo) {
        Log.e("insertThread: ", "insertThread");
        SQLiteDatabase db = myDBHelper.getWritableDatabase();
        db.execSQL("insert into thread_info(thread_id,url,start,end,finished) values(?,?,?,?,?)",
                new Object[]{threadInfo.getId(), threadInfo.getUrl(),
                        threadInfo.getStart(), threadInfo.getEnd(), threadInfo.getFinished()});
        db.close();
    }

    /**
     * 删除下载好的文件下载信息
     * @param url       地址
     * @param thread_id id
     */
    @Override
    public void deleteThread(String url, int thread_id) {
        Log.e("deleteThread: ", "deleteThread");
        SQLiteDatabase db = myDBHelper.getWritableDatabase();
        db.execSQL("delete from  thread_info where url = ? and thread_id= ?",
                new Object[]{url, thread_id});
        db.close();
    }

    /**
     * 更新下载进度到数据库中
     * @param url       地址
     * @param thread_id id
     * @param finished  完成进度
     */
    @Override
    public void updateThread(String url, int thread_id, long finished) {
        Log.e("updateThread: ", "updateThread 更新的进度为+"+finished+" 跟新的id为"+thread_id+"-- url为:"+url);
        SQLiteDatabase db = myDBHelper.getWritableDatabase();
        String sql = "update thread_info set finished = "+finished+" where url = '"+url+"' and thread_id = "+thread_id+";";
        db.execSQL("update thread_info set finished = ?  where url = ? and thread_id = ?",
                new Object[]{finished, url, thread_id});

        Log.e(TAG, "updateThread: ----[[:"+sql );
//        db.execSQL(sql);
        db.close();
    }

    /**
     * 查询数据库中下载某个url的线程列表
     * @param url 地址
     * @return
     */
    @Override
    public List getThread(String url) {
        Log.e("getThread: ", "getThread");
        List list = new ArrayList<>();
        SQLiteDatabase db = myDBHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info where url=?", new String[]{url});
        while (cursor.moveToNext()) {
            ThreadInfo thread = new ThreadInfo();
            thread.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
            thread.setUrl(cursor.getString(cursor.getColumnIndex("url")));
            thread.setStart(cursor.getLong(cursor.getColumnIndex("start")));
            thread.setEnd(cursor.getLong(cursor.getColumnIndex("end")));
            thread.setFinished(cursor.getLong(cursor.getColumnIndex("finished")));
            list.add(thread);
        }
        cursor.close();
        db.close();
        return list;
    }

    /**
     * 判断下载指定url的线程是否存在
     * @param url       地址
     * @param thread_id id
     * @return
     */
    @Override
    public boolean isExists(String url, int thread_id) {
        SQLiteDatabase db = myDBHelper.getWritableDatabase();
        Cursor cursor = db.rawQuery("select * from thread_info where url=? and thread_id = ?",
                new String[]{url, String.valueOf(thread_id)});
        boolean isExist = cursor.moveToNext();
        cursor.close();
        db.close();
        Log.e(TAG, "isExists: " + isExist);
        return isExist;
    }

}

MyDBHelper 数据库
public class MyDBHelper extends SQLiteOpenHelper {


    /**
     * 数据库的名字
     */
    private static final String DB_NAME = "download.db";

    /**
     * 创建表
     */
    private static final String SQL_CREATE = "create table thread_info(_id integer primary key autoincrement," +
            "thread_id integer,url text,start long,end long,finished long)";

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

    /**
     * 当前数据库的版本
     */
    private static final int VERSION = 1;


    private static MyDBHelper myDBHelper;

    private MyDBHelper(Context context) {
        super(context, DB_NAME, null, VERSION);
    }

    public static MyDBHelper getInstance(Context context) {
        if (myDBHelper == null) {
            myDBHelper = new MyDBHelper(context);
        }
        return myDBHelper;
    }

    @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);
    }
}

okhttp 网络请求

implementation 'com.squareup.okhttp3:okhttp:4.4.0'
代码实现
 private void downLoadFile() {
        try {
            if (file == null) {
                file = new File(rootFile, name);
                raf = new RandomAccessFile(file, "rwd");
            } else {
                downLoadSize = file.length();
                if (raf == null) {
                    raf = new RandomAccessFile(file, "rwd");
                }
                raf.seek(downLoadSize);
            }
            totalSize = getContentLength(path);
            if (downLoadSize == totalSize) {
                //已经下载完成
                return;
            }

            OkHttpClient client = new OkHttpClient();
            Request request = new Request.Builder().url(path).
                    addHeader("Range", "bytes=" + downLoadSize + "-" + totalSize).build();
            Response response = client.newCall(request).execute();
            InputStream ins = response.body().byteStream();
            int len = 0;
            byte[] by = new byte[1024];
            long endTime = System.currentTimeMillis();
            while ((len = ins.read(by)) != -1 && isDown) {
                raf.write(by, 0, len);
                downLoadSize += len;
                if (System.currentTimeMillis() - endTime > 1000) {
                    final double dd = downLoadSize / (totalSize * 1.0);
                    DecimalFormat format = new DecimalFormat("#0.00");
                    String value = format.format((dd * 100)) + "%";
                    Log.i("tag", "==================" + value);
                    handler.post(new Runnable() {
                        @Override
                        public void run() {
                            progress.onProgress((int) (dd * 100));
                        }
                    });
                }
            }
            response.close();
        } catch (Exception e) {
            e.getMessage();
        }

    }

    /**
     * 通过OkhttpClient获取文件的大小
     *
     * @param url
     * @return
     * @throws IOException
     */
    public long getContentLength(String url) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(url).build();
        Response response = client.newCall(request).execute();
        long length = response.body().contentLength();
        response.close();
        return length;
    }

你可能感兴趣的:(Android-单线程断点续传)