ThinDownloadManager源碼解析

關于Android自帶的DownloadManager可能大家都知道,使用自帶的DownloadManager可以很方便的就實現文件下載。還不知道的也沒關系,看看下面的示例代碼你就知道怎么去使用了。

DownloadManager manager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
//下載請求
DownloadManager.Request down = new DownloadManager.Request(Uri.parse("https://ss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/logo_top_ca79a146.png"));
//設置允許使用的網絡類型
down.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
//禁止發出通知,既后臺下載
down.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
//下載完成文件存放的位置
down.setDestinationInExternalFilesDir(MainActivity.this, null, "logo.png");
//將下載請求放入隊列中,開始下載
manager.enqueue(down);

是不是很簡單,直接使用系統自帶的,幾行代碼就搞定了一個下載。使用這種方法,默認的會在系統的通知欄告訴用戶有個下載的任務正在進行,如果不想讓用戶知道有任務正在進行你就必須加上

down.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);````

但是單單加這個還是不行的,你不信可以運行一次試試,應用會直接Crash掉。我們還需要在清單文件聲明下面這個權限:
```html
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />

如果不再在意這些直接使用系統自帶的DownloadManager 就足夠了,但是很多人不想聲明權限,就想讓任務在后臺運行(ps:不要問我為什么,我什么都不知道),這樣就需要自己去實現下載功能了。

該說說ThinDownloadManager了,其實我也是無意中發現這個的,因為我想在我的開源項目MaterialDesignDemo中實現一個圖片下載的功能,但是又不想自己去造輪子,我就想有一個下載功能而已,也不想使用那些很笨重的網絡框架,所以就去到處找,最后發現了這個東東,比較輕量級,使用起來也還是比較的方便。因為我自己之前本來也計劃自己去擼一個網絡相關的框架,但是如果想寫一個網絡框架涉及的東西太多了,而我現在也還沒有達到那個層次,所以就放棄了,平時使用的也就是對一些開源框架的再封裝而已。

現在發現了這個,也還是對他的源碼比較好奇,所以就決定去研究一下他的源碼,看完了之后,我發現這個框架的結構比較簡單不算復雜,于是就決定把我怎么去分析ThinDownloadManager這個框架源碼的過程記錄下來,給那些還不知道怎么去分析源碼的同學一點經驗。雖然這個庫在現在已經有點過時了,但是他的一些思想還是值得我們去學習的。學習Android一定要學會看源碼,不要一遇到問題就一昧的去網上搜,這個怎么實現,那個怎么實現,其實你只要先看看源碼,可能問題很容易的就解決了,看源碼對自己的提升也是顯而易見的,就算以后Android不火了,但是他的很多編程思想是值得借鑒的
ThinDownloadManager的GitHub地址:ThinDownloadManager

在分析源碼之前我們先來看看ThinDownloadManager怎么去使用吧,如果連怎么使用都不會,分析源碼也就是紙上談兵了



ThinDownloadManager的使用也還是比較簡單的,和系統自帶的差不多,我這里是為了我以后使用的方便所以就做了一次再封裝,只需傳入下載地址,保存地址,還有下載時的回調就可以了。

  1. 根據使用的順序,我們就先來看看DownloadRequest,其實根據單詞意思我們都能大概知道這里面是封裝的肯定是下載請求的一些參數。
    先來看看他的構造方法
public DownloadRequest(Uri uri) {
        if (uri == null) {
            throw new NullPointerException();
        }

        String scheme = uri.getScheme();
        if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
            throw new IllegalArgumentException("Can only download HTTP/HTTPS URIs: " + uri);
        }
        mCustomHeader = new HashMap<>();
        mDownloadState = DownloadManager.STATUS_PENDING;
        mUri = uri;
    }

在構造方法中對傳入的uri做了校驗,以及一些初始化的操作。
在示例代碼中我們在使用DownloadRequest的時候,我們用到了setRetryPolicy和setDestinationURI以及setStatusListener,所以現在我們定位到這幾個方法的源碼中去。
看看setRetryPolicy(關于RetryPolicy 就是重試策略而已,在第一次看的時候,需先走一遍的流程,不需要去在意部分細節)

public DownloadRequest setRetryPolicy(RetryPolicy mRetryPolicy) {
        this.mRetryPolicy = mRetryPolicy;
        return this;
    }

這里的實現就是一個簡單的賦值操作而已,既然有賦值方法,那么應該就有對應的取值方法,我Ctrl+F找了一下果然有getRetryPolicy這么一個方法。

public RetryPolicy getRetryPolicy() {
        return mRetryPolicy == null ? new DefaultRetryPolicy() : mRetryPolicy;
    }

從這里看出如果mRetryPolicy 設置了,我們就返回mRetryPolicy ,如果沒有被設置我們就返回默認的。從這里可見在示例代碼中的那句<code>setRetryPolicy(new DefaultRetryPolicy())</code>
是可以不用設置的,因為默認的就是DefaultRetryPolicy。
再來看看setDestinationURI這個方法

 public DownloadRequest setDestinationURI(Uri destinationURI) {
        this.mDestinationURI = destinationURI;
        return this;
    }

和setRetryPolicy方法一樣,就是一個簡單的賦值操作而已。
setStatusListener同理,我就不再做多余的解釋了。

  1. DownloadRequest配置完成之后,我們就使用到了ThinDownloadManager,也還是先進他的構造方法里面去看看。ThinDownloadManager的構造方法就不只一個了,我們就直接來看看他默認的吧
 public ThinDownloadManager() {
        mRequestQueue = new DownloadRequestQueue();
        mRequestQueue.start();
    }

在這里他新建了一個下載的請求隊列DownloadRequestQueue,然后調用了DownloadRequestQueue的start方法。
再來看看ThinDownloadManager的add方法都干了些什么

public int add(DownloadRequest request) throws IllegalArgumentException {
        if(request == null) {
            throw new IllegalArgumentException("DownloadRequest cannot be null");
        }

        return mRequestQueue.add(request);
    }

從源碼中可以看到add方法就是把我們的下載請求添加到DownloadRequestQueue這個請求隊列中去。

  1. 在分析ThinDownloadManager這個類的時候,我們發現DownloadRequestQueue用到的比較多,并且也比較重要,現在我們就定位到DownloadRequestQueue這個類中去。一樣我們還是先來看看其構造方法,一般在使用一個類的時候,我們都需要先初始化他,所以先看構造方法也還是有必要的。
    /**
     * Default constructor.
     */
     public DownloadRequestQueue() {
        initialize(new Handler(Looper.getMainLooper()));
    }

在這里他調用了 initialize方法,所以我們就來看看initialize的實現

private void initialize(Handler callbackHandler) {
        int processors = Runtime.getRuntime().availableProcessors();
        mDownloadDispatchers = new DownloadDispatcher[processors];
        mDelivery = new CallBackDelivery(callbackHandler);
    }

在這里processors 就是java虛擬機可用的處理器個數,最初的時候我也不知道這是啥,最后去查了一下才知道,所以看源碼還是能學到很多東西的,廢話不多說,我們繼續。從代碼中可以看出在initialize中新建了一個processors 大小的DownloadDispatcher數組,以及CallBackDelivery。

在ThinDownloadManager的構造方法中調用了DownloadRequestQueue的start方法,所以這里我們就先來看看start方法

public void start() {
        stop(); // Make sure any currently running dispatchers are stopped.
        // Create download dispatchers (and corresponding threads) up to the pool size.
        for (int i = 0; i < mDownloadDispatchers.length; i++) {
            DownloadDispatcher downloadDispatcher = new DownloadDispatcher(mDownloadQueue, mDelivery);
            mDownloadDispatchers[i] = downloadDispatcher;
            downloadDispatcher.start();
        }
    }

stop可以先不用去看他,直接來看for循環里面都干了些什么,可以看到在for循環里就是對mDownloadDispatchers中的每一個DownloadDispatcher 做了一次初始化操作
隨后就調用了DownloadDispatcher 的start方法。

4.上面這么多的代碼都涉及到了DownloadDispatcher ,下面我們就來看看DownloadDispatcher 的實現吧。
其構造方法如下:

/** Constructor take the dependency (DownloadRequest queue) that all the Dispatcher needs */
    public DownloadDispatcher(BlockingQueue<DownloadRequest> queue,
                              DownloadRequestQueue.CallBackDelivery delivery) {
        mQueue = queue;
        mDelivery = delivery;
    }

在這里就傳了一個下載請求的隊列和CallBackDelivery(CallBackDelivery是用來進行回調的分發用的,是DownloadRequestQueue的一個內部類,后面自己去看源碼就知道了,第一次走流程的時候不需要太在意這個) 進來并給成員變量賦值。在DownloadRequestQueue使用到了downloadDispatcher.start(),所以我就點進start去看看,結果發現怎么進了Thread的start方法了,仔細看了一下DownloadDispatcher 源碼,發現原來DownloadDispatcher 繼承了Thread,也就是說DownloadDispatcher 其實就是一個線程,所以之前調用了start方法就是讓這個線程開始執行,既然繼承了Thread那么肯定得有run方法吧,于是我就定位到了run方法:

 public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        mTimer = new Timer();
        while(true) {
            try {
                mRequest = mQueue.take();
                mRedirectionCount = 0;
                Log.v(TAG, "Download initiated for " + mRequest.getDownloadId());
                updateDownloadState(DownloadManager.STATUS_STARTED);
                executeDownload(mRequest.getUri().toString());
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    if(mRequest != null) {
                        mRequest.finish();
                        updateDownloadFailed(DownloadManager.ERROR_DOWNLOAD_CANCELLED, "Download cancelled");
                        mTimer.cancel();
                    }
                    return;
                }
                continue;               
            }
        }
    }

可見在run方法中有一個while的無限循環,里面的最主要的事的就是從當前的請求隊列中取出一個請求然后調用executeDownload去執行下載。(PS:因為這個隊列是可阻塞的隊列,所以當隊列里沒有任何請求的話,就會阻塞在mQueue.take(),直到隊列中有請求的時候,才會繼續向下執行,這個是Java并發編程相關的知識點)

private void executeDownload(String downloadUrl) {
        URL url;
        try {
            url = new URL(downloadUrl);
        } catch (MalformedURLException e) {
            updateDownloadFailed(DownloadManager.ERROR_MALFORMED_URI,"MalformedURLException: URI passed is malformed.");
            return;
        }

        HttpURLConnection conn = null;

        try {
            conn = (HttpURLConnection) url.openConnection();
            conn.setInstanceFollowRedirects(false);
            conn.setConnectTimeout(mRequest.getRetryPolicy().getCurrentTimeout());
            conn.setReadTimeout(mRequest.getRetryPolicy().getCurrentTimeout());

            HashMap<String, String> customHeaders = mRequest.getCustomHeaders();
            if (customHeaders != null) {
                for (String headerName : customHeaders.keySet()) {
                    conn.addRequestProperty(headerName, customHeaders.get(headerName));
                }
            }

            // Status Connecting is set here before
            // urlConnection is trying to connect to destination.
            updateDownloadState(DownloadManager.STATUS_CONNECTING);
            
            final int responseCode = conn.getResponseCode();
            
            Log.v(TAG, "Response code obtained for downloaded Id "
                + mRequest.getDownloadId()
                + " : httpResponse Code "
                + responseCode);
            
            switch (responseCode) {
                case HTTP_PARTIAL:
                case HTTP_OK:
                    shouldAllowRedirects = false;
                    if (readResponseHeaders(conn) == 1) {
                        transferData(conn);
                    } else {
                        updateDownloadFailed(DownloadManager.ERROR_DOWNLOAD_SIZE_UNKNOWN, "Transfer-Encoding not found as well as can't know size of download, giving up");
                    }
                    return;
                case HTTP_MOVED_PERM:
                case HTTP_MOVED_TEMP:
                case HTTP_SEE_OTHER:
                case HTTP_TEMP_REDIRECT:
                    // Take redirect url and call executeDownload recursively until
                    // MAX_REDIRECT is reached.
                    while (mRedirectionCount++ < MAX_REDIRECTS && shouldAllowRedirects) {
                        Log.v(TAG, "Redirect for downloaded Id "+mRequest.getDownloadId());
                        final String location = conn.getHeaderField("Location");
                        executeDownload(location);
                        continue;
                    }

                    if (mRedirectionCount > MAX_REDIRECTS) {
                        updateDownloadFailed(DownloadManager.ERROR_TOO_MANY_REDIRECTS, "Too many redirects, giving up");
                        return;
                    }
                    break;
                case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
                    updateDownloadFailed(HTTP_REQUESTED_RANGE_NOT_SATISFIABLE, conn.getResponseMessage());
                    break;
                case HTTP_UNAVAILABLE:
                    updateDownloadFailed(HTTP_UNAVAILABLE, conn.getResponseMessage());
                    break;
                case HTTP_INTERNAL_ERROR:
                    updateDownloadFailed(HTTP_INTERNAL_ERROR, conn.getResponseMessage());
                    break;
                default:
                    updateDownloadFailed(DownloadManager.ERROR_UNHANDLED_HTTP_CODE, "Unhandled HTTP response:" + responseCode +" message:" +conn.getResponseMessage());
                    break;
            }
        } catch(SocketTimeoutException e) {
            e.printStackTrace();
            // Retry.
            attemptRetryOnTimeOutException();
        } catch (ConnectTimeoutException e) {
            e.printStackTrace();
            attemptRetryOnTimeOutException();
        } catch(IOException e) {
            e.printStackTrace();
            updateDownloadFailed(DownloadManager.ERROR_HTTP_DATA_ERROR, "Trouble with low-level sockets");
        } finally {
            if (conn != null) {
                conn.disconnect();
            }
        }
    }

學習過Android網絡編程的同學肯定對這里面的代碼很熟悉了,從代碼中可以看到其實ThinDownloadManager的底層就是用HttpURLConnection去實現的,其他的代碼其實就是去更新當前下載的狀態,還有網絡連接超時的重試等等。

到這里整個過程就分析的差不多了,更詳細的實現可以去下載ThinDownloadManager源碼自己詳看。

通過上面的分析大致流可以總結為:先創建一個下載請求DownloadRequest,隨后再新建一個請求隊列DownloadRequestQueue和下載器DownloadDispatcher,下載器DownloadDispatcher隨即開始執行,然后再將請求加入到下載請求隊列中由下載器去從請求隊列中取出下載請求,然后下載。

ThinDownloadManager的源碼的層次結構還是比較清晰的,所以看起來相對也比較容易,看源碼一般都是從我們使用的地方進去,一步一步的跟進,剛開始注重流程,不要太去部分在意細節的實現,流程走通了,然后再來看實現,我相信你會收獲的更多,本文主要介紹了看源碼的其中一種方法,僅供參考,可以讓你看源碼的時候多一種思路,希望能給正在閱讀的你帶來幫助。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,836評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,808評論 25 708
  • 注:本文轉自http://codekk.com/open-source-project-analysis/deta...
    Ten_Minutes閱讀 1,317評論 1 16
  • 20170610 今天是公公的生日,按照慣例,總是一家族的人聚在一起整一天,男同志最終都會跑到麻將桌,女同志干完事...
    春蕓1216閱讀 154評論 4 7
  • 光纖跳線又叫(光纖連接器)是光纖與光纖之間進行可坼卸(活動)連接器件,那么光纖跳線怎樣接才正確,什么樣的光纖跳線接...
    enrilink閱讀 5,454評論 0 0