Android原生实现多线程断点下载

各位父老乡亲,我单汉三又回来了,今天为大家带来一个用原生的安卓写的下载Demo。

通过本文你可以学习到:

  • SQLite的基本使用,数据库的增删改查。
  • Handler的消息处理与更新UI(你也可以看我的这篇文章)。
  • Service(主要用于下载)的进阶与使用。
  • 原生的json文件解析(多层嵌套)。
  • RandomAccessFile的基本使用,可以将文件分段。
  • 基于HttpURLConnection的大文件下载。
  • 上面内容结合,实现多线程,断点下载。

Demo是在TV上运行的,图片显示的问题不要纠结了。

文件下载的Demo已完成,没时间上传与讲解,今天为您展示并讲解一下,纯原生的东西来下载文件,希望可以帮你理解更多安卓比较基础的问题。

我们的思路:建立一个数据库,两个表,一个用来保存网络数据,一个保存本地下载的进度等等。在点击下载按钮的时候启动DownloadService,在Service中开启线程进行下载文件,文件采用RandomAccessFile,可进行多线程分段下载,如不是第一次下载,比对之后进行断点下载。

先看一下Demo的目录结构:

所有的步骤在代码里有非常详细的讲解,一定要看代码(下面是抽取的几个重要的类讲解)!

数据库的建立与DAO

DownLoadDBHelper  创建数据库  数据库更新升级,检测到版本的变化,发现版本号不一样,就会自动调用onUpgrade函数。

/**
 * Created by Administrator on 2017/3/6 0006.
 */

public class DownLoadDBHelper extends SQLiteOpenHelper {
    /**
     * DownLoadDBHelper用于创建数据库,如果不会使用原生的建库的话
     *
     * 跟随小司机我的脚步来一起练一练
     * 建两个表:
     * download_info表存储下载信息
     * localdownload_info表存储本地下载信息
     * 之后对比两个表进行继续下载等等
     */

    public static String DATABASE_NAME = "downloadFILES.db";
    public static String TABLE_DOWNLOAD_INFO = "download_info";
    public static String TABLE_LOCALDOWNLOAD_INFO = "localdownload_info";
    private static int version = 1;


    public DownLoadDBHelper(Context context) {
        super(context, DATABASE_NAME, null, version);
    }


    @Override
    public void onCreate(SQLiteDatabase db) {
        /*在此进行创建数据库和表格,来一起动手写一遍,就是两个sqlite语句*/

        db.execSQL("create table " + TABLE_DOWNLOAD_INFO + "(" + "id integer PRIMARY KEY AUTOINCREMENT," +
                "thread_id integer," + "start_position integer," + "end_position integer," + " completed_size integer," + "url varchar(100))");
        db.execSQL("create table " + TABLE_LOCALDOWNLOAD_INFO + "(" + "id integer PRIMARY KEY AUTOINCREMENT," + "name varchar(50)," +
                "url varchar(100)," + "completedSize integer," + "fileSize integer," + "status integer)");

    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        /*数据库更新升级,检测到版本的变化,发现版本号不一样,就会自动调用onUpgrade函数
        * 新版本号和老版本号都会作为onUpgrade函数的参数传进来,便于开发者知道数据库应该从哪个版本升级到哪个版本。
        * */
        String sql = "drop table if exists " + TABLE_DOWNLOAD_INFO + "";
        String sqlOne = "drop table if exists " + TABLE_LOCALDOWNLOAD_INFO + "";
        db.execSQL(sql);
        db.execSQL(sqlOne);
        onCreate(db);//删除数据库,重新创建。这里只是简单的,并没有添加或者减少数据库中的其他字段

    }
}

DAO对数据库进行增删改查

DAO层主要是做数据持久层的工作,负责与数据库进行联络的一些任务都封装在此。对数据库的操作,我们基本要用到的就是新增,更新,删除,查询等方法。

/**
 * Created by ShanCanCan on 2017/3/6 0006.
 */

