1.今天带来的是Android原生下载的上篇,主要核心是断点续传,多线程下载将会在下篇介绍
2.本例使用了Activity
,Service
,BroadcastReceiver
三个组件
3.本例使用了两个线程:LinkURLThread
作一些初始工做,DownLoadThread
进行核心下载工做
4.本例使用SQLite进行暂停时的进度保存,使用Handler进行消息的传递,使用Intent进行数据传递
5.对着代码,整理了一下思路,画了一幅下面的流程图,感受思路清晰多了
6.本例比较基础,但串联了Android的不少知识点,做为总结仍是很不错的。java
改善了一下界面UI,整个画风都不一样了,我的感受还不错,用了之前的自定义进度条:详见android
先实现上面一半的代码:git
既然是下载,固然要有连接了,就那掘金的apk来测试吧!查看方式:github
public class FileBean implements Serializable {
private int id;//文件id
private String url;//文件下载地址
private String fileName;//文件名
private long length;//文件长度
private long loadedLen;//文件已下载长度
//构造函数、get、set、toString省略...
}
复制代码
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;
复制代码
界面比较简单,就不贴了编程
/**
* 点击下载时逻辑
*/
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);//启动服务---中止标示
}
复制代码
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;
}
}
复制代码
不要忘记注册Service:
<service android:name=".service.DownLoadService"/>
经过点击两个按钮,测试能够看出FileBean对象的传递和下载开始、中止的逻辑没有问题bash
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();
}
}
}
}
复制代码
因为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();
复制代码
可见开启线程后,拿到文件大小,Handler发送消息到Service,再在Service(主线程)进行UI的显示(吐司)网络
先说一下数据库是干吗用的:记录下载线程的
信息
、信息
、信息
!
当暂停时,将当前下载的进度及线程信息保存到数据库中,当再点击开始是从数据库查找线程信息,恢复下载
private int id;//线程id
private String url;//线程所下载文件的url
private long start;//线程开始的下载位置(为多线程准备)
private long end;//线程结束的下载位置
private long loadedLen;//该线程已下载的长度
//构造函数、get、set、toString省略...
复制代码
关于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);
}
}
复制代码
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 = ?";
复制代码
提供数据库操做的接口 ,至于为何要一个dao的接口,直接用实现类不行吗,这里重点说一下
接口体现的是一种能力保证,实现类的对象是具备这种能力的对象之一。
若是你很是肯定这种实现不会改变(即这里肯定一种用SQLite),直接使用实现类固然能够。
不过若是你不想存入数据库了,而是存在文件里或SP里,那全部与实现类相关的部分都要修改,若是散布各个地方,还不崩溃。
使用接口的好处在于,无论你黑猫白狗(实现方案),帮我抓住耗子(解决问题)就好了。
因此你彻底能够写一套在文件里储存线程信息的方案,而后实现dao里的方法,
再只要更换代码中的dao实现就能够轻松地将黑猫(数据库实现)切换成白狗(文件操做实现),
固然你也能够准备一头猫头鹰(SP实现),或一门灭鼠大炮(网络流实现),这样就让下载逻辑和存储逻辑解耦
你想上午让白狗(文件操做实现)抓老鼠,下午让白猫(数据库实现),晚上让猫头鹰(SP实现),都不是问题
这就是面相接口编程的好处,若是你遇到相似的情形,不少实现都各有优劣,你彻底能够面相接口,后期再根据不一样的需求写实现
复制代码
/**
* 做者:张风捷特烈<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);
}
复制代码
一些基础的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;
}
}
复制代码
注意请求中使用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();
}
}
}
}
复制代码
注意这里并不是只能用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);
for (ProgressBar progressBar : mProgressBar) {
progressBar.setProgress(progress);
}
}
}
}
复制代码
在接收到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;
}
复制代码
@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);
}
复制代码
/**
* 注册广播接收者
*/
private void register() {
//注册广播接收者
mUpdateReceiver = new UpdateReceiver(mProgressBar,mIdRoundPb);
IntentFilter filter = new IntentFilter();
filter.addAction(Cons.ACTION_UPDATE);
registerReceiver(mUpdateReceiver, filter);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mUpdateReceiver != null) {//注销广播
unregisterReceiver(mUpdateReceiver);
}
}
复制代码
下载完后,安装正常,打开正常,下载OK
项目源码 | 日期 | 备注 |
---|---|---|
V0.1--无 | 2018-11-12 | Android原生下载(上篇)基本逻辑+断点续传 |
V0.1--无 | 2018-11-13 | UI界面优化 |
笔名 | 微信 | 爱好 | |
---|---|---|---|
张风捷特烈 | 1981462002 | zdl1994328 | 语言 |
个人github | 个人简书 | 个人CSDN | 我的网站 |
1----本文由张风捷特烈原创,转载请注明 2----欢迎广大编程爱好者共同交流 3----我的能力有限,若有不正之处欢迎你们批评指证,一定虚心改正 4----看到这里,我在此感谢你的喜欢与支持