您的位置:首页 > 移动开发 > Android开发

Android-Service系列之断点续传下载

2016-02-14 12:12 423 查看
课程地址:http://www.imooc.com/learn/363

源码:http://download.csdn.net/download/qq_22804827/9424950

本次,将会利用Service进行一个单线程的断点续传下载的实例练习。

在开始之前呢,先进行一下简单的案例分析:



会涉及到一下几点内容:

基本UI定义

数据库的操作:在下载的同时将下载进度保存到数据库里面

Service的启动

Activity给Service传递参数

Service中使用广播回传数据到Activity中

线程和Handler

网络操作

还有,在网络下载的时候有几个关键点需要注意一下:

1. 获得网络文件的长度(即大小)

2. 在本地创建一个文件,设置其长度(相当于一个容器,存储下载的文件)

3. 从数据库中获得上次下载的进度

4. 从上次下载的位置下载数据,同时保存进度到数据库中

5. 将下载进度回传给Activity

6. 下载完成后删除下载信息

接下来,就正式开始吧:

(因为接下来还会有多线程的断点续传,所以在这次代码中的一些类,是基于多线程的断点续传而考虑的)

1.首先定义两个实体类

FileInfo.java

/**
* 文件实体类
* 将该类序列化后(即实现Serializable接口)
* 就可以在intent中进行传递
*
*/
public class FileInfo implements Serializable{
private int id;
private String url;
private String fileName;
/**
* 文件的大小
*/
private int length;
/**
* 文件的下载进度
*/
private int progress;

public FileInfo() {
}

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

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 int getProgress() {
return progress;
}

public void setProgress(int progress) {
this.progress = progress;
}

@Override
public String toString() {
return "FileInfo [id=" + id + ", url=" + url + ", fileName=" + fileName
+ ", length=" + length + ", progress=" + progress + "]";
}
}


2.ThreadInfo.java