public class Dao {
    /*
    * DAO层主要是做数据持久层的工作,负责与数据库进行联络的一些任务都封装在此。
    * DAO层所定义的接口里的方法都大同小异,这是由我们在DAO层对数据库访问的操作来决定的,
    * 对数据库的操作,我们基本要用到的就是新增,更新,删除,查询等方法。
    * 因而DAO层里面基本上都应该要涵盖这些方法对应的操作。
    * */
    private static Dao dao;
    private static DownLoadDBHelper dbHelper;
    public static final byte[] Lock = new byte[0]; //新建两个字节作为对象锁
    public static final byte[] file_Lock = new byte[0];

    public Dao() {//空构造方法,
    }

    public static synchronized Dao getInstance(Context context) {//本demo用单例模式中的懒汉模式+线程不安全  线程安全的代价是效率变低
        if (dao == null) {
            dao = new Dao();
            dbHelper = new DownLoadDBHelper(context);
        }
        return dao;
    }

    /* public static synchronized Dao getInstance(Context context) {//本demo用单例模式中的懒汉模式+线程安全  线程安全的代价是效率变低,99%情况下不需要同步
        if (dao == null) {  //你可以在这两个方法中随便选择一个
            dao = new Dao();
            dbHelper = new DownLoadDBHelper(context);
        }
        return dao;
    }*/

    /***************************************   下方Dao层中对数据库的增、删、改、查   *********************************************************/


    /**
     * 检查本地下载记录,是否下载过
     *
     * @param url
     * @return
     */
    public boolean isExist(String url) {
        SQLiteDatabase database = dbHelper.getReadableDatabase(); //获取本app所创建的数据库
        String sql = "select count(*) from " + TABLE_LOCALDOWNLOAD_INFO + " where url=?"; //查询语句,查询总共有多少条的语句
        Cursor cursor = database.rawQuery(sql, new String[]{url});
        /**
         *
         * @Cursor
         *  Cursor 是每行的集合。
         *  使用 moveToFirst() 定位第一行。
         *  你必须知道每一列的名称。
         *  你必须知道每一列的数据类型。
         *  Cursor 是一个随机的数据源。
         *  所有的数据都是通过下标取得。
         *  Cursor按照我的理解就是一个箭头,指到哪一行就是那一行的集合
         *  比较重要的方法有:close(),moveToFirst(),moveToNext(),moveToLast(),moveToPrevious(),getColumnCount()等。
         *
         * @rawQuery
         * rawQuery是直接使用SQL语句进行查询的,也就是第一个参数字符串,
         * 在字符串内的“?”会被后面的String[]数组逐一对换掉
         * cursor用完之后要关闭,cursor用完之后要关闭,cursor用完之后要关闭。重要的事情说三遍!!!
         *
         * */
        cursor.moveToFirst();
        int count = cursor.getInt(0);
        cursor.close();
        return count > 0;
    }

    /**
     * 是否为首次下载
     *
     * @param url
     * @return
     */
    public boolean isFirstDownload(String url) {
        SQLiteDatabase database = dbHelper.getReadableDatabase();
        String sql = "select count(*) from " + TABLE_DOWNLOAD_INFO + " where url=?";
        Cursor cursor = database.rawQuery(sql, new String[]{url});
        cursor.moveToFirst();
        int count = cursor.getInt(0);
        cursor.close();
        return count == 0;
    }

    /**
     * 保存下载的具体信息 保存所下载的list集合中的数据
     *
     * @param infos
     * @param context
     */
    public void saveInfos(List infos, Context context) {
        /**
         * 事务(Transaction)是并发控制的单位,是用户定义的一个操作序列。
         * 这些操作要么都做,要么都不做,是一个不可分割的工作单位。
         * 通过事务,SQL Server能将逻辑相关的一组操作绑定在一起,
         * 以便保持数据的完整性。
         *
         * 事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、
         * 隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。
         *
         * */
        synchronized (Lock) {
            SQLiteDatabase database = dbHelper.getWritableDatabase();
            database.beginTransaction();//开启事务
            try {//如果有异常,在这里捕获
                for (DownLoadInfo info : infos) {//for循环将数据存入数据库
                    String sql = "insert into " + TABLE_DOWNLOAD_INFO + "(thread_id,start_position, end_position, completed_size, url) values (?,?,?,?,?)";
                    Object[] bindArgs = {info.getThreadId(), info.getStartPosition(), info.getEndPosition(), info.getCompletedSize(), info.getUrl()};
                    database.execSQL(sql, bindArgs);
                }
                database.setTransactionSuccessful();//结束事务
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                database.endTransaction();//关闭事务

            }
        }


    }


    /**
     * 得到下载具体信息
     *
     * @param urlstr
     * @return List 一个下载器信息集合器,里面存放了每条线程的下载信息
     */
    public List getInfos(String urlstr) {
        List list = new ArrayList();
        SQLiteDatabase database = dbHelper.getReadableDatabase();
        String sql = "select thread_id, start_position, end_position, completed_size, url from " + TABLE_DOWNLOAD_INFO + " where url=?";
        Cursor cursor = database.rawQuery(sql, new String[]{urlstr});
        while (cursor.moveToNext()) {//通过cursor取到下载器信息,循环遍历,得到下载器集合
            DownLoadInfo info = new DownLoadInfo(cursor.getInt(0), cursor.getInt(1), cursor.getInt(2), cursor.getInt(3), cursor.getString(4));
            list.add(info);
        }
        cursor.close();
        return list;
    }

    /**
     * 本地下载列表添加记录,添加本地数据库信息,完成度等等
     *
     * @param fileStatus
     **/
    public void insertFileStatus(FileStatus fileStatus) {
        synchronized (file_Lock) {//异步加开启事务,保证数据的完整性
            SQLiteDatabase database = dbHelper.getWritableDatabase();
            database.beginTransaction();
            try {
                String sql = "insert into " + TABLE_LOCALDOWNLOAD_INFO + " (name,url,completedSize,fileSize,status) values(?,?,?,?,?)";
                Object[] bindArgs = {fileStatus.getName(), fileStatus.getUrl(), fileStatus.getCompletedSize(), fileStatus.getFileSize(), fileStatus.getStatus()};
                database.execSQL(sql, bindArgs);
                database.setTransactionSuccessful();

            } catch (SQLException e) {
                e.printStackTrace();

            } finally {
                database.endTransaction();

            }

        }

    }

    /**
     * @param context
     * @param compeletedSize
     * @param threadId
     * @param urlstr         这里是更新数据库,建议在保存一个表格的时候就对另一个表格数据库进行更新
     */

    public void updataInfos(int threadId, int compeletedSize, String urlstr, Context context) {
        synchronized (Lock) {
            String sql = "update " + TABLE_DOWNLOAD_INFO + "set  completed_size  = ? where thread_id =? and url=?";
            String localSql = "update " + TABLE_LOCALDOWNLOAD_INFO + "set completedSize = (select sum(completed_size) from " +
                    TABLE_DOWNLOAD_INFO + "where url=? group by url ) where url=?";
            Object[] bindArgs = {compeletedSize, threadId, urlstr};
            Object[] localArgs = {urlstr, urlstr};
            SQLiteDatabase database = dbHelper.getWritableDatabase();
            database.beginTransaction();
            try {
                database.execSQL(sql, bindArgs);
                database.execSQL(localSql, localArgs);
                database.setTransactionSuccessful();

            } catch (SQLException e) {
                e.printStackTrace();

            } finally {
                database.endTransaction();
            }

        }

    }

    /**
     * @param url 更新文件的状态,0为正在下载,1为已经下载完成,2为下载出错
     **/
    public void updateFileStatus(String url) {
        synchronized (file_Lock) {
            String sql = "update " + TABLE_LOCALDOWNLOAD_INFO + " set status = ? where url = ?";
            Object[] bindArgs = {1, url};
            SQLiteDatabase database = dbHelper.getWritableDatabase();
            database.beginTransaction();
            try {
                database.execSQL(sql, bindArgs);
                database.setTransactionSuccessful();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                database.endTransaction();
            }
        }
    }


    /**
     * @return List
     * 取出本地下载列表数据,如在重新进入应用时,要重新把进度之类的设置好
     **/
    public List getFileStatus() {
        List list = new ArrayList();
        SQLiteDatabase database = dbHelper.getReadableDatabase();
        //String sql = "slect * from " + TABLE_LOCALDOWNLOAD_INFO + "";  //不能用,需要哪些条件就在语句中写出哪些条件
        String sql = "select name, url, status, completedSize, fileSize from " + TABLE_LOCALDOWNLOAD_INFO + "";
        Cursor cursor = database.rawQuery(sql, null);
        while (cursor.moveToNext()) {
            FileStatus fileState = new FileStatus(cursor.getString(0), cursor.getString(1), cursor.getInt(2), cursor.getInt(3), cursor.getInt(4));
            list.add(fileState);
        }
        cursor.close();
        return list;
    }


    /**
     * @param url
     * @param completeSize
     * @param status       更新文件的下载状态
     **/
    public void updateFileDownStatus(int completeSize, int status, String url) {
        synchronized (file_Lock) {
            String sql = "update " + TABLE_LOCALDOWNLOAD_INFO + " set completedSize = ?,status = ? where url = ?";
            SQLiteDatabase database = dbHelper.getWritableDatabase();
            database.beginTransaction();
            try {
                Object[] bindArgs = {completeSize, status, url};
                database.execSQL(sql, bindArgs);
                database.delete(TABLE_DOWNLOAD_INFO, "url = ?", new String[]{url});
                database.setTransactionSuccessful();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                database.endTransaction();
            }
        }
    }

    /**
     * @param url 获取文件名称
     **/
    public String getFileName(String url) {
        String result = "";
        String sql = "select name from " + TABLE_LOCALDOWNLOAD_INFO + " where url = ?";
        SQLiteDatabase database = dbHelper.getReadableDatabase();
        Cursor cursor = database.rawQuery(sql, new String[]{url});
        if (cursor.moveToNext()) {
            result = cursor.getString(0);
        }
        cursor.close();
        return result;
    }

    /**
     * 删除文件之后,要删除下载的数据,一个是用户可以重新下载
     * 另一个是表再次添加一条数据的时候不出现错误
     *
     * @param url
     */
    public void deleteFile(String url) {
        SQLiteDatabase database = dbHelper.getWritableDatabase();
        database.beginTransaction();
        try {
            database.delete(TABLE_DOWNLOAD_INFO, " url = ?", new String[]{url});
            database.delete(TABLE_LOCALDOWNLOAD_INFO, " url = ?", new String[]{url});
            database.setTransactionSuccessful();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            database.endTransaction();
        }
    }

    /**
     * 关闭数据库
     *
     * @close
     */
    public void closeDB() {
        dbHelper.close();
    }

}

DownloadService 主要代码

开启线程进行下载 文件保存地址 保存每个文件下载的下载器 每个下载文件完成的长度

@SuppressLint("HandlerLeak")
public class DownloadService extends Service
{

    public IBinder binder = new MyBinder();

    public class MyBinder extends Binder
    {
        public DownloadService getService()
        {
            return DownloadService.this;
        }
    }

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

    public static int number = 0;

    // 文件保存地址
    public final String savePath = "/mnt/sdcard/Download/";

    // 存放下载列表的引用
    public static List list = new ArrayList();

    public static Map localDownList = new HashMap();
    // 保存每个文件下载的下载器
    public static Map downloaders = new HashMap();
    // 每个下载文件完成的长度
    private Map completeSizes = new HashMap();
    // 每个下载文件的总长度
    private Map fileSizes = new HashMap();

    private Downloader downloader;
    private int threadCount = 5;

    private Dao dao;
    private DownLoadCallback loadCallback;

    private FileStatus mFileStatus = null;

    private Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            super.handleMessage(msg);
            if (msg.what == 1)
            {
                String url = (String) msg.obj;
                int length = msg.arg1;
                int completeSize = completeSizes.get(url);
                int fileSize = fileSizes.get(url);

                completeSize += length;
                completeSizes.put(url, completeSize);

                synchronized (list)
                {
                    for (int i = 0; i < list.size(); i++)
                    {
                        FileStatus fileStatus = list.get(i);
                        if (fileStatus.getUrl().equals(url))
                        {
                            if (completeSize == fileStatus.getFileSize())
                            {

                                list.set(i, new FileStatus(fileStatus.getName(), fileStatus.getUrl(), 1, completeSize, fileStatus.getFileSize()));
                                dao.updateFileDownStatus(completeSize, 1, url);
                            }
                            else
                            {
                                list.set(i, new FileStatus(fileStatus.getName(), fileStatus.getUrl(), 0, completeSize, fileStatus.getFileSize()));
                            }
                            mFileStatus = list.get(i);
                        }
                    }

                    this.postDelayed(new Runnable()
                    {
                        @Override
                        public void run()
                        {
                            if (loadCallback != null && mFileStatus != null)
                            {
                                loadCallback.refreshUI(mFileStatus);
                            }
                        }
                    }, 1000);
                }

            }
        }
    };

    @Override
    public void onCreate()
    {
        super.onCreate();
        dao = Dao.getInstance(this);

        list = dao.getFileStatus();
        for (FileStatus fileStatus : list)
        {
            localDownList.put(fileStatus.getUrl(), fileStatus.getUrl());
        }

        Timer timer = new Timer();
        timer.schedule(new TimerTask()
        {
            @Override
            public void run()
            {
                number++;
            }
        }, 0, 1000);
    }

    public void download(final Button button, final String url, final String name, final Handler mHandler)
    {
        if (dao.isExist(url))
        {
            Toast.makeText(this, "别点了,已经在下载了", Toast.LENGTH_SHORT).show();
            return;
        }

        final String fileName = name + url.substring(url.lastIndexOf("."));

        new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                downloader = downloaders.get(url);
                if (downloader == null)
                {
                    downloader = new Downloader(url, savePath, fileName, threadCount, DownloadService.this, handler);
                    downloaders.put(url, downloader);
                }
                if (downloader.isDownloading())
                    return;

                LoadInfo loadInfo = downloader.getDownloaderInfors();

                if(loadInfo != null)
                {
                    FileStatus fileStatus = new FileStatus(fileName, url, 0, loadInfo.getComplete(), loadInfo.getFileSize());
                    dao.insertFileStatus(fileStatus);
                    completeSizes.put(url, loadInfo.getComplete());
                    fileSizes.put(url, fileStatus.getFileSize());

                    list.add(fileStatus);
                    localDownList.put(url, url);

                    downloader.download();

                    Message msg = new Message();
                    msg.what = 1;
                    msg.obj = button;
                    mHandler.sendMessage(msg);
                }
                else
                {
                    Message msg = new Message();
                    msg.what = 2;
                    msg.obj = button;
                    mHandler.sendMessage(msg);
                }
            }
        }).start();
    }

    //暂停下载
    public void Pause(Downloader downloader)
    {
        downloader.pause();
    }

    //继续下载
    public void reDownload(final Button button, final String url, final String name, final Handler mHandler)
    {
        new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                String fileName = dao.getFileName(url);

                downloader = downloaders.get(url);
                if (downloader == null)
                {
                    downloader = new Downloader(url, savePath, fileName, threadCount, DownloadService.this, handler);
                    downloaders.put(url, downloader);
                }
                if (downloader.isDownloading())
                    return;

                LoadInfo loadInfo = downloader.getDownloaderInfors();

                if(loadInfo != null && !fileName.equals(""))
                {
                    if(!completeSizes.containsKey(url))
                    {
                        completeSizes.put(url, loadInfo.getComplete());
                    }
                    if(!fileSizes.containsKey(url))
                    {
                        fileSizes.put(url, loadInfo.getFileSize());
                    }

                    downloader.download();

                    Message msg = new Message();
                    msg.what = 1;
                    msg.obj = button;
                    mHandler.sendMessage(msg);
                }
                else
                {
                    Message msg = new Message();
                    msg.what = 2;
                    msg.obj = button;
                    mHandler.sendMessage(msg);
                }
            }
        }).start();
    }

    public void delete(final String url)
    {
        Downloader down = downloaders.get(url);
        if(down != null)
        {
            down.pause();
        }

        handler.postDelayed(new Runnable()
        {
            @Override
            public void run()
            {
                dao.deleteFile(url);

                for (int i = 0; i < list.size(); i++)
                {
                    FileStatus fileStatus = list.get(i);
                    if (fileStatus.getUrl().equals(url))
                    {
                        list.remove(i);
                    }
                }

                localDownList.remove(url);
                downloaders.remove(url);
                completeSizes.remove(url);
                fileSizes.remove(url);

                if(loadCallback != null)
                {
                    loadCallback.deleteFile(url);
                }
            }
        }, 1000);
    }

    public interface DownLoadCallback
    {
        public void refreshUI(FileStatus fileStatus);

        public void deleteFile(String url);
    }

    public void setLoadCallback(DownLoadCallback loadCallback)
    {
        this.loadCallback = loadCallback;
    }
}

下载工具类DownLoadUtil

此类的主要功能 1、检查是否下载 2、下载文件,文件的下载采用httpurlconnection  定义三种下载的状态:初始化状态,正在下载状态,暂停状态。 进行下载的比对。

/**
 * Created by ShanCanCan on 2017/3/7 0007.
 */

public class DownLoadUtil {

    /**
     * 此类的主要功能
     * 1、检查是否下载
     * 2、下载文件,文件的下载采用httpurlconnection
     */
    private String downPath;// 下载路径
    private String savePath;// 保存路径
    private String fileName;// 文件名称
    private int threadCount;// 线程数
    private Handler mHandler;
    private Dao dao;
    private Context context;
    private int fileSize;// 文件大小
    private int range;
    private List infos;// 存放下载信息类的集合
    private int state = INIT;
    private static final int INIT = 1;// 定义三种下载的状态:初始化状态,正在下载状态,暂停状态
    private static final int DOWNLOADING = 2;
    private static final int PAUSE = 3;

    /**
     * 构造方法,获取dao的对象
     *
     * @param downPath
     * @param savePath
     * @param fileName
     * @param threadCount
     * @param context
     * @param mHandler
     */
    public DownLoadUtil(String downPath, String savePath, String fileName, int threadCount, Handler mHandler, Context context) {
        this.downPath = downPath;
        this.savePath = savePath;
        this.fileName = fileName;
        this.threadCount = threadCount;
        this.mHandler = mHandler;
        this.context = context;
        dao = Dao.getInstance(context);
    }


    /**
     * 判断是否PAUSE
     **/
    public boolean isPause() {
        return state == PAUSE;
    }


    /**
     * 判断是否DOWNLOADING
     */
    public boolean isDownloading() {
        return state == DOWNLOADING;
    }

    /**
     * @param url 判断是否是第一次下载,利用dao查询数据库中是否有下载这个地址的记录
     */
    private boolean isFirst(String url) {
        return dao.isFirstDownload(url);
    }

    /**
     * 获取要下载的东西
     */

    public LoadItemInfo getDownloadInfos() {
        if (isFirst(downPath)) {
            if (initFirst()) {//如果是第一次下载的话,要进行初始化,1.获得下载文件的长度 2.创建文件,设置文件的大小
                range = this.fileSize / this.threadCount;
                infos = new ArrayList();

                //这里就是启动多线程下载,看出来了吗?配合RandomAccessFile。每一个DownLoadInfo就是RandomAccessFile文件的一部分
                for (int i = 0; i < this.threadCount - 1; i++) {
                    DownLoadInfo info = new DownLoadInfo(i, i * range, (i + 1) * range - 1, 0, downPath);
                    infos.add(info);
                }
                DownLoadInfo info = new DownLoadInfo(this.threadCount - 1, (this.threadCount - 1) * range, this.fileSize, 0, downPath);

                infos.add(info);
                dao.saveInfos(infos, this.context);
                //(String urlDownload, int completePercent, int fileSize)
                LoadItemInfo loadInfo = new LoadItemInfo(this.downPath, 0, this.fileSize);
                return loadInfo;
            } else {
                return null;
            }


        } else {
            //不是第一次下载,我们应该怎么做呢?从数据库里面取回来

            infos = dao.getInfos(this.downPath);
            if (infos != null && infos.size() > 0) {
                int size = 0;
                int completeSize = 0;

                for (DownLoadInfo info : infos) {
                    completeSize += info.getCompletedSize();
                    size += info.getEndPosition() - info.getStartPosition() + this.threadCount - 1;
                }
                LoadItemInfo loadInfo = new LoadItemInfo(this.downPath, completeSize, size);
                return loadInfo;
            } else {
                return null;
            }

        }
    }

    // 设置暂停
    public void pause() {
        state = PAUSE;
    }

    // 重置下载状态,将下载状态设置为init初始化状态
    public void reset() {
        state = INIT;
    }

    /**
     * 基本上,RandomAccessFile的工作方式是,把DataInputStream和DataOutputStream结合起来,再加上它自己的一些方法,
     * 比如定位用的getFilePointer( ),在文件里移动用的seek( ),以及判断文件大小的length( )、skipBytes()跳过多少字节数。
     * 此外,它的构造函数还要一个表示以只读方式("r"),还是以读写方式("rw")打开文件的参数 (和C的fopen( )一模一样)。它不支持只写文件。
     */
    private boolean initFirst() {
        boolean result = true;

        HttpURLConnection conn = null;
        RandomAccessFile randomFile = null;
        URL url = null;
        try {
            url = new URL(downPath);
            conn = (HttpURLConnection) url.openConnection();
            conn.setConnectTimeout(5 * 1000);
            conn.setRequestMethod("GET");
            // 如果http返回的代码是200或者206则为连接成功
            if (conn.getResponseCode() == 200 || conn.getResponseCode() == 206)  //状态码(206),表示服务器已经执行完部分对资源的GET请求
            {
                fileSize = conn.getContentLength();// 得到文件的大小
                if (fileSize <= 0) {
                    //("网络故障,无法获取文件大小");
                    return false;
                }
                File dir = new File(savePath);
                // 如果文件目录不存在,则创建
                if (!dir.exists()) {
                    if (dir.mkdirs()) {
                        //("mkdirs success.");
                    }
                }
                File file = new File(this.savePath, this.fileName);
                randomFile = new RandomAccessFile(file, "rwd");
                randomFile.setLength(fileSize);// 设置保存文件的大小
                randomFile.close();
                conn.disconnect();
            }
        } catch (Exception e) {
            e.printStackTrace();
            result = false;
        } finally {
            if (randomFile != null) {
                try {
                    randomFile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (conn != null) {
                conn.disconnect();
            }
        }
        return result;
    }

    /**
     * 下面的这个方法就是开启多线程进行下载了数据了
     */

    public void downLoad() {

        if (infos != null) {
            if (state == DOWNLOADING) {
                return;
            }
            state = DOWNLOADING;// 把状态设置为正在下载
            for (DownLoadInfo info : infos) {//为什么说我们是多线程呢?因为我们分别用新线程去下载刚才分割好的一个RandomAccessFile文件
                new DownLoadThread(info.getThreadId(), info.getStartPosition(), info.getEndPosition(), info.getCompletedSize(), info.getUrl(), this.context).start();
            }
        }

    }

    /**
     * 现在要创建线程用来下载了,这里采用内部类
     */


    public class DownLoadThread extends Thread {
        private int threadId;
        private int startPostion;
        private int endPostion;
        private int compeletedSize;
        private String url;
        private Context context;


        public static final int PROGRESS = 1;

        public DownLoadThread(int threadId, int startPostion, int endPostion, int compeletedSize, String url, Context context) {//构造方法,传入特定的参数
            this.threadId = threadId;
            this.startPostion = startPostion;
            this.endPostion = endPostion;
            this.compeletedSize = compeletedSize;
            this.url = url;
            this.context = context;
        }


        //开始下载


        @Override
        public void run() {
            HttpURLConnection conn = null;
            RandomAccessFile randomAccessFile = null;
            InputStream inStream = null;
            File file = new File(savePath, fileName);

            URL url = null;


            try {
                url = new URL(this.url);
                conn = (HttpURLConnection) url.openConnection();
                constructConnection(conn);

                if (conn.getResponseCode() == 200 || conn.getResponseCode() == 206) {
                    randomAccessFile = new RandomAccessFile(file, "rwd");

                    randomAccessFile.seek(this.startPostion + this.compeletedSize);//RandomAccessFile移动指针,到需要下载的块
                    inStream = conn.getInputStream();
                    byte buffer[] = new byte[4096];//这个4096为么子呢?我也不知道,就是看阿里的人下载apk的时候都用4096,我也用
                    int length = 0;
                    while ((length = inStream.read(buffer, 0, buffer.length)) != -1) {
                        randomAccessFile.write(buffer, 0, length);
                        compeletedSize += length;
                        // 更新数据库中的下载信息
                        dao.updataInfos(threadId, compeletedSize, this.url, this.context);
                        // 用消息将下载信息传给进度条,对进度条进行更新
                        Message message = Message.obtain();
                        message.what = PROGRESS;
                        message.obj = this.url;
                        message.arg1 = length;
                        mHandler.sendMessage(message);// 给DownloadService发送消息
                        if (state == PAUSE) {
                            //("-----pause-----");
                            return;
                        }
                    }
                    //  ("------------线程:" + this.threadId + "下载完成");
                }

            } catch (IOException e) {
                e.printStackTrace();
                //("-----下载异常-----"); 这里下载异常我就不处理了,你可以发一条重新下载的消息
            } finally {//用完只后流要关闭,不然容易造成资源抢占,内存泄漏

                try {
                    if (inStream != null) {
                        inStream.close();
                    }
                    if (randomAccessFile != null) {
                        randomAccessFile.close();
                    }
                    if (conn != null) {
                        conn.disconnect();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }


            }


        }

        /**
         * 构建请求连接时的参数 返回开始下载的位置
         *
         * @param conn
         */
        private void constructConnection(HttpURLConnection conn) throws IOException {
            conn.setConnectTimeout(5 * 1000);// 设置连接超时5秒
            conn.setRequestMethod("GET");// GET方式提交,如果你是用post请求必须添加 conn.setDoOutput(true); conn.setDoInput(true);
            conn.setRequestProperty(
                    "Accept",
                    "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*");
            conn.setRequestProperty("Accept-Language", "zh-CN");
            conn.setRequestProperty("Referer", this.url);
            conn.setRequestProperty("Charset", "UTF-8");
            int startPositionNew = this.startPostion + this.compeletedSize;
            // 设置获取实体数据的范围
            conn.setRequestProperty("Range", "bytes=" + startPositionNew + "-" + this.endPostion);
            conn.setRequestProperty(
                    "User-Agent",
                    "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)");
            conn.setRequestProperty("Connection", "Keep-Alive");
            conn.connect();

        }


    }


}

进入查看下载进度

代码的逻辑很好懂,看完记得点赞!

Github地址(求star,求星星):https://github.com/Shanlovana/AndroidStudy

你可能感兴趣的:(学习总结)