在慕课网学习了断点续传这堂课以后深深的感觉到了自己的基础不够扎实,尤其在网络和数据库方面
首先断点续传的重点在于断点和续传,断点就是要当点击暂停的时候能够保存下载进度,续传就是能够从上次保存的进度中继续下载。而这中间牵扯到两个方面的断点续传:
其一:是在文件中断点续传,首先我们要读取文件的长度,并且在本地创建一个大小相同的文件用来写入之后的内容。
其二:是在网络中断点续传,在网络中断点续传需要注意的是,连接的返回码不是200,而是206(部分内容),而且还要设置要读取的字节数范围。
在这中间,下载的进度通过数据库的方式来存储和读写。
以下是思维流图,用来说明整个流程:
以上为思维导图,具体实现步骤为:
1首先按钮点击事件用intent来实现,在下载服务中定义了两个action,用来标识是下载还是暂停
public void doclick(View v){
switch (v.getId()){
case R.id.START:{
Intent intent=new Intent(MainActivity.this,DownSerivce.class);
intent.setAction(DownSerivce.SATRT_ACTION);
intent.putExtra("fileinfo",fileinfo);
intent.setPackage("com.example.downtest");
startService(intent);
break;
}
case R.id.STOP:{
Intent intent=new Intent(MainActivity.this,DownSerivce.class);
intent.setAction(DownSerivce.STOP_ACTION);
intent.putExtra("fileinfo",fileinfo);
intent.setPackage("com.example.downtest");
startService(intent);
break;
}
}
}
这里启动了下载服务,下载服务中定义了两个action
public class DownSerivce extends Service {
//获得SD卡根目录,并且创建下载文件夹
public static final String PATH=Environment.getExternalStorageDirectory().getAbsolutePath()+"/down/";
public static final String SATRT_ACTION="SATRT_ACTION";//用来标识具体动作是下载还是暂停
public static final String STOP_ACTION="STOP_ACTION";
private DownFile downfile=null;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if(SATRT_ACTION.equals(intent.getAction())){
Fileinfo fileinfo= (Fileinfo) intent.getSerializableExtra("fileinfo");
new Fileinit(fileinfo).start();//启动初始化进程
}else if (STOP_ACTION.equals(intent.getAction())){
Fileinfo fileinfo= (Fileinfo) intent.getSerializableExtra("fileinfo");
if(downfile!=null){
downfile.isPause=true;//设置下载的进程标识为暂停,当下载进程监测到后暂停下载并return
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
Fileinfo fileinfo= (Fileinfo) msg.obj;
downfile=new DownFile(DownSerivce.this,fileinfo);
downfile.init();//开始下载前的准备
}
};
class Fileinit extends Thread{
private Fileinfo fileinfo;
public Fileinit(Fileinfo fileinfo) {
this.fileinfo = fileinfo;
}
@Override
public void run() {
HttpURLConnection connection;
int lenth=0;
try {
URL url=new URL(fileinfo.getUrl());
connection= (HttpURLConnection) url.openConnection();
connection.setReadTimeout(3000);
connection.setRequestMethod("GET");//下载的时候一定要为get方式而不能为post
if(connection.getResponseCode()== HttpURLConnection.HTTP_OK){
lenth=connection.getContentLength();//获得文件大小
}else{
return;
}
fileinfo.setLenth(lenth);//这里在文件信息中设置文件大小
File file=new File(PATH);
if(!file.exists()){
file.mkdir();
}
File file1=new File(PATH+fileinfo.getFilename());
RandomAccessFile raf=new RandomAccessFile(file1,"rwd");
//这里在文件中设置文件大小,
// 这样就在本地文件中创建了一个同样大小的文件,用来随后写入
raf.setLength(lenth);
Message message = handler.obtainMessage();
//在这里我们通过sendmessage方式回到了主线程,并且把文件信息传给了主线程
message.obj=fileinfo;
handler.sendMessage(message);
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这样在下载服务中,首先获得文件长度,并且在本地初始化文件信息,设置文件大小。然后把文件信息传入给主线程,在主线程中启动下载进程开始下载。这里,当标识为暂停时,我们把下载进程中的下载标识设置为true,这样,下载标识在检测到isPause为true的时候就会停止下载,并且return;
在下载进程中,我们首先根据得到的url判断数据库中是否有相应的进程信息。
public void init(){
data=new SqlData(context);
Threadinfo threadinfo=null;
List<Threadinfo> threadinfos = data.gettherad(fileinfo.getUrl());
if(threadinfos.size()==0){
threadinfo=new Threadinfo(fileinfo.getUrl(),fileinfo.getId(),0,fileinfo.getLenth(),0);
}else {
threadinfo=threadinfos.get(0);
}
new DownTask(threadinfo).start();
}
进程信息实体类中保存着文件下载的url和start位置,end位置,finish位置,这样我们在数据库读取信息,如果有的话说明之前按了暂停按钮,这时候我们直接从数据库中读取线程信息继续开始下载,如果没有就创建新的进程信息开始从头下载。
这里为下载进程类:
public class DownFile{
private Fileinfo fileinfo;
public boolean isPause=false;//暂停标识,决定是否暂停下载
private int start=0;
private Context context;
private SqlData data;
public DownFile(Context context,Fileinfo fileinfo) {
this.context=context;
this.fileinfo = fileinfo;
}
public void init(){
data=new SqlData(context);
Threadinfo threadinfo=null;
//从数据库中读取进程信息,如果size为0的话,则代表数据库中没有进程信息,需要重新创建,如果有则表示之前暂停了,那么开始续传
List<Threadinfo> threadinfos = data.gettherad(fileinfo.getUrl());
if(threadinfos.size()==0){
threadinfo=new Threadinfo(fileinfo.getUrl(),fileinfo.getId(),0,fileinfo.getLenth(),0);
}else {
threadinfo=threadinfos.get(0);
}
new DownTask(threadinfo).start();
}
class DownTask extends Thread{
private Threadinfo threadinfo;
private int mfinished;
public DownTask(Threadinfo threadinfo) {
this.threadinfo = threadinfo;
}
@Override
public void run() {
//这里判断如果当前进程信息在数据库中没有的话,那么就把当前信息写入到数据库中,以便保存,同时也是为了区分首次下载和续传
if(!data.exits(threadinfo.getUrl())){
data.insert(threadinfo);
}
InputStream is=null;
HttpURLConnection conn=null;
RandomAccessFile raf=null;
try {
URL url=new URL(threadinfo.getUrl());
conn= (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setReadTimeout(3000);
//这个start为已经0+已经写入的进度。如果第一次的话,都为0.如果暂停过,那么finish不为0
start=threadinfo.getStart()+threadinfo.getFinish();
//设置文件下载位置
conn.setRequestProperty("Range","bytes="+start+"-"+threadinfo.getEnd());
//设置文件写入位置
File file=new File(DownSerivce.PATH,fileinfo.getFilename());
raf=new RandomAccessFile(file,"rwd");
//这里是跳转到文件上次写入的位置
raf.seek(start);
//这里还需要得到之前文件已经下载好的进度
mfinished+=threadinfo.getFinish();
Intent intent=new Intent();
//这里设置intent的action,在进度更新的时候发送广播以更新ui
intent.setAction("com.updata");
//这里注意,我们需要的是读取部分内容,所以返回码是206不是200,所以应该用partial
if(conn.getResponseCode()==HttpURLConnection.HTTP_PARTIAL){
is=conn.getInputStream();
byte [] buffer=new byte[1024*4];
int len=-1;
while((len=is.read(buffer))!=-1) {
raf.write(buffer, 0, len);
mfinished += len;//这里需要不断地更新finish
//这里有个地方需要注意,当都是int的时候 先乘以100在除以总数就会得到具体的数字
//如果先除在乘以100的话,精度会丢失
intent.putExtra("value", mfinished*100/fileinfo.getLenth());
//更新了finish,所以发送广播以更新UI
context.sendBroadcast(intent);
//这里每次都更新线程信息中的finish
threadinfo.setFinish(mfinished);
if (isPause) {
//当下载标识为暂停的时候,我们首先要做的就是保存当前进度到数据库中
//然后结束
data.updata(threadinfo);
return;
}
}
}
//如果下载完成了,这里要删除数据库中的进程信息
data.delete(threadinfo);
Log.i("kk","下载完毕");
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注释写的比较全了,大概流程就是如果有进程信息就直接下载,如果数据库中没有就创建,并且进入下载线程后,首先要保存当前保存当前进程信息到数据库中,否则就无法断点续传。
其次我们得到线程信息中的finish,决定开始下载的地方,然后开始下载并且每当finish更新时,发送广播以更新ui
其中不熟练的是数据库的操作
创建数据库的步骤就是:
首先继承SQLiteOpenHelper类,并且在类中创建表或者更新表,然后通过实例化这个类调用helper.getWritableDatabase();方法得到数据库,返回的是一个SQLiteDatabase对象,可以直接执行sql语句。
代码如下:
public void insert(Threadinfo threadinfo){
SQLiteDatabase db=helper.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.getFinish()});
db.close();
}
public void updata(Threadinfo threadinfo){
SQLiteDatabase db=helper.getWritableDatabase();
db.execSQL("update thread_info set finished=? where url=? and thread_id=? ",new Object[]{
threadinfo.getFinish(),threadinfo.getUrl(),threadinfo.getId()
});
db.close();
}
public void delete(Threadinfo threadinfo){
SQLiteDatabase db=helper.getWritableDatabase();
db.execSQL("delete from thread_info where thread_id=? and url=?",new Object[]{
threadinfo.getId(),threadinfo.getUrl()
});
db.close();
}
这里数据库就不再多做介绍了,总的来说。断点续传这个理清了思路还是比较简单的,重点在与断点的保存与读取。