/**
* 线程信息实体类
*
*/
public class ThreadInfo {
private int id;

/**
* 跟下载的文件的url一致
*/
private String url;

/**
* 上次保存的文件的下载进度
*/
private int start;

/**
* 代表的线程中下载文件的总长度
*/
private int end;

/**
* 下载的进度(即文件下载到了哪儿,以字节数为单位)
*/
private int finished;

public ThreadInfo() {
}

public ThreadInfo(int id, String url, int start, int end, int finished) {
super();
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 int getStart() {
return start;
}

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

public int getEnd() {
return end;
}

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

public int getFinished() {
return finished;
}

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

@Override
public String toString() {
return "ThreadInfo [id=" + id + ", url=" + url + ", start=" + start
+ ", end=" + end + ", finished=" + finished + "]";
}
}


然后是有关数据库操作的几个类

1.DBHelper.java

/**
* 数据库帮助类
* 用来创建数据库
*
*/
public class DBHelper extends SQLiteOpenHelper {
private static final String DB_NAME="download.db";
private static final int VERSION=1;//数据库的版本
/**
* 建表语法
*/
private static final String TABLE_CREATE="create table thread_info(_id integer primary key autoincrement," +
"thread_id integer,url text,start integer,end integer,finished integer)";
/**
* 删表语法
*/
private static final String TABLE_DROP="drop table if exits thread_info";

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

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

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


2.ThreadDAO.java

/**
* 数据访问接口i
*
*/
public interface ThreadDAO {
/**
* 插入线程信息
* @param threadInfo
*/
public void insertThreadInfo(ThreadInfo threadInfo);
/**
* 删除线程信息
* @param url 文件的url
* @param id 线程的id
*/
public void deleteThreadInfo(String url,int id);
/**
* 更新线程下载进度
*/
public void updateThreadInfo(String url,int threadId,int finished);
/**
* 查询下载文件的线程信息
*/
public List<ThreadInfo> getThreads(String url);
/**
* 判断指定线程信息是否已经存在数据库中
*/
public boolean isExists(String url,int threadId);
}


3.ThreadDAOImpl.java

/**
* 线程数据访问接口实现
*
*/
public class ThreadDAOImpl implements ThreadDAO {
private DBHelper mHelper;

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

@Override
public void insertThreadInfo(ThreadInfo threadInfo) {
SQLiteDatabase db=mHelper.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();
}

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

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

@Override
public List<ThreadInfo> getThreads(String url) {
List<ThreadInfo> list=null;
SQLiteDatabase db=mHelper.getWritableDatabase();
Cursor cursor=db.rawQuery("select * from thread_info where url = ?",new String[]{url});
if (cursor != null) {
list=new ArrayList<ThreadInfo>();
while (cursor.moveToNext()) {
ThreadInfo temp = new ThreadInfo();
temp.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
temp.setUrl(cursor.getString(cursor.getColumnIndex("url")));
temp.setStart(cursor.getInt(cursor.getColumnIndex("start")));
temp.setEnd(cursor.getInt(cursor.getColumnIndex("end")));
temp.setFinished(cursor.getInt(cursor
.getColumnIndex("finished")));
list.add(temp);
}
cursor.close();
}
db.close();
return list;
}

@Override
public boolean isExists(String url, int threadId) {
SQLiteDatabase db=mHelper.getWritableDatabase();
Cursor cursor=db.rawQuery("select * from thread_info where url = ? and thread_id = ?",new String[]{url,""+threadId});
boolean exists=false;
if(cursor!=null) {
exists=cursor.moveToNext();
}
db.close();
return exists;
}
}


接着是要用到的Service类以及用于下载的类

1.DownloadService.java

//每个服务都只会存在一个实例
public class DownloadService extends Service {
public static final String ACTION_DOWNLOAD="DOWNLOAD";
public static final String ACTION_STOP="STOP";
public static final String ACTION_UPDATE="UPDATE";

/**
* 存放下载文件的文件夹路径
*/
public static final String DOWNLOAD_PATH = Environment
.getExternalStorageDirectory().getAbsolutePath()
+ "/DownloadsTest/";

private static final int MSG_INIT=0;//代表创建本地文件完成

private DownloadTask mTask;

/**
* 在每次服务启动的时候调用
* 如果我们希望服务一旦启动就立刻去执行某个动作,就可以将逻辑写在onStartCommand()方法里
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
//获得Activity传来的参数
if(ACTION_DOWNLOAD.equals(intent.getAction())) {
if(mTask==null||mTask.isPause||mTask.isFinished) {
//这里的判断语句要将mTask==null放在最前面,因为mTask还没有new出来
FileInfo fileInfo = (FileInfo) intent
.getSerializableExtra("fileInfo");
Log.d("测试", "ACTION_DOWNLOAD:" + fileInfo.toString());

// 启动初始化线程
new InitThread(fileInfo).start();
}
}
else if(ACTION_STOP.equals(intent.getAction())){
if (mTask!=null&&!mTask.isPause&&!mTask.isFinished) {
//这里的判断语句要将mTask!=null放在最前面,因为mTask还没有new出来
FileInfo fileInfo = (FileInfo) intent
.getSerializableExtra("fileInfo");
Log.d("测试", "ACTION_STOP:" + fileInfo.toString());
if (mTask != null) {
mTask.isPause = true;
}
}
}
return super.onStartCommand(intent, flags, startId);
}

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

private Handler mHandler=new Handler() {
public void handleMessage(android.os.Message msg) {
switch(msg.what) {
case MSG_INIT:
FileInfo fileInfo=(FileInfo) msg.obj;
Log.d("测试", "mHandler"+fileInfo.toString());
//启动下载任务
mTask=new DownloadTask(DownloadService.this, fileInfo);
mTask.download();
break;
}
}
};

/**
* 从网上读取文件的长度然后再本地建立文件
*/
private class InitThread extends Thread {
private FileInfo mFileInfo;

public InitThread(FileInfo fileInfo) {
mFileInfo=fileInfo;
}

public void run() {
Log.d("测试", "InitThread");

HttpURLConnection connection=null;
RandomAccessFile raf=null;
try {
//连接网络文件
URL url=new URL(mFileInfo.getUrl());
connection=(HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);//设置连接超时
connection.setReadTimeout(3000);//设置读取超时
connection.setRequestMethod("GET");

int length=-1;
if(connection.getResponseCode()==HttpStatus.SC_OK) {//判断是否成功连接
//获得文件长度
length=connection.getContentLength();
}
if(length<=0) {
return;
}

File dir=new File(DOWNLOAD_PATH);
if(!dir.exists()) {//判断存放下载文件的文件的文件夹是否存在
dir.mkdir();
}
//在本地创建文件
File file=new File(dir,mFileInfo.getFileName());
raf=new RandomAccessFile(file,"rwd");//随机存取文件,用于断点续传,r-读取/w-写入/d-删除权限

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

mFileInfo.setLength(length);
mHandler.obtainMessage(MSG_INIT, mFileInfo).sendToTarget();
} catch (Exception e) {
} finally {
try {
raf.close();
connection.disconnect();
} catch (Exception e) {
}
}
}
}
}


Activity属于一个前台的组件,有可能会被用户关闭,或者被切到后台(这时就有可能会被安卓系统回收),如果在Activity中创建一个线程去下载,一旦Activity被回收,就无法对在其中创建的线程进行管理,会导致不必要的麻烦
而Service的优先级比较高,一般不会被系统回收


2.DownloadTask.java

/**
* 下载任务类
*
*/
public class DownloadTask {
private Context mContext;
private FileInfo mFileInfo;

private ThreadDAO mDAO;

private int mFinished;//用于更新UI的下载进度

public boolean isPause;//判断是否正在下载
public boolean isFinished;//判断是否下载完成

public DownloadTask(Context context, FileInfo fileInfo) {
super();
this.mContext = context;
this.mFileInfo = fileInfo;
mDAO=new ThreadDAOImpl(context);
isPause=false;
isFinished=false;
}

public void download() {
Log.d("测试","download");

//读取上次的线程信息
List<ThreadInfo> list=mDAO.getThreads(mFileInfo.getUrl());
ThreadInfo threadInfo=null;
if(list.size()==0||list==null) {//有可能是第一次下载,数据库中还没有信息
threadInfo=new ThreadInfo(0,mFileInfo.getUrl(),0,mFileInfo.getLength(),0);
}
else {
//因为这里是单线程下载,所以这里直接get(0)就行了,下次会涉及多线程
threadInfo=list.get(0);
}
new DownloadThread(threadInfo).start();
}

/**
* 下载线程
*
*/
class DownloadThread extends Thread {
private ThreadInfo mThreadInfo;
public DownloadThread(ThreadInfo ThreadInfo) {
mThreadInfo=ThreadInfo;
}

public void run() {
Log.d("测试","DownloadThread");

//向数据库中插入线程信息
if(!mDAO.isExists(mThreadInfo.getUrl(), mThreadInfo.getId())) {
mDAO.insertThreadInfo(mThreadInfo);
}

HttpURLConnection connection=null;
RandomAccessFile raf=null;
InputStream input=null;
try {
URL url=new URL(mThreadInfo.getUrl());
connection=(HttpURLConnection) url.openConnection();
connection.setConnectTimeout(3000);
connection.setRequestMethod("GET");

//设置下载位置
int start=mThreadInfo.getFinished();//上一次保存的下载进度即为这次要开始下载的地方

connection.setRequestProperty("Range", "bytes="+start+"-"+mThreadInfo.getEnd());
//设置请求属性,将field参数设置为Range(范围),newValues为指定的字节数区间

//设置文件写入位置
File file=new File(DownloadService.DOWNLOAD_PATH,mFileInfo.getFileName());
raf=new RandomAccessFile(file, "rwd");
raf.seek(start);
//在读写的时候跳过设置的字节数,从下一个字节数开始读写
//如seek(100)则跳过100个字节从第101个字节开始读写

Intent intent=new Intent(DownloadService.ACTION_UPDATE);

mFinished=start;

//开始下载
if(connection.getResponseCode()==HttpStatus.SC_PARTIAL_CONTENT) {
//因为前面的RequestProperty设置的Range,服务器会认为进行部分的下载,所以这里判断是否成功连接要用SC_PARTIAL_CONTENT
//读取数据
input=connection.getInputStream();
byte[] buffer=new byte[1024*4];
int len=-1;//标记每次读取的长度

long time=System.currentTimeMillis();

while((len=input.read(buffer))!=-1) {
//写入文件
raf.write(buffer,0,len);

//把下载进度发送给Activity
mFinished += len;
if(System.currentTimeMillis()-time>500) {//因为该循环运行较快,所以这里减缓一下UI更新的频率
time=System.currentTimeMillis();
intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());
mContext.sendBroadcast(intent);
Log.d("测试", ""+mFinished * 100 / mFileInfo.getLength());

mDAO.updateThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId(), mFinished);
//如果隔断时间就更新一下数据库的内容
//可以防止没有按下暂停就关闭程序重进后要重新开始下载的问题
//而又不至于更新数据库太频繁,影响效率
}

//在下载暂停时,保存下载进度至数据库
if(isPause) {
mDAO.updateThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId(), mFinished);
intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());
mContext.sendBroadcast(intent);
raf.close();
input.close();
connection.disconnect();
return;
}
}
intent.putExtra("finished", mFinished * 100 / mFileInfo.getLength());
mContext.sendBroadcast(intent);
//因为有可能在刚好下载完成的时候没有进入到if(isPause)中,所以进度条会停在上次更新的时候,显示的时候还有一小段没有下载,但是实际已经下载完成了

//删除线程信息
mDAO.deleteThreadInfo(mThreadInfo.getUrl(), mThreadInfo.getId());
isFinished=true;
}
} catch (Exception e) {
} finally {
try {
raf.close();
input.close();
connection.disconnect();
} catch (Exception e) {
}
}
}
}
}


最后就是有关布局的代码以及相关的配置

1.activity_main.xml

<RelativeLayout 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: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.downloaddemo.MainActivity" >

<TextView
android:id="@+id/id_fileName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="文件名" />

<ProgressBar
android:id="@+id/id_progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignLeft="@+id/id_fileName"
android:layout_below="@+id/id_fileName"
android:layout_marginTop="15dp" />

<Button
android:id="@+id/id_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/id_download"
android:layout_alignBottom="@+id/id_download"
android:layout_alignRight="@+id/id_progressBar"
android:text="暂停" />

<Button
android:id="@+id/id_download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/id_progressBar"
android:layout_marginTop="22dp"
android:layout_toLeftOf="@+id/id_stop"
android:text="下载" />

</RelativeLayout>


2.MainActivity.java

public class MainActivity extends Activity {
private ProgressBar mProgressBar;
private Button mStop;
private Button mDownload;
private TextView mFileName;
private FileInfo fileInfo;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mFileName=(TextView) findViewById(R.id.id_fileName);
mProgressBar=(ProgressBar) findViewById(R.id.id_progressBar);
mProgressBar.setMax(100);
mStop=(Button) findViewById(R.id.id_stop);
mDownload=(Button) findViewById(R.id.id_download);

//注册广播接收器
IntentFilter filter=new IntentFilter();
filter.addAction(DownloadService.ACTION_UPDATE);
registerReceiver(mReceiver, filter);

//创建文件信息对象
String url="http://www.imooc.com/mobile/imooc.apk";
fileInfo=new FileInfo(0,url,"imooc.apk");

mDownload.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,
DownloadService.class);
intent.setAction(DownloadService.ACTION_DOWNLOAD);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
});

mStop.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,
DownloadService.class);
intent.setAction(DownloadService.ACTION_STOP);
intent.putExtra("fileInfo", fileInfo);
startService(intent);
}
});
}

@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(mReceiver);
}

/**
* 更新UI广播的广播接收器
*/
BroadcastReceiver mReceiver=new BroadcastReceiver() {

@Override
public void onReceive(Context context, Intent intent) {
if(intent.getAction().equals(DownloadService.ACTION_UPDATE)) {
Log.d("测试", "mReceiver");
mFileName.setText(fileInfo.getFileName());
int finished=intent.getIntExtra("finished", 0);
mProgressBar.setProgress(finished);
}
}
};
}


3.在AndroidMainfest.xml中添加权限

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>


注册服务

<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
<service      android:name="com.example.downloaddemo.services.DownloadService"></service>
...
</application>


新增知识点:RandomAccessFile类的使用

本来是,是想另外用一篇博客来写开始提到的“Android-Service系列之多线程断点续传”课程的(http://www.imooc.com/learn/376),但是,在看了视频课程之后,顿时有种哔了狗的感觉,因为觉得老师讲得让我感觉有点凌乱,虽然在里面学到了几个好的思想,但是也有一些疏漏,而且让我感觉逻辑性不强。

下面先来说说几点不错的思想:

1、将DatabaseHelper修改成单例模式,防止多个实例同时对数据库进行操作

2、同时也要保证同一时刻只有一个线程能够对数据库的内容进行修改(即对数据库的增删改操作进行同步)

3、尽量把对数据库的操作放在线程外面去做,减少对数据库的锁定

4、因为这这个实例中设计的线程比较多,使得系统对线程的创建、销毁所占用的时间,以及性能的消耗是占了相当的比例的,所以在这里用到了线程池来进行优化

好了,说了我觉得不错的地方,就要来找找茬了(都怪小编的强迫症):

1、在视频的2-3节说提到的在适配器的getView方法中将一部分语句放在if语句里面,达到减少其执行次数来进行优化的做法是错误的,因为当复用item的时候,显示的文件名等会错乱

2、因为在DownloadTask的download方法中每个线程需要下载的文件长度总是由文件的总长度/线程数量决定的(相当于固定了的),所以即使在更新UI的时候把下载的进度保存了起来了,但是如果正在下载的文件没有按下暂停而关闭了程序,那么在重新打开程序后就要重头下载文件

3、因为这次的教程只有实现了同时下载几个文件,所以下显示下载任务的item都在同一屏,因此不会涉及到item的复用,但是如果多下载几个文件,要用到第二屏,就会因为item的复用出现按钮响应以及进度条显示的错乱

多线程断点续传涉及的源码(有做修改):

http://download.csdn.net/download/qq_22804827/9431070
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: