優雅設計封裝基於Okhttp3的網絡框架(四):多線程下載添加數據庫支持(greenDao)及 進度更新

通過前三篇博文的學習,已經編碼實現多線程下載功能的核心代碼,通過多個線程之間的管理和調度來處理下載任務,最後再引入隊列機制來完善功能。此篇博文主旨需要將下載的進度存儲到數據庫中,目的是爲了可以在恢復時可以取出進程下載進度,從未下載部分開始下載,能更節省流量,提高用戶體驗。

此篇文章將學習:

  • 多線程下載添加數據庫支持
  • greenDao開源庫自動生成數據庫相關代碼
  • 完善網絡請求接口中的進度更新功能

(建議閱讀此篇文章之前,需理解前兩篇文章的講解,此係列文章是環環相扣,不可缺一,鏈接如下:)
優雅設計封裝基於Okhttp3的網絡框架(一):Http網絡協議與Okhttp3解析
優雅設計封裝基於Okhttp3的網絡框架(二):多線程下載功能原理設計 及 簡單實現
優雅設計封裝基於Okhttp3的網絡框架(三):多線程下載功能核心實現 及 線程池、隊列機制解析


一. 整體項目回顧

首先在進行功能編碼之前,需要將前三篇完成的多線程下載代碼核心類邏輯理清除:

這裏寫圖片描述

基於插件化思想,將 easyokhttp網絡框架作爲項目中的一個module來封裝,這樣不論哪個項目需要網絡框架,將此module添加進去即可。

1. 整體代碼

目前爲止,以上代碼是前3篇博文的所有編碼內容:

  • file
    • FileStorageManager: 文件下載管理類
  • http
    • DownloadCallback:封裝網絡請求調用接口
    • HttpManager:封裝基本的網絡同步、異步請求
  • utils
    • Logger:打印日誌工具
    • Md5Uills: MD5加密工具
  • DownloadManager: 多線程下載主要功能封裝
  • DownloadRunnable:多線程下載功能中的單個線程Runnable
  • DownloadTask:多線程下載任務,DownloadManager多線程下載功能使用到隊列機制,進行增刪任務


2. 多線程下載主要代碼

這裏寫圖片描述

(1)DownloadTask

作用及組成

在上一遍博文教學中,我們在多線程下載功能中加入了隊列機制,所以DownloadTask是一個下載任務類,成員變量有請求Url、接口回調callback,還要實現equals()hashCode()方法,有點類似於一個JavaBean。

使用

既然是因爲“隊列機制”而衍生出的,主要被應用於DownloadManager類中,維護了一個隊列任務集合,在需要增刪下載任務時,會進行相應的操作。實現較爲簡單,查看代碼即可理解,以下是代碼結構圖:

這裏寫圖片描述


(2)DownloadRunnable

作用

既然是實現多線程下載功能,在DownloadManager類需要對多個線程進行管理,在上篇博文講解中該管理類已經使用了線程池,所以當接收到文件下載任務時,將它分給多個線程,各自分配不同的下載起始、結束位置進行下載,而這個DownloadRunnable就是多個線程中的單個線程。

組成

DownloadRunnable實現了Runnable 接口,當創建此單個下載線程時,要傳入必需的下載URL、下載起始位置、終止位置、接口回調這四個成員變量。最重要的就是run() 方法具體實現:根據指定長度進行下載(這裏下載動作還是調用底層封裝好的HttpManager中的同步請求方法),根據響應判斷是否成功再進行接口回調,若成功將生成對應URL的文件,進行讀寫數據。

使用

所以,最主要的文件下載、讀寫操作都是在DownloadRunnable中進行。主要被使用於DownloadManager類中,在接收到文件下載任務時,會進行算法合理分配各個線程下載的文件位置,創建各個線程DownloadRunnable,由線程池對象進行執行execute(...)

這裏寫圖片描述


(3)DownloadManager ☆☆☆☆☆

作用及組成

DownloadManager 類主要實現了多線程下載功能,其中的重點方法download 方法即暴露給上層調用,此方法中運用到了以上兩個重點類:

  • 涉及DownloadTask: 當上層調用多線程下載類 DownloadManager 中的download 方法時,意味着一個任務,根據“隊列機制”,創建新任務添加到此類維護的隊列集合中;當下載任務完成時(不論請求成功或失敗),從集合中移除。

  • 涉及DownloadRunnable:DownloadManager 類的download 方法中主要邏輯爲多線程下載文件,所以再分配好每個線程的具體下載任務後,逐個創建多線程中的單個線程—–DownloadRunnable,令線程池對象執行execute(...)

這裏寫圖片描述


3. 其餘類小結

以上第二點單獨介紹的DownloadTask、DownloadRunnable、DownloadManager這三個類是多線程下載功能的主要實現類,需多注意理解構思這封裝思想,另外將其他的綜合歸納一二。

(1)DownloadCallback

http文件夾中的DownloadCallback:基本的網絡請求回調接口,其中有3個方法:successfailprogress,很常見的寫法,在此無需贅述。

使用

既然是網絡回調接口,所以只要涉及到網絡請求的地方都會使用到,最普通的使用場景就是在調用Okhttp提供的原生網絡請求後,在後續封裝的回調中調用自己封裝請求接口中的方法。

例如在Okhttp3提供的回調onResponse(...)方法中進行一系列邏輯判斷後,在取決於調用自行封裝的success還是fail。至於好處這裏無需多言,自行封裝後避免了重複冗雜的邏輯判斷等等。

這裏寫圖片描述


(2)HttpManager

http文件夾中的HttpManager:注意該類與DownloadManager的區別:

  • 兩者的共同點: 都暴露了下載方法供給上層調用。
  • 區別: 但是DownloadManager類中的下載是基於多線程的,而HttpManager中封裝的方法是基於單線程的,並且只是最基本的網絡同步、異步請求。

使用

  • 該類提供的基本網絡同步、異步請求方法,可直接供給上層調用
  • 該類提供的根據下載位置的網絡同步請求,在多線程下載中的DownloadRunnable類中有被調用到。

這裏寫圖片描述


(3)FileStorageManager

作用及組成

file文件夾中的FileStorageManager類:此類主要是一個文件管理類,重點方法getFileByName(...) —— 根據url獲取文件,邏輯爲首先根據url判斷內存中是否有相對應的文件,若無則重新創建。

使用

  • 主要被使用於DownloadRunnable類中,該類中的run()方法在成功下載文件後,根據url獲取文件,將下載獲取的數據寫到此文件中。
  • 另外在HttpManage類——封裝基本的網絡同步、異步請求也有涉及,在成功下載文件後,,根據url獲取文件,將下載獲取的數據寫到此文件中。

這裏寫圖片描述





二. 添加數據庫支持

以上第一大點我詳細介紹總結歸納了多線程下載功能涉及到的主要類和其它類的作用、使用位置,因爲多線程下載的主要功能已經實現,後面功能的完善、優化等工作是基於此部分上進行修改,所以各位在瞭解以下內容之前一定要將以上部分學習透徹,再細細參悟這封裝思想。

接下來將在多線程下載中再完善一步,即添加數據庫支持。

1. greendao

首先採用greendao這樣一個開源框架,依賴它可以幫組我們自動創建一系列有關於數據庫的相關代碼(這裏不會去詳細講解此開源庫各種使用方法,僅介紹項目有關使用,望讀者自行學習)。

(1)添加依賴

根據github上提示,在module中的build.gradle 文件中添加如下依賴即可:

https://github.com/greenrobot/greenDAO

apply plugin: 'org.greenrobot.greendao'

greendao {
    schemaVersion 1
    daoPackage 'com.anye.greendao.gen'
    targetGenDir 'src/main/java'
}

dependencies {
    ......
    compile 'org.greenrobot:greendao:3.0.1'
    compile 'org.greenrobot:greendao-generator:3.0.0'
}

在項目文件夾中的build.gradle中添加:

dependencies {
        ...
        classpath 'org.greenrobot:greendao-gradle-plugin:3.0.0'

    }

(2)編寫自動生成類

在module中的db包中創建一個實體類 DownloadEntity,用於後續自動生成代碼(GreenDao 3.0採用註解的方式來定義實體類,通過gradle插件生成相應的代碼。),代碼如下:

@Entity
public class DownloadEntity {

    @Id
    private Long id;

    private Long startPosition;
    private Long endPosition;
    private Long progressPosition;
    private String downloadUrl;
    private Integer threadId;
}

以上代碼並不複雜,相當於確定表中數據項,編譯項目後,該類中對應的get/set方法都會自動生成,以下數據操作代碼類也會自動生成在我們指定的位置中(easyokhttp這個module中的db包中):

這裏寫圖片描述

  • DaoMaster類和DaoSession類是用於管理項目中的整個數據庫,自動生成,不建議修改。
  • DownloadEntityDao類是用於管理DownloadEntity這一個實體類,即這張表的創建、刪除相關操作。

(3)編寫調用幫助類DownloadHelper

其實greendao開源庫已經爲DownloadEntity實體類對象提供了CRUD相關API使用,爲了上層調用和擴展性考慮,最好在此基礎上再進行封裝,編寫一個專門的幫助類DownloadHelper,此類以單例模式對外,暴露需要供上層調用的查找、插入等操作。編寫不難,代碼如下:

/**
 * @function 封裝操作DownloadEntity數據增刪改查的基本方法
 * (greendao已爲該實體類提供CRUD方法,此類在此基礎上做基本封裝)
 * 
 * @author lemon Guo
 */
public class DownloadHelper {

    private static DownloadHelper sHelper = new DownloadHelper();

    public static DownloadHelper getInstance() {
        return sHelper;
    }

    private DownloadHelper() {

    }

    public void init(Context context) {
        SQLiteDatabase db = new DaoMaster.DevOpenHelper(context, "download.db", null).getWritableDatabase();
        mMaster = new DaoMaster(db);
        mSession = mMaster.newSession();
        mDao = mSession.getDownloadEntityDao();
    }

    private DaoMaster mMaster;

    private DaoSession mSession;

    private DownloadEntityDao mDao;


    public void insert(DownloadEntity entity) {
        mDao.insertOrReplace(entity);
    }

    public List<DownloadEntity> getAll(String url) {
        return mDao.queryBuilder().where(DownloadEntityDao.Properties.DownloadUrl.eq(url)).orderAsc(DownloadEntityDao.Properties.ThreadId).list();
    }
}

(4)Application中初始化

注意:在下載幫助類DownloadHelper 中的init方法中封裝了初始化download這張表的操作,而對實體類DownloadEntity的數據操作是直接關聯到download表中的數據,所以在項目初始化即
Application中顯示調用此幫助類的初始化方法。

【項目app文件夾下自定義的OkhttpApllication類】
 DownloadHelper.getInstance().init(this);


2. 融合數據庫操作到多線程下載邏輯(DownloadManage、DownloadRunnable)

以上操作已經完成數據庫管理相關代碼,下面需要將數據庫操作與邏輯融合在一起。

(1)DownloadManage中的download方法邏輯

主要需要修改的還是多線程下載核心管理類——DownloadManage其中的download方法,在第一點已經介紹過,這裏再次回顧其方法邏輯:

  • 判斷此下載任務是否已存在於任務隊列(集合)中,若無進行添加。
  • 調用HttpManager網絡同步請求獲取待下載文件總長度。
    • 若成功請求,在響應方法onResponse中分配多個線程各自的下載任務,調用線程池對象進行執行。
  • 網絡請求完成(不論成功或失敗),將該任務從任務隊列(集合)中移除。

以上邏輯,若要融合數據庫操作到多線程下載中,在添加任務隊列後就需要查找本地數據庫是否已存在相關數據:

  • 若數據爲空,則後續網絡請求邏輯不變,但在分配多線程下載任務時,考慮到下載中斷情況,DownloadRunnable執行完時需要將下載的字節位置、url相關信息存儲到本地數據庫中,每次下載時判斷該位置,避免部分數據重複下載,實現斷點續傳。
  • 若數據不爲空(完整數據或者部分數據),則無需下載(或從斷點處繼續下載)。

(2)本地數據庫url對應數據 爲空 的情況

private List<DownloadEntity> mCache;

public void download(final String url, final DownloadCallback callback) {
        ......//添加任務到隊列集合

        //在本地數據庫中查找與url有關數據(以url作爲每個數據的標識)
        mCache = DownloadHelper.getInstance().getAll(url);
        if (mCache == null || mCache.size() == 0) {
            HttpManager.getInstance().asyncRequest(url, new Callback() {
                ......
            });
        }
 }           

    private void processDownload(String url, long length, DownloadCallback callback) {
        // 100   2  50  0-49  50-99
        long threadDownloadSize = length / MAX_THREAD;
        if (mCache == null && mCache.size() == 0) {
            mCache = new ArrayList<>();
        }
        for (int i = 0; i < MAX_THREAD; i++) {
            DownloadEntity entity = new DownloadEntity();
            long startSize = i * threadDownloadSize;
            long endSize = 0;
            if (endSize == MAX_THREAD - 1) {
                endSize = length - 1;
            } else {
                endSize = (i + 1) * threadDownloadSize - 1;
            }

            //將每個線程下載的具體信息存儲到DownloadEntity實體類中,後續在DownloadRunnable進行操作,存儲到表中。
            entity.setDownload_url(url);
            entity.setStart_position(startSize);
            entity.setEnd_position(endSize);
            entity.setThread_id(i + 1);
            sThreadPool.execute(new DownloadRunnable(startSize, endSize, url, callback, entity));
        }
    }

新增代碼分析

除了本地數據判斷外,這裏主要是修改了processDownload方法,在分配給每個線程下載任務時,將相關數據存儲到DownloadEntity實體類,然後創建DownloadRunnable時傳入該參數。

涉及DownloadRunnable修改

DownloadRunnable類是由線程池對象進行操作執行其run方法,在第一點中已詳細介紹其邏輯就是根據創建時傳入的URL、文件具體位置進行下載,然後將數據寫入到本地文件File中。

但現在多傳入的這個參數DownloadEntity主要是考慮到後續邏輯,即下載過程中可能出現中斷的情況,爲了避免重複下載,實現斷點續傳的功能,需要引入這一實體類,在run方法最後(不論下載成功或失敗),將讀寫到File的實際長度等相關信息記錄到DownloadEntity實體類,然後寫入到本地數據庫中。

DownloadRunnable類新增代碼

    private DownloadEntity mEntity;

    public DownloadRunnable(long mStart, long mEnd, String mUrl, DownloadCallback mCallback, DownloadEntity mEntity) {
        this.mStart = mStart;
        this.mEnd = mEnd;
        this.mUrl = mUrl;
        this.mCallback = mCallback;
        this.mEntity = mEntity;
    }

    @Override
    public void run() {
        Response response = HttpManager.getInstance().syncRequestByRange(mUrl, mStart, mEnd);
        if (response == null && mCallback != null) {
            mCallback.fail(HttpManager.NETWORK_ERROR_CODE, "網絡出問題了");
            return;
        }
        File file = FileStorageManager.getInstance().getFileByName(mUrl);

//每次下載時去獲取本地數據庫中相關信息(避免重複下載,實現斷點續傳)
        long finshProgress = mEntity.getProgress_position() == null ? 0 : mEntity.getProgress_position();
        long progress = 0;
        try {
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rwd");
            randomAccessFile.seek(mStart);
            byte[] buffer = new byte[1024 * 500];
            int len;
            InputStream inStream = response.body().byteStream();
            while ((len = inStream.read(buffer, 0, buffer.length)) != -1) {
                randomAccessFile.write(buffer, 0, len);
                //記錄寫入文件的長度
                progress += len;
                mEntity.setProgress_position(progress);
            }

     //不論下載成功或失敗),將讀寫到File的實際長度等相關信息記錄到DownloadEntity實體類       
                   mEntity.setProgress_position(mEntity.getProgress_position() + finshProgress);
            randomAccessFile.close();
            mCallback.success(file);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
        //同步,即插入數據到數據庫中
            DownloadHelper.getInstance().insert(mEntity);
        }
    }

(3)本地數據庫url對應數據 不爲空 的情況

如果從本地數據庫中查找的對應數據(集合)不爲空,意味着可能存在下載過程中出現中斷的情況,此時直接循環數據集合,因爲每個對象中都記錄着當前下載的位置、url相關信息,遍歷所有創建線程調用線程池對象進行下載,上面已實現DownloadRunnable 的run方法中會判斷傳入參數實體類的相關數據,以其記錄位置開始下載,避免某些數據重複下載,實現斷點續傳功能,

    public void download(final String url, final DownloadCallback callback) {
        ......//添加任務到隊列集合

        mCache = DownloadHelper.getInstance().getAll(url);
        if (mCache == null || mCache.size() == 0) {
            HttpManager.getInstance().asyncRequest(url, new Callback() {
             ......
            });

        } else {
            // 處理已經下載過的數據,即從斷點處繼續下載
            for (int i = 0; i < mCache.size(); i++) {
                DownloadEntity entity = mCache.get(i);
                if (i == mCache.size() - 1) {
                    mLength = entity.getEnd_position() + 1;
                }
                long startSize = entity.getStart_position() + entity.getProgress_position();
                long endSize = entity.getEnd_position();
                sThreadPool.execute(new DownloadRunnable(startSize, endSize, url, callback, entity));
            }
        }
 }




三. 進度更新

1. 實現思想

進度更新的實現方法有多種,最容易想到的首先是根據線程下載長度來判斷,可是這有一個隱形問題,文件下載採用的是多線程下載,還可能存在下載中斷的情況,所以以線程下載爲準來編寫會有些複雜。

這裏採取一種簡單實現方法,以本地數據庫中文件的變化大小來判斷,可以避免多線程造成的問題,而我們需要編碼的只是不斷監控本地文件大小,算出百分比呈現出即可。


2. 代碼實現

需要修改的代碼還是多線程下載核心管理類—— DownloadManager注意“監控”必然會是一個耗時的操作,所以創建一個線程ExecutorService來實現,調用該對象的execute方法,創建一個Runnable,其run方法核心邏輯就是一個死循環,每過500毫秒獲取本地文件大小,計算百分比,將其結果調用回調DownloadCallback中的progress方法傳送出去,這樣可以在調用此請求的地方進行UI顯示相關操作,若百分比達到100,則跳出死循環。

ExecutorService進行監控的位置就在DownloadManager類中的download方法最後,因爲無論是重新下載還是斷點續傳下載,都會有一個進度上的更新。

   private static ExecutorService sLocalProgressPool = Executors.newFixedThreadPool(LOCAL_PROGRESS_SIZE);


    public void download(final String url, final DownloadCallback callback) {
        //添加任務至隊列
        ......

        mCache = DownloadHelper.getInstance().getAll(url);
        if (mCache == null || mCache.size() == 0) {
            HttpManager.getInstance().asyncRequest(url, new Callback() {
            ......
            });

        } else {
            // 處理已經下載過的數據,即從斷點處繼續下載
            ......
            }
        }

        //進度更新
        sLocalProgressPool.execute(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(500);
                        File file = FileStorageManager.getInstance().getFileByName(url);
                        long fileSize = file.length();
                        int progress = (int) (fileSize * 100.0 / mLength);
                        if (progress >= 100) {
                            callback.progress(progress);
                            return;
                        }
                        callback.progress(progress);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

3. 測試

測試代碼依舊不變,只是在增加進度更新功能後,在進度回調方法中設置進度條顯示即可。

final String url = "http://img1.gtimg.com/20/2000/200037/20003735_980x1200_0.png";
        DownloadManager.getInstance().download(url, new DownloadCallback() {
            @Override
            public void success(File file) {
               final Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath());

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mImageView.setImageBitmap(bitmap);
                    }
                });
                Logger.debug("MainActivity", "success " + file.getAbsoluteFile());

            }

            @Override
            public void fail(int errorCode, String errorMessage) {
                Logger.debug("MainActivity", "fail " + errorCode + "  " + errorMessage);
            }

            @Override
            public void progress(int progress) {
                mProgress.setProgress(progress);
            }
        });

這裏寫圖片描述





四. 總結

1. 本篇總結

此篇文章內容完成了添加數據庫支持、進度更新的功能代碼編寫,內容有些多,若要完全理解需多花時間消化。其中重點在於需要對前三篇博文完成的多線程下載核心類 ——- DownloadManagerRuunable的邏輯和整體思想熟透於心,這樣在後續融合數據庫支持時,纔會思路清晰,瞭解在哪裏需要添加數據庫支持。

  • 在第一大點中講解了藉助greendao開源庫實現數據庫相關代碼自動生成,開發者只需要寫好實體類,在編譯時藉助gradle會生成系列數據庫、表創建、刪除代碼,非常方便,使用也不難,推薦最後在此基礎上封裝體幫助類,對代碼以後擴展和解耦有很大幫助。

  • 在第二大點中正式融合數據庫代碼操作到多線程下載邏輯中,其中主要修改的是DownloadManagerRuunable類,增添代碼的核心邏輯就是將各個線程已下載的位置長度(存在未下載完成時出現中斷的情況)、url信息存儲到本地數據庫,在每次下載前首先判斷本地數據庫中存儲的下載情況,根據本地數據爲空與否,而選擇是否重新下載或者斷點續傳。

  • 在完成以上兩大點之後,進度更新的實現就非常簡單了,實現思路很多,這裏採用比較簡單的一種:以本地數據庫中文件的變化大小來判斷,可以避免多線程造成的問題,而我們需要編碼的只是不斷監控本地文件大小,算出百分比呈現出即可。

這是前三篇博文對應的源碼,關於此篇博文修改後的源碼正在整理,稍後貼出,正好讀者可以先捋清封裝思路,自行思考


2. 下篇預告

此篇內容編寫實現其實並不容易,其中的封裝思想一定要多加思考揣摩,博主這兩天在完成的過程中遇到了些許bug,還有一些編寫時遺漏的細節問題,讀者可對應代碼理解思考。

在下一篇博文中將對代碼進行優化,開發一個新功能並不複雜,難的是考慮到代碼的擴展性和解耦性,後續需要進行的bug修復、完善功能等方面。主要從代碼優化,將從線程優化、單例優化、設計優化這三個方面進行講解。



若有問題,虛心指教~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章