美文网首页手机移动程序开发Android技术栈Android进阶之路
Android原生下载(上篇)基本逻辑+断点续传

Android原生下载(上篇)基本逻辑+断点续传

作者: e4e52c116681 | 来源:发表于2018-11-12 23:48 被阅读21次

    零、前言

    1.今天带来的是Android原生下载的上篇,主要核心是断点续传,多线程下载将会在下篇介绍
    2.本例使用了ActivityServiceBroadcastReceiver三个组件
    3.本例使用了两个线程:LinkURLThread做一些初始工作,DownLoadThread进行核心下载工作
    4.本例使用SQLite进行暂停时的进度保存,使用Handler进行消息的传递,使用Intent进行数据传递
    5.对着代码,整理了一下思路,画了一幅下面的流程图,感觉思路清晰多了
    6.本例比较基础,但串联了Android的很多知识点,作为总结还是很不错的。

    断点续传逻辑总览.png

    一、前置准备工作

    先实现上面一半的代码:

    初始准备.png
    1.关于下载的链接:

    既然是下载,当然要有链接了,就那掘金的apk来测试吧!查看方式:

    查看下载地址.png
    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;
        }
    }
    

    不要忘记注册Service:<service android:name=".service.DownLoadService"/>
    通过点击两个按钮,测试可以看出FileBean对象的传递和下载开始、停止的逻辑没有问题

    测试.png

    二、下载的初始线程及使用:

    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();
    
    

    可见开启线程后,拿到文件大小,Handler发送消息到Service,再在Service(主线程)进行UI的显示(吐司)

    初始连接线程测试.png

    三、数据库相关操作:

    数据库相关.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

    下载核心线程.png
    1.下载线程:

    注意请求中使用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

    下载完后,安装正常,打开正常,下载OK

    掘金.png

    后记:捷文规范

    1.本文成长记录及勘误表
    项目源码 日期 备注
    V0.1--无 2018-11-12 Android原生下载(上篇)基本逻辑+断点续传
    2.更多关于我
    笔名 QQ 微信 爱好
    张风捷特烈 1981462002 zdl1994328 语言
    我的github 我的简书 我的CSDN 个人网站
    3.声明

    1----本文由张风捷特烈原创,转载请注明
    2----欢迎广大编程爱好者共同交流
    3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正
    4----看到这里,我在此感谢你的喜欢与支持

    相关文章

      网友评论

        本文标题:Android原生下载(上篇)基本逻辑+断点续传

        本文链接:https://www.haomeiwen.com/subject/jhghfqtx.html