零、前言
断点续传逻辑总览.png1.今天带来的是Android原生下载的上篇,主要核心是断点续传,多线程下载将会在下篇介绍
2.本例使用了Activity
,Service
,BroadcastReceiver
三个组件
3.本例使用了两个线程:LinkURLThread
做一些初始工作,DownLoadThread
进行核心下载工作
4.本例使用SQLite进行暂停时的进度保存,使用Handler进行消息的传递,使用Intent进行数据传递
5.对着代码,整理了一下思路,画了一幅下面的流程图,感觉思路清晰多了
6.本例比较基础,但串联了Android的很多知识点,作为总结还是很不错的。
一、前置准备工作
初始准备.png先实现上面一半的代码:
1.关于下载的链接:
查看下载地址.png既然是下载,当然要有链接了,就那掘金的apk来测试吧!查看方式:
2.文件信息封装类:FileBean
public class FileBean implements Serializable {
private int id;//文件id
private String url;//文件下载地址
private String fileName;//文件名
private long length;//文件长度
private long loadedLen;//文件已下载长度
//构造函数、get、set、toString省略...
}
2.关于常量:Cons.java
无论是Intent添加的Action,还是Intent传递数据的标示,或Handler发送消息的标示
一个项目中肯定会有很多这样的常量,如果散落各处感觉会很乱,我习惯使用一个Cons类统一处理
//intent传递数据----开始下载时,传递FileBean到Service 标示
public static final String SEND_FILE_BEAN = "send_file_bean";
//广播更新进度
public static final String SEND_LOADED_PROGRESS = "send_loaded_length";
//下载地址
public static final String URL = "https://imtt.dd.qq.com/16891/4611E43165D203CB6A52E65759FE7641.apk?fsname=com.daimajia.gold_5.6.2_196.apk&csr=1bbd";
//文件下载路径
public static final String DOWNLOAD_DIR =
Environment.getExternalStorageDirectory().getAbsolutePath() + "/b_download/";
//Handler的Message处理的常量
public static final int MSG_CREATE_FILE_OK = 0x00;
2.Activity与Service的协作
界面.png界面比较简单,就不贴了
1).Activity中:
/**
* 点击下载时逻辑
*/
private void start() {
//创建FileBean对象
FileBean fileBean = new FileBean(0, Cons.URL, "掘金.apk", 0, 0);
Intent intent = new Intent(MainActivity.this, DownLoadService.class);
intent.setAction(Cons.ACTION_START);
intent.putExtra(Cons.SEND_FILE_BEAN, fileBean);//使用intent携带对象
startService(intent);//开启服务--下载标示
mIdTvFileName.setText(fileBean.getFileName());
}
/**
* 点击停止下载逻辑
*/
private void stop() {
Intent intent = new Intent(MainActivity.this, DownLoadService.class);
intent.setAction(Cons.ACTION_STOP);
startService(intent);//启动服务---停止标示
}
2).DownLoadService:下载的服务
public class DownLoadService extends Service {
@Override//每次启动服务会走此方法
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent.getAction() != null) {
switch (intent.getAction()) {
case Cons.ACTION_START:
FileBean fileBean = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
L.d("action_start:" + fileBean + L.l());
break;
case Cons.ACTION_STOP:
L.d("action_stop:");
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
测试.png不要忘记注册Service:
<service android:name=".service.DownLoadService"/>
通过点击两个按钮,测试可以看出FileBean对象的传递和下载开始、停止的逻辑没有问题
二、下载的初始线程及使用:
1.LinkURLThread线程的实现
1).连接网络文件
2).获取文件长度
3).创建等大的本地文件:RandomAccessFile
4).从mHandler的消息池中拿个消息,附带mFileBean和MSG_CREATE_FILE_OK标示发送给mHandler
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:13:42<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:连接url做一些准备工作:获取文件大小。创建文件夹及等大的文件
*/
public class LinkURLThread extends Thread {
private FileBean mFileBean;
private Handler mHandler;
public LinkURLThread(FileBean fileBean, Handler handler) {
mFileBean = fileBean;
mHandler = handler;
}
@Override
public void run() {
HttpURLConnection conn = null;
RandomAccessFile raf = null;
try {
//1.连接网络文件
URL url = new URL(mFileBean.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
if (conn.getResponseCode() == 200) {
//2.获取文件长度
long len = conn.getContentLength();
if (len > 0) {
File dir = new File(Cons.DOWNLOAD_DIR);
if (!dir.exists()) {
dir.mkdir();
}
//3.创建等大的本地文件
File file = new File(dir, mFileBean.getFileName());
//创建随机操作的文件流对象,可读、写、删除
raf = new RandomAccessFile(file, "rwd");
raf.setLength(len);//设置文件大小
mFileBean.setLength(len);
//4.从mHandler的消息池中拿个消息,附带mFileBean和MSG_CREATE_FILE_OK标示发送给mHandler
mHandler.obtainMessage(Cons.MSG_CREATE_FILE_OK, mFileBean).sendToTarget();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
try {
if (raf != null) {
raf.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
2.在Service中的使用:DownLoadService
由于Service也是运行在主线程的,访问网络的耗时操作是进制的,所以需要新开线程
由于子线程不能更新UI,这里使用传统的Handler进行线程间通信
/**
* 处理消息使用的Handler
*/
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Cons.MSG_CREATE_FILE_OK:
FileBean fileBean = (FileBean) msg.obj;
//已在主线程,可更新UI
ToastUtil.showAtOnce(DownLoadService.this, "文件长度:" + fileBean.getLength());
download(fileBean);
break;
}
}
};
//下载的Action时开启线程:
new LinkURLThread(fileBean, mHandler).start();
初始连接线程测试.png可见开启线程后,拿到文件大小,Handler发送消息到Service,再在Service(主线程)进行UI的显示(吐司)
三、数据库相关操作:
数据库相关.png先说一下数据库是干嘛用的:记录下载线程的
信息
、信息
、信息
!
当暂停时,将当前下载的进度及线程信息保存到数据库中,当再点击开始是从数据库查找线程信息,恢复下载
1.线程信息封装类:ThreadBean
private int id;//线程id
private String url;//线程所下载文件的url
private long start;//线程开始的下载位置(为多线程准备)
private long end;//线程结束的下载位置
private long loadedLen;//该线程已下载的长度
//构造函数、get、set、toString省略...
2.下载的数据库帮助类:DownLoadDBHelper
关于SQLite可详见SI--安卓SQLite基础使用指南:
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:14:19<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:下载的数据库帮助类
*/
public class DownLoadDBHelper extends SQLiteOpenHelper {
public DownLoadDBHelper(@Nullable Context context) {
super(context, Cons.DB_NAME, null, Cons.VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(Cons.DB_SQL_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(Cons.DB_SQL_DROP);
db.execSQL(Cons.DB_SQL_CREATE);
}
}
3.关于数据库的常量:Cons.java
/**
* 数据库相关常量
*/
public static final String DB_NAME = "download.db";//数据库名
public static final int VERSION = 1;//版本
public static final String DB_TABLE_NAME = "thread_info";//数据库名
public static final String DB_SQL_CREATE = //创建表
"CREATE TABLE " + DB_TABLE_NAME + "(\n" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT,\n" +
"thread_id INTEGER,\n" +
"url TEXT,\n" +
"start INTEGER,\n" +
"end INTEGER,\n" +
"loadedLen INTEGER\n" +
")";
public static final String DB_SQL_DROP =//删除表表
"DROP TABLE IF EXISTS " + DB_TABLE_NAME;
public static final String DB_SQL_INSERT =//插入
"INSERT INTO " + DB_TABLE_NAME + " (thread_id,url,start,end,loadedLen) values(?,?,?,?,?)";
public static final String DB_SQL_DELETE =//删除
"DELETE FROM " + DB_TABLE_NAME + " WHERE url = ? AND thread_id = ?";
public static final String DB_SQL_UPDATE =//更新
"UPDATE " + DB_TABLE_NAME + " SET loadedLen = ? WHERE url = ? AND thread_id = ?";
public static final String DB_SQL_FIND =//查询
"SELECT * FROM " + DB_TABLE_NAME + " WHERE url = ?";
public static final String DB_SQL_FIND_IS_EXISTS =//查询是否存在
"SELECT * FROM " + DB_TABLE_NAME + " WHERE url = ? AND thread_id = ?";
4.数据访问接口:DownLoadDao
提供数据库操作的接口
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:14:36<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:数据访问接口
*/
public interface DownLoadDao {
/**
* 在数据库插入线程信息
*
* @param threadBean 线程信息
*/
void insertThread(ThreadBean threadBean);
/**
* 在数据库删除线程信息
*
* @param url 下载的url
* @param threadId 线程的id
*/
void deleteThread(String url, int threadId);
/**
* 在数据库更新线程信息---下载进度
*
* @param url 下载的url
* @param threadId 线程的id
*/
void updateThread(String url, int threadId ,long loadedLen);
/**
* 获取一个文件下载的所有线程信息(多线程下载)
* @param url 下载的url
* @return 线程信息集合
*/
List<ThreadBean> getThreads(String url);
/**
* 判断数据库中该线程信息是否存在
*
* @param url 下载的url
* @param threadId 线程的id
*/
boolean isExist(String url, int threadId);
}
5.数据库接口实现类:DownLoadDaoImpl
一些基础的SQL操作,个人习惯原生的SQL,在每次操作之后不要忘记关闭db,以及游标
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:14:43<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:数据访问接口实现类
*/
public class DownLoadDaoImpl implements DownLoadDao {
private DownLoadDBHelper mDBHelper;
private Context mContext;
public DownLoadDaoImpl(Context context) {
mContext = context;
mDBHelper = new DownLoadDBHelper(mContext);
}
@Override
public void insertThread(ThreadBean threadBean) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
db.execSQL(Cons.DB_SQL_INSERT,
new Object[]{threadBean.getId(), threadBean.getUrl(),
threadBean.getStart(), threadBean.getEnd(), threadBean.getLoadedLen()});
db.close();
}
@Override
public void deleteThread(String url, int threadId) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
db.execSQL(Cons.DB_SQL_DELETE,
new Object[]{url, threadId});
db.close();
}
@Override
public void updateThread(String url, int threadId, long loadedLen) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
db.execSQL(Cons.DB_SQL_UPDATE,
new Object[]{loadedLen, url, threadId});
db.close();
}
@Override
public List<ThreadBean> getThreads(String url) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
Cursor cursor = db.rawQuery(Cons.DB_SQL_FIND, new String[]{url});
List<ThreadBean> threadBeans = new ArrayList<>();
while (cursor.moveToNext()) {
ThreadBean threadBean = new ThreadBean();
threadBean.setId(cursor.getInt(cursor.getColumnIndex("thread_id")));
threadBean.setUrl(cursor.getString(cursor.getColumnIndex("url")));
threadBean.setStart(cursor.getLong(cursor.getColumnIndex("start")));
threadBean.setEnd(cursor.getLong(cursor.getColumnIndex("end")));
threadBean.setLoadedLen(cursor.getLong(cursor.getColumnIndex("loadedLen")));
threadBeans.add(threadBean);
}
cursor.close();
db.close();
return threadBeans;
}
@Override
public boolean isExist(String url, int threadId) {
SQLiteDatabase db = mDBHelper.getWritableDatabase();
Cursor cursor = db.rawQuery(Cons.DB_SQL_FIND_IS_EXISTS, new String[]{url, threadId + ""});
boolean exists = cursor.moveToNext();
cursor.close();
db.close();
return exists;
}
}
四、核心下载线程:DownLoadThread 与进度广播:BroadcastReceiver
下载核心线程.png1.下载线程:
注意请求中使用Range后,服务器返回的成功状态码是206:不是200,表示:部分内容和范围请求成功 注释写的很详细了,就不赘述了
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:15:10<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:下载线程
*/
public class DownLoadThread extends Thread {
private ThreadBean mThreadBean;//下载线程的信息
private FileBean mFileBean;//下载文件的信息
private long mLoadedLen;//已下载的长度
public boolean isDownLoading;//是否在下载
private DownLoadDao mDao;//数据访问接口
private Context mContext;//上下文
public DownLoadThread(ThreadBean threadBean, FileBean fileBean, Context context) {
mThreadBean = threadBean;
mDao = new DownLoadDaoImpl(context);
mFileBean = fileBean;
mContext = context;
}
@Override
public void run() {
if (mThreadBean == null) {//1.下载线程的信息为空,直接返回
return;
}
//2.如果数据库没有此下载线程的信息,则向数据库插入该线程信息
if (!mDao.isExist(mThreadBean.getUrl(), mThreadBean.getId())) {
mDao.insertThread(mThreadBean);
}
HttpURLConnection conn = null;
RandomAccessFile raf = null;
InputStream is = null;
try {
//3.连接线程的url
URL url = new URL(mThreadBean.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
//4.设置下载位置
long start = mThreadBean.getStart() + mThreadBean.getLoadedLen();//开始位置
//conn设置属性,标记资源的位置(这是给服务器看的)
conn.setRequestProperty("Range", "bytes=" + start + "-" + mThreadBean.getEnd());
//5.寻找文件的写入位置
File file = new File(Cons.DOWNLOAD_DIR, mFileBean.getFileName());
//创建随机操作的文件流对象,可读、写、删除
raf = new RandomAccessFile(file, "rwd");
raf.seek(start);//设置文件写入位置
//6.下载的核心逻辑
Intent intent = new Intent(Cons.ACTION_UPDATE);//更新进度的广播intent
mLoadedLen += mThreadBean.getLoadedLen();
//206-----部分内容和范围请求 不要200写顺手了...
if (conn.getResponseCode() == 206) {
//读取数据
is = conn.getInputStream();
byte[] buf = new byte[1024 * 4];
int len = 0;
long time = System.currentTimeMillis();
while ((len = is.read(buf)) != -1) {
//写入文件
raf.write(buf, 0, len);
//发送广播给Activity,通知进度
mLoadedLen += len;
if (System.currentTimeMillis() - time > 500) {//减少UI的渲染速度
mContext.sendBroadcast(intent);
intent.putExtra(Cons.SEND_LOADED_PROGRESS,
(int) (mLoadedLen * 100 / mFileBean.getLength()));
mContext.sendBroadcast(intent);
time = System.currentTimeMillis();
}
//暂停保存进度到数据库
if (!isDownLoading) {
mDao.updateThread(mThreadBean.getUrl(), mThreadBean.getId(), mLoadedLen);
return;
}
}
}
//下载完成,删除线程信息
mDao.deleteThread(mThreadBean.getUrl(), mThreadBean.getId());
//下载完成后,发送完成度100%的广播
intent.putExtra(Cons.SEND_LOADED_PROGRESS, 100);
mContext.sendBroadcast(intent);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
try {
if (raf != null) {
raf.close();
}
if (is != null) {
is.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3.进度广播:BroadcastReceiver
/**
* 作者:张风捷特烈<br/>
* 时间:2018/11/12 0012:16:05<br/>
* 邮箱:1981462002@qq.com<br/>
* 说明:更新ui的广播接收者
*/
public class UpdateReceiver extends BroadcastReceiver {
private ProgressBar mProgressBar;
public UpdateReceiver(ProgressBar progressBar) {
mProgressBar = progressBar;
}
@Override
public void onReceive(Context context, Intent intent) {
if (Cons.ACTION_UPDATE.equals(intent.getAction())) {
int progress = intent.getIntExtra(Cons.SEND_LOADED_PROGRESS, 0);
mProgressBar.setProgress(progress);
}
}
}
五、将两大部分拼合一起
1.DownLoadService:下载服务
在接收到Handler的信息后调用下载函数
/**
* 下载逻辑
*
* @param fileBean 文件信息对象
*/
public void download(FileBean fileBean) {
//从数据获取线程信息
List<ThreadBean> threads = mDao.getThreads(fileBean.getUrl());
if (threads.size() == 0) {//如果没有线程信息,就新建线程信息
mThreadBean = new ThreadBean(
0, fileBean.getUrl(), 0, fileBean.getLength(), 0);//初始化线程信息对象
} else {
mThreadBean = threads.get(0);//否则取第一个
}
mDownLoadThread = new DownLoadThread(mThreadBean, fileBean, this);//创建下载线程
mDownLoadThread.start();//开始线程
mDownLoadThread.isDownLoading = true;
}
2.开始与停止下载的优化:
@Override//每次启动服务会走此方法
public int onStartCommand(Intent intent, int flags, int startId) {
mDao = new DownLoadDaoImpl(this);
if (intent.getAction() != null) {
switch (intent.getAction()) {
case Cons.ACTION_START:
FileBean fileBean = (FileBean) intent.getSerializableExtra(Cons.SEND_FILE_BEAN);
if (mDownLoadThread != null) {
if (mDownLoadThread.isDownLoading) {
return super.onStartCommand(intent, flags, startId);
}
}
new LinkURLThread(fileBean, mHandler).start();
break;
case Cons.ACTION_STOP:
if (mDownLoadThread != null) {
mDownLoadThread.isDownLoading = false;
}
break;
}
}
return super.onStartCommand(intent, flags, startId);
}
3.Activity中注册和注销广播
/**
* 注册广播接收者
*/
private void register() {
//注册广播接收者
mUpdateReceiver = new UpdateReceiver(mProgressBar);
IntentFilter filter = new IntentFilter();
filter.addAction(Cons.ACTION_UPDATE);
registerReceiver(mUpdateReceiver, filter);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mUpdateReceiver != null) {//注销广播
unregisterReceiver(mUpdateReceiver);
}
}
数据库.png
掘金.png下载完后,安装正常,打开正常,下载OK
后记:捷文规范
1.本文成长记录及勘误表
项目源码 | 日期 | 备注 |
---|---|---|
V0.1--无 | 2018-11-12 | Android原生下载(上篇)基本逻辑+断点续传 |
2.更多关于我
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
我的github | 我的简书 | 我的CSDN | 个人网站 |
3.声明
1----本文由张风捷特烈原创,转载请注明
2----欢迎广大编程爱好者共同交流
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
4----看到这里,我在此感谢你的喜欢与支持
网友评论