断点续传的原理 看上一篇
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;
}