Android原生下載(上篇)基本邏輯+斷點續傳

零、前言

1.今天帶來的是Android原生下載的上篇,主要核心是斷點續傳,多線程下載將會在下篇介紹
2.本例使用了ActivityServiceBroadcastReceiver三個組件
3.本例使用了兩個線程:LinkURLThread作一些初始工做,DownLoadThread進行核心下載工做
4.本例使用SQLite進行暫停時的進度保存,使用Handler進行消息的傳遞,使用Intent進行數據傳遞
5.對着代碼,整理了一下思路,畫了一幅下面的流程圖,感受思路清晰多了
6.本例比較基礎,但串聯了Android的不少知識點,做爲總結仍是很不錯的。java

2018-11-13更新:

改善了一下界面UI,整個畫風都不一樣了,我的感受還不錯,用了之前的自定義進度條:詳見android

效果展現.png

斷點續傳邏輯總覽

斷點續傳邏輯總覽.png


1、前置準備工做

先實現上面一半的代碼:git

初始準備.png

1.關於下載的連接:

既然是下載,固然要有連接了,就那掘金的apk來測試吧!查看方式:github

查看下載地址.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對象的傳遞和下載開始、中止的邏輯沒有問題bash

測試.png


2、下載的初始線程及使用:

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


3、數據庫相關操做:

數據庫相關.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

提供數據庫操做的接口 ,至於爲何要一個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);
}

複製代碼
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;
    }
}
複製代碼

4、核心下載線程: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

注意這裏並不是只能用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);

            }
        }
    }
}
複製代碼

5、將兩大部分拼合一塊兒

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,mIdRoundPb);
    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原生下載(上篇)基本邏輯+斷點續傳
V0.1--無 2018-11-13 UI界面優化
2.更多關於我
筆名 QQ 微信 愛好
張風捷特烈 1981462002 zdl1994328 語言
個人github 個人簡書 個人CSDN 我的網站
3.聲明

1----本文由張風捷特烈原創,轉載請註明 2----歡迎廣大編程愛好者共同交流 3----我的能力有限,若有不正之處歡迎你們批評指證,一定虛心改正 4----看到這裏,我在此感謝你的喜歡與支持

相關文章
相關標籤/搜索