安卓多线程下载文件(一)

安卓多线程下载文件(一)

前言

可以说只要是软件开发行业,大多数都会有文件下载的场景吧,那么这个系列来简单写下如何实现一个文件下载的功能;

以应用推送新版本升级为背景,来做一个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;
    }

}

接下来下载文件的实现请看 安卓多线程下载文件(二)

四、参考文献

java多线程下载文件(断点下载、进度展示、网速展示)

【Java多线程】如何使用Java多线程下载网络文件 断点续传

java 多线程下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值