安卓多线程下载文件(一)
前言
可以说只要是软件开发行业,大多数都会有文件下载的场景吧,那么这个系列来简单写下如何实现一个文件下载的功能;
以应用推送新版本升级为背景,来做一个APP 应用新版本文件下载安装的功能;
由于这个功能涉及到的知识稍微有点多,所以将分成几篇文章来写;
由于要满足断点续下功能,即当文件下载了一部分,因为某些原因被迫中断,我们希望程序恢复的时候,不是重新下,而是从上次中断的地方接着下;
要满足这样的要求,那么我们就得使用数据库来记录一些重要的信息。
一、分析
假设把下载文件比喻成这样一个场景:
把A地区的砖(文件)搬到 B地区;(就像把服务器上的文件 下载 到本地 机器的sd卡的某个目录下);
A地区有一堆砖块,假设总共100块砖;
如果只有1人搬,那么
这个人分到的砖块是100,开始位置是0,结束位置是100;
分给2人搬,那么
第一个人编号为0,分到砖块50,开始位置是0,结束位置是50;
第二个人编号为1,分到砖块50,开始位置是50,结束位置是100;
分给3人搬,那么
第一个人编号为0,分到砖块33,开始位置是0,结束位置是33;
第二个人编号为1,分到砖块33,开始位置是33,结束位置是66;
第三个人编号为2,分到砖块34,开始位置是66,结束位置是100;
依次类推
二、设计数据表
2.1设计数据表字段
创建一个TableThreadInfo类,用来表示线程信息表有哪些字段;
需要
数据表的名字
id
线程编号 ,因为要使用多条线程来下载文件,线程编号是为了区分是哪个线程
地址 ,被下载的文件放在什么地方
开始位置
结束位置
已下载的字节数
资源总长度
public class TableThreadInfo {
public static String TABLE_NAME_THREAD_INFO = "threadInfo"; //线程信息表的名字
public static String ID = "id";
public static String THREAD_ID = "threadId"; //线程的编号
public static String URL = "url"; //资源网址,被下载的资源在什么地方
public static String START_POSITION = "startPosition"; //这条线程负责下载的开始位置
public static String END_POSITION = "endPosition";//这条线程负责下载的结束位置
public static String DOWNLOADED_LENGTH = "downloaded_length";// 已经下载了多少字节
public static String TOTAL_LENGTH = "total_length"; //被下载的资源的总长度是多少
}
2.2设计数据库的实现类
创建一个 DBHelper.java 继承安卓系统的SQLiteOpenHelper 类;
重写必要的构造函数;
定义建表语句:
id 用整型,自动增长;
线程id, 整型;
文件的地址, 字符串;
剩下的
开始位置
结束位置
已下载的字节数
纵长度
都用长整型
所以具体实现如下:
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class DBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "test.db";// 数据库 名称
private static int DB_VERSION = 1;//数据库 版本号
public DBHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
public DBHelper(Context context, int version) {
this(context, DB_NAME, null, version);
}
public DBHelper(Context context) {
this(context, DB_VERSION);
}
/*** 线程信息 */
public String CREATE_TABLE_THREAD_INFO_SQL = "create table if not exists " + TableThreadInfo.TABLE_NAME_THREAD_INFO + " ("
+ TableThreadInfo.ID + " " + "integer primary key autoincrement, "
+ TableThreadInfo.THREAD_ID + " " + "integer,"
+ TableThreadInfo.URL + " " + "text,"
+ TableThreadInfo.START_POSITION + " " + "long,"
+ TableThreadInfo.END_POSITION + " " + "long,"
+ TableThreadInfo.DOWNLOADED_LENGTH + " " + "long,"
+ TableThreadInfo.TOTAL_LENGTH + " " + "long"
+ ")";
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CREATE_TABLE_THREAD_INFO_SQL);
}
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
deleteTableThreadInfoThenReCreate(sqLiteDatabase);
}
public void deleteTableThreadInfoThenReCreate(SQLiteDatabase sqLiteDatabase) {
try {
String sqlDelTableThreadInfo = "drop table if exists " + TableThreadInfo.TABLE_NAME_THREAD_INFO;
sqLiteDatabase.execSQL(sqlDelTableThreadInfo); // 删除 线程信息 表
sqLiteDatabase.execSQL(CREATE_TABLE_THREAD_INFO_SQL); //创建 线程信息 表
//Log.i(TAG, "deleteTablePickInfoThenReCreate 重新 创建 线程信息 表 ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.3创建实体ThreadInfo
这个比较简单,就不多说了;
import java.io.Serializable;
public class ThreadInfo implements Serializable {
private int threadId;
// url标识这个线程是属于哪个任务
// 根据 url 确定是哪个 FileInfo
private String url;
// 这个线程要完成从 startPosition 开始到 endPosition 这一区间的下载
// 像一个 100 KB 的文件,我们用三个线程对它进行下载,
// 三个线程分别完成 0KB - 33KB,33KB - 66KB,66KB - 100KB
private long startPosition;
private long endPosition;
// 已经下载了多少
private long downloadedLength;
// 总共要下载的是多大
private long totalLength;
public ThreadInfo() {
}
public int getThreadId() {
return threadId;
}
public void setThreadId(int threadId) {
this.threadId = threadId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public long getStartPosition() {
return startPosition;
}
public void setStartPosition(long startPosition) {
this.startPosition = startPosition;
}
public long getEndPosition() {
return endPosition;
}
public void setEndPosition(long endPosition) {
this.endPosition = endPosition;
}
public long getDownloadedLength() {
return downloadedLength;
}
public void setDownloadedLength(long downloadedLength) {
this.downloadedLength = downloadedLength;
}
public long getTotalLength() {
return totalLength;
}
public void setTotalLength(long totalLength) {
this.totalLength = totalLength;
}
}
2.4 实现线程信息表的增删查改
在下载前 我们需要按照线程数量 来分配每条线程 应该负责下载哪部分;
所以需要把分配好的信息插入到我前面设计好的 threadInfo 表中,就需要一个插入数据的方法,即
insertData;
中断或暂停下载时,我们需要更新 threadInfo 中对应的记录;
updateData
恢复下载前,我们需要查询已经下载了多少等信息;
所以需要getThreads 方法
下载完成后,要删掉对应的记录;
所以完整的实现如下:
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;
import com.linkpoon.mixed.bean.ThreadInfo;
import com.linkpoon.mixed.table.TableThreadInfo;
import java.util.ArrayList;
import java.util.List;
public class ThreadInfoManager {
public ThreadInfoManager() {
}
/**
* 插入一条记录
*/
public synchronized final long insertData(Context context, ThreadInfo info) {
DBHelperManager manager = DBHelperManager.getInstance(context);
SQLiteDatabase db = manager.openDb();
ContentValues values = new ContentValues();
// id是自增长的,这里不要手动填值了
//values.put(TableThreadInfo.ID, info.getId());
values.put(TableThreadInfo.THREAD_ID, info.getThreadId());
values.put(TableThreadInfo.URL, info.getUrl());
values.put(TableThreadInfo.START_POSITION, info.getStartPosition());
values.put(TableThreadInfo.END_POSITION, info.getEndPosition());
values.put(TableThreadInfo.DOWNLOADED_LENGTH, info.getDownloadedLength());
values.put(TableThreadInfo.TOTAL_LENGTH, info.getTotalLength());
long rowId = db.insert(TableThreadInfo.TABLE_NAME_THREAD_INFO, null, values);
manager.closeDb();
return rowId;
}
public synchronized final int deleteData(Context context, String url) {
DBHelperManager manager = DBHelperManager.getInstance(context);
SQLiteDatabase db = manager.openDb();
int sum = db.delete(TableThreadInfo.TABLE_NAME_THREAD_INFO, TableThreadInfo.URL + "=?", new String[]{url});
manager.closeDb();
return sum;
}
public synchronized final int deleteData(Context context, int threadId) {
DBHelperManager manager = DBHelperManager.getInstance(context);
SQLiteDatabase db = manager.openDb();
int sum = db.delete(TableThreadInfo.TABLE_NAME_THREAD_INFO, TableThreadInfo.THREAD_ID + "=?", new String[]{threadId + ""});
manager.closeDb();
return sum;
}
/**
* 按 id 和 url 更新一条数据
**/
public synchronized final int updateData(Context context, ThreadInfo info) {
DBHelperManager manager = DBHelperManager.getInstance(context);
SQLiteDatabase db = manager.openDb();
ContentValues values = new ContentValues();
// 更新的时候,一般情况下id是不用的更新的
//values.put(TableThreadInfo.ID, info.getId());
values.put(TableThreadInfo.THREAD_ID, info.getThreadId());
values.put(TableThreadInfo.URL, info.getUrl());
values.put(TableThreadInfo.START_POSITION, info.getStartPosition());
values.put(TableThreadInfo.END_POSITION, info.getEndPosition());
values.put(TableThreadInfo.DOWNLOADED_LENGTH, info.getDownloadedLength());
values.put(TableThreadInfo.TOTAL_LENGTH, info.getTotalLength());
int rows = db.update(TableThreadInfo.TABLE_NAME_THREAD_INFO, values, TableThreadInfo.THREAD_ID + "=?" + " and " + TableThreadInfo.URL + "=?", new String[]{info.getThreadId() + "", info.getUrl()});
manager.closeDb();
return rows;
}
public List<ThreadInfo> getThreads(Context context, String url) {
DBHelperManager manager = DBHelperManager.getInstance(context);
SQLiteDatabase db = manager.openDb();
Cursor cursor = db.query(TableThreadInfo.TABLE_NAME_THREAD_INFO,
null,
TableThreadInfo.URL + "=?",
new String[]{url},
null,
null,
TableThreadInfo.ID + " asc");
List<ThreadInfo> list = cursorToList(cursor);
cursor.close();
manager.closeDb();
return list;
}
public static List<ThreadInfo> cursorToList(Cursor cursor) {
List<ThreadInfo> list = new ArrayList<>();
while (cursor.moveToNext()) {
int index1 = cursor.getColumnIndex(TableThreadInfo.THREAD_ID);
int threadId = 0;
if (index1 >= 0) {
threadId = cursor.getInt(index1);
}
String url = "";
int index2 = cursor.getColumnIndex(TableThreadInfo.URL);
if (index2 >= 0) {
url = cursor.getString(index2);
}
long start = 0;
int index3 = cursor.getColumnIndex(TableThreadInfo.START_POSITION);
if (index3 >= 0) {
start = cursor.getLong(index3);
}
long end = 0;
int index4 = cursor.getColumnIndex(TableThreadInfo.END_POSITION);
if (index4 >= 0) {
end = cursor.getLong(index4);
}
long downloaded = 0;
int index5 = cursor.getColumnIndex(TableThreadInfo.DOWNLOADED_LENGTH);
if (index5 >= 0) {
downloaded = cursor.getLong(index5);
}
int index6 = cursor.getColumnIndex(TableThreadInfo.TOTAL_LENGTH);
long total = 0;
if (index6 >= 0) {
total = cursor.getLong(index6);
}
if (!TextUtils.isEmpty(url)) {
ThreadInfo threadInfo = new ThreadInfo();
threadInfo.setUrl(url);
threadInfo.setThreadId(threadId);
threadInfo.setStartPosition(start);
threadInfo.setEndPosition(end);
threadInfo.setTotalLength(total);
threadInfo.setDownloadedLength(downloaded);
list.add(threadInfo);
}
}
return list;
}
}
2.5 设计打开关闭数据库的控制类
使用单例模式
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
public class DBHelperManager {
private static int count = 0;
public DBHelper getDbHelper() {
return dbHelper;
}
private DBHelper dbHelper;
public SQLiteDatabase getDb() {
return db;
}
private SQLiteDatabase db;
private DBHelperManager(Context context) {
dbHelper = new DBHelper(context);
}
private static DBHelperManager instance;
public static synchronized DBHelperManager getInstance(Context context) {
if (instance == null) {
instance = new DBHelperManager(context);
}
return instance;
}
public synchronized SQLiteDatabase openDb() {
if (count == 0 && dbHelper != null) {
db = dbHelper.getReadableDatabase();
}
count++;
return db;
}
public synchronized void closeDb() {
count--;
if (count == 0) {
if (getDb() != null) {
getDb().close();
}
}
}
}
三、设计网络访问工具类
由于被下载的文件是放在服务器上的,所以我们需要通过访问网址来获得要下载的文件的信息;
所以设计了一个Http 工具类
其中的LogUtil 只是对安卓系统Log 类做了一层包装 ,非常简单,代码就不贴了;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
/**
* Http工具
*/
public class HttpEngine {
private final static String TAG = "HttpEngine";
private static HttpEngine instance = null;
/**
* 编码
*/
private final static String ENCODE_TYPE = "UTF-8";
public final static String METHOD_POST = "POST";
public final static String METHOD_GET = "GET";
/**
* 超时时长
*/
private final static int TIME_OUT = 10000;
/**
* 获取单例
*/
public static HttpEngine getInstance() {
if (instance == null) {
instance = new HttpEngine();
}
return instance;
}
/**
* 获取http请求的connection,失败返回null
*/
private HttpURLConnection getConnection(String serverUrl, String requestMethod, Map<String, String> paramsMap) {
HttpURLConnection connection = null;
try {
URL url = new URL(serverUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod(requestMethod);
if (METHOD_POST.equals(requestMethod)) {
connection.setDoInput(true);
connection.setDoOutput(true);
} else {
connection.setDoInput(true);
connection.setDoOutput(false);
}
connection.setUseCaches(false);
connection.setReadTimeout(TIME_OUT);
connection.setConnectTimeout(TIME_OUT);
connection.setRequestProperty("contentType", ENCODE_TYPE);
connection.setRequestProperty("Connection", "close");
connection.setRequestProperty("Response-Type", "json");
if (paramsMap != null) {
for (String key : paramsMap.keySet()) {
try {
if (paramsMap.get(key) != null) {
connection.setRequestProperty(key, paramsMap.get(key));
}
} catch (Exception e) {
LogUtil.e(TAG, "getConnection,Exception: ", e);
}
}
}
} catch (IOException e) {
LogUtil.e(TAG, "getConnection,IOException: ", e);
}
return connection;
}
/**
* 发送http请求
*/
public String sendRequests(String[] serverUrls,
String requestMethod,
Map<String, String> paramsMap,
byte[] data,
int length) {
String result = null;
for (int i = 0; i < serverUrls.length; i++) {
String serverUrl = serverUrls[i];
try {
HttpURLConnection connection = getConnection(serverUrl, requestMethod, paramsMap);
if (length > 0) {
connection.setRequestProperty("Content-Length", String.valueOf(length));
}
connection.connect();
if (length > 0) {
OutputStream os = connection.getOutputStream();
os.write(data, 0, length);
os.flush();
os.close();
}
int respCode = connection.getResponseCode();
if (respCode == HttpURLConnection.HTTP_OK) {
InputStream is = connection.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
is.close();
baos.close();
connection.disconnect();
result = new String(baos.toByteArray());
LogUtil.i(TAG, "sendRequests response: result.length()= " + result.length());
} else if (respCode == 302) {// 重定向
String newServerURL = "";
result = sendRequest(newServerURL, requestMethod, paramsMap, data, length);
connection.disconnect();
} else if (respCode == 401) {// 身份验证
LogUtil.i(TAG, "getResponseCode: 401 ,rand = " + connection.getHeaderField("Rand"));
// String Rand = connection.getHeaderField("Rand");
// String
// auth=EncryptUtil.SHA1(paramsMap.get("TelNum")+paramsMap.get("Password")+Rand);
// paramsMap.put("auth", auth);
result = sendRequest(serverUrl, requestMethod, paramsMap, data, length);
connection.disconnect();
} else {// 其他错误码
LogUtil.i(TAG, "getResponseCode: " + respCode);
//result = "error ,ResponseCode= " + respCode;
connection.disconnect();
}
} catch (Exception e) {// 连接错误
LogUtil.e(TAG, "sendRequests error:", e);
}
if (result != null) {
break;
}
}
return result;
}
/**
* 发送一次http请求
*/
public String sendRequest(String serverUrl,
String requestMethod,
Map<String, String> paramsMap,
byte[] data,
int length) {
String result = null;
try {
HttpURLConnection connection = getConnection(serverUrl, requestMethod, paramsMap);
if (length > 0) {
connection.setRequestProperty("Content-Length", String.valueOf(length));
}
connection.connect();
if (length > 0) {
OutputStream os = connection.getOutputStream();
os.write(data, 0, length);
os.flush();
os.close();
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
InputStream is = connection.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
is.close();
baos.close();
connection.disconnect();
result = new String(baos.toByteArray());
LogUtil.i(TAG, "sendRequest response: result.length()= " + result.length());
return result;
} else {
//result = connection.getResponseMessage();
connection.disconnect();
}
} catch (Exception e) {// 连接错误
// result = e.getMessage();
LogUtil.e(TAG, "sendRequest error:", e);
}
return result;
}
}
接下来下载文件的实现请看 安卓多线程下载文件(二)