DownloadManager之DownloadThread淺析

第一次寫博客,但願給力,我知道會有不少錯誤,但我會繼續努力,望你們共勉。     java

DownloadThread是DownloadManager裏最核心的類,整個下載的框架中,其餘的類都是圍繞着這個類在打轉。這個類完成的工做大概有:數據庫

一、記錄當前的狀態服務器

二、計算下載速度網絡

三、獲取數據流、處理重定向、處理斷點續傳app

四、更新狀態、進度等框架

五、最重要的固然是下載完整個文件了socket

    下面就帶着這幾個問題去分析這個類,這樣會比較有針對性。首先,介紹一下它裏面的內部類的做用,明白內部類的做用,能夠更加深刻的理解裏面的一些思路和設計方法。固然,若是是初看,不要想着一會兒所有弄懂,先看懂本身最感興趣的地方。若是是第一次看,建議從它的run開始看起,由於線程是從run開始的嘛,萬事開頭難,抓住頭了就好辦了。說了廢話後,仍是先看看幾個類的做用吧。ide

    一、State,做用於整個run方法,衆所周知,thread就這麼一個run是主體,那就是做用於這次下載了。再看其成員變量,也能夠很明白的看出,就是記錄了一次下載的過程數據。系統這一點作的很好,沒有和Downloadnfoui

合在一塊兒用,DownloadInfo只用於讀取數據,以後就沒它啥事了,以後全部與下載有關的各類狀況以及數據都被記錄了在此this

/**
* State for the entire run() method.
*/
static class State {
public String mFilename;
public FileOutputStream mStream;
public String mMimeType;
public boolean mCountRetry = false;
public int mRetryAfter = 0;
public int mRedirectCount = 0;
public String mNewUri;
public boolean mGotData = false;
public String mRequestUri;
public long mTotalBytes = -1;
public long mCurrentBytes = 0;
public String mHeaderETag;
public boolean mContinuingDownload = false;
public long mBytesNotified = 0;
public long mTimeLastNotification = 0;
/** Historical bytes/second speed of this download. */
public long mSpeed;
/** Time when current sample started. */
public long mSpeedSampleStart;
/** Bytes transferred since current sample started. */
public long mSpeedSampleBytes;
public State(DownloadInfo info) {
mMimeType = Intent.normalizeMimeType(info.mMimeType);
mRequestUri = info.mUri;
mFilename = info.mFileName;
mTotalBytes = info.mTotalBytes;
mCurrentBytes = info.mCurrentBytes;
}
}


二、InnerState,做用於executeDownload方法,亦即真正執行下載的方法,這也告訴咱們run裏面還作了其餘事兒。從三個屬性成員來看,都是記錄head的,與http協議相關

/**
* State within executeDownload()
*/
private static class InnerState {
public String mHeaderContentLength;
public String mHeaderContentDisposition;
public String mHeaderContentLocation;
}

三、RetryDownload 仍是做用於executeDownload的,可是用於告訴線程這次下載要當即從新開始。固然這裏的從新開始,不是線程立刻給你從新跑一次,而是將當前的DownloadInfo入庫,從新進入下載隊列。RetryDownload繼承Throwable,只是爲了拋出一個異常而已。其實線程裏的整個下載邏輯,亦即線程的生死,都是經過異常來控制的。這一點不得不認可其高明之處,至少對於咱們這樣的小菜來講,想不出這樣的設計出來。
/**
* Raised from methods called by executeDownload() to indicate that the download should be
* retried immediately.
*/
private class RetryDownload extends Throwable {}  

在粗略的看明白這幾個內部類的做用後,接下來開始下載的流程吧。那麼怎麼看呢,很簡單,前面說過,Thread的嘛,確定是從run開始的。咱們看看它的代碼如何:


 

/**
* Executes the download in a separate thread
*/
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    try {
        runInternal();
        } finally {
    DownloadHandler.getInstance().dequeueDownload(mInfo.mId);
    }    
}


這個方法的實現是否是一目瞭然呢。首先設置線程的優先級爲後臺運行,而後執行runInternal方法,這個方法怎麼實現的先無論,但必定是實現了下載了。完成後,將本身從下載隊列裏移除掉。下載的起始與結束流程是用try...finally控制的,這裏充分運用了java語言的這種異常特性。接下來也會看到。貫穿整個下載的線程生死就是由Java的這種異常機制實現的。

進一步看看runInternal()的實現。如下經過添加註釋來講明其整個流程,代碼以下:

  private void runInternal() {
        // 第一步,判斷當前要下載的DownloadInfo的狀態是否爲下載成功,若是成功,則直接返回,線程結束
        if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mInfo.mId)
                == Downloads.Impl.STATUS_SUCCESS) {
           return;
        }

        // 第二步,作一些初始化的工做,用State創建一個當前下載整個狀態
        State state = new State(mInfo);
        // 建立一個Http網絡客戶端
        AndroidHttpClient client = null;
        // 獲取電源管理,這裏主要用於控制休眠。在下載過程當中,控制系統不讓其休眠,只有等待下載完成,或者發生了網絡不通等下載失敗的異常後,才恢復系統休眠
        PowerManager.WakeLock wakeLock = null;
        // 初始狀態爲未知狀態
        int finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
        // 下載失敗後的錯誤描述
        String errorMsg = null;

        //獲取網絡策略管理與電源管理的服務
        final NetworkPolicyManager netPolicy = NetworkPolicyManager.from(mContext);
        final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);

        try {
        //第三步開始進入下載控制了,第一個控制的就休眠了
            wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
            wakeLock.acquire();

            // 註冊監聽
            netPolicy.registerListener(mPolicyListener);
            // 實化化Http客戶端端
            client = AndroidHttpClient.newInstance(userAgent(), mContext);

           // 流量統計方面的吧
            TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_DOWNLOAD);
            TrafficStats.setThreadStatsUid(mInfo.mUid);

            // 第四步,正式進入下載
            boolean finished = false;
            while(!finished) {
                //設置一個代理
                ConnRouteParams.setDefaultProxy(client.getParams(),
                        Proxy.getPreferredHttpHost(mContext, state.mRequestUri));
                // 一個很關鍵的點,下載是用的Http的Get方法構造的請求。
                HttpGet request = new HttpGet(state.mRequestUri);
                try {
                    //用前面準備好的參數,執行下載
                    executeDownload(state, client, request);
                    // 完成後,標記爲true,下載完了,就退出循環
                    finished = true;
                } catch (RetryDownload exc) {
                    // fall through
                } finally {
                    // 最後記得關閉掉鏈接
                    request.abort();
                    request = null;
                }
            }

            // 處理下載後的文件
            finalizeDestinationFile(state);
            // 標記狀態爲成功
            finalStatus = Downloads.Impl.STATUS_SUCCESS;
        } catch (StopRequestException error) {
            
            // 記錄下載緣由。StopRequestException是DownloadManager自已定義的,它由不一樣狀況下的失敗後拋出來的,後面會分別講到的
            errorMsg = error.getMessage();
            String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
            finalStatus = error.mFinalStatus;
            
        } catch (Throwable ex) { //sometimes the socket code throws unchecked exceptions
            errorMsg = ex.getMessage();
            String msg = "Exception for id " + mInfo.mId + ": " + errorMsg;
            Log.w(Constants.TAG, msg, ex);
            finalStatus = Downloads.Impl.STATUS_UNKNOWN_ERROR;
            // falls through to the code that reports an error
        } finally {
        
            //結束下載工做,關閉鏈接,發送下載完成的通知,釋放休眠鎖
            TrafficStats.clearThreadStatsTag();
            TrafficStats.clearThreadStatsUid();

            if (client != null) {
                client.close();
                client = null;
            }
            cleanupDestination(state, finalStatus);
            notifyDownloadCompleted(finalStatus, state.mCountRetry, state.mRetryAfter,
                                    state.mGotData, state.mFilename,
                                    state.mNewUri, state.mMimeType, errorMsg);

            netPolicy.unregisterListener(mPolicyListener);

            if (wakeLock != null) {
                wakeLock.release();
                wakeLock = null;
            }
        }
        mStorageManager.incrementNumDownloadsSoFar();
    }

代碼應該也不算太多,下面簡單理一理這個流程。首先判斷當前狀態,而後再初始下化載的信息,而後再構造出Http的客戶端 ,而後就執行下載了,下載完成後,還要作一些收尾的工做,最後結束掉。再次看到這個,就又會想到以前提的,整個i流程是由Java的異常機制來控制的。

上面的流程也是極其簡單的,可是它有一個關鍵的調用,那就是executeDownload。那麼先來看看它的實現吧。

 /**
     * Fully execute a single download request - setup and send the request, handle the response,
     * and transfer the data to the destination file.
     */
    private void executeDownload(State state, AndroidHttpClient client, HttpGet request)
            throws StopRequestException, RetryDownload {
        //主要是用於記錄Http協議的Header方面的狀態
        InnerState innerState = new InnerState();
        //讀取數據流時的Buffer
        byte data[] = new byte[Constants.BUFFER_SIZE];
        //設置目標文件,也就是你要保存的文件,在這裏除了肯定文件的保存路徑和文件名外,還有一個更爲重要的,就是判斷出是第一次下載仍是繼續下載
        setupDestinationFile(state, innerState);
        //添加用戶的HTTP協議的請求首部,其中包括了對斷點續傳的設置
        addRequestHeaders(state, request);

        //經過當前大小與文件總大小來判斷是否已經下載完成
        // skip when already finished; remove after fixing race in 5217390
        if (state.mCurrentBytes == state.mTotalBytes) {
            Log.i(Constants.TAG, "Skipping initiating request for download " +
                  mInfo.mId + "; already completed");
            return;
        }

        // check just before sending the request to avoid using an invalid connection at all
        checkConnectivity();
        // 向服務器發起請求
        HttpResponse response = sendRequest(state, client, request);
        //處理請求的異常
        handleExceptionalStatus(state, innerState, response);

        if (Constants.LOGV) {
            Log.v(Constants.TAG, "received response for " + mInfo.mUri);
        }
        //處理響應首部,這裏很重要
        processResponseHeaders(state, innerState, response);
        // 如下兩個方法就是完成數據流的讀取和寫入了
        InputStream entityStream = openResponseEntity(state, response);
        transferData(state, innerState, data, entityStream);
    }

這個真正執行下載的過程也十分的清楚,整個方法裏面都是方法級的調用,讓人看了有一目瞭然的感受。這裏最重要的就是

addRequestHeaders(state, request);和processResponseHeaders(state, innerState, response);


這兩個方法和HTTP協議自己緊密相關,也是實現斷點續傳的關鍵。下面不妨來反着看,假設不支持斷點續,就當作第一次下載來看。這時能夠直接看

processResponseHeaders(state, innerState, response);

的實現了。

 /**
     * Read HTTP response headers and take appropriate action, including setting up the destination
     * file and updating the database.
     */
    private void processResponseHeaders(State state, InnerState innerState, HttpResponse response)
            throws StopRequestException {
        if (state.mContinuingDownload) {
            // ignore response headers on resume requests
            return;
        }

        // 讀取響應首部
        readResponseHeaders(state, innerState, response);
        if (DownloadDrmHelper.isDrmConvertNeeded(state.mMimeType)) {
            mDrmConvertSession = DrmConvertSession.open(mContext, state.mMimeType);
            if (mDrmConvertSession == null) {
                throw new StopRequestException(Downloads.Impl.STATUS_NOT_ACCEPTABLE, "Mimetype "
                        + state.mMimeType + " can not be converted.");
            }
        }

        state.mFilename = Helpers.generateSaveFile(
                mContext,
                mInfo.mUri,
                mInfo.mHint,
                innerState.mHeaderContentDisposition,
                innerState.mHeaderContentLocation,
                state.mMimeType,
                mInfo.mDestination,
                (innerState.mHeaderContentLength != null) ?
                        Long.parseLong(innerState.mHeaderContentLength) : 0,
                mInfo.mIsPublicApi, mStorageManager);
        try {
            state.mStream = new FileOutputStream(state.mFilename);
        } catch (FileNotFoundException exc) {
            throw new StopRequestException(Downloads.Impl.STATUS_FILE_ERROR,
                    "while opening destination file: " + exc.toString(), exc);
        }
        if (Constants.LOGV) {
            Log.v(Constants.TAG, "writing " + mInfo.mUri + " to " + state.mFilename);
        }

        //更新響應首部到數據庫裏
        updateDatabaseFromHeaders(state, innerState);
        // check connectivity again now that we know the total size
        checkConnectivity();
    }


唉呀,還有一層,那就看看

readResponseHeaders(state, innerState, response);

的實現吧。

 /**
     * Read headers from the HTTP response and store them into local state.
     */
    private void readResponseHeaders(State state, InnerState innerState, HttpResponse response)
            throws StopRequestException {
        Header header = response.getFirstHeader("Content-Disposition");
        if (header != null) {
            innerState.mHeaderContentDisposition = header.getValue();
        }
        header = response.getFirstHeader("Content-Location");
        if (header != null) {
            innerState.mHeaderContentLocation = header.getValue();
        }
        if (state.mMimeType == null) {
            header = response.getFirstHeader("Content-Type");
            if (header != null) {
                state.mMimeType = Intent.normalizeMimeType(header.getValue());
            }
        }
        //獲取ETag的值
        header = response.getFirstHeader("ETag");
        if (header != null) {
            state.mHeaderETag = header.getValue();
        }
        String headerTransferEncoding = null;
        header = response.getFirstHeader("Transfer-Encoding");
        if (header != null) {
            headerTransferEncoding = header.getValue();
        }
        if (headerTransferEncoding == null) {
            //獲取文件大小
            header = response.getFirstHeader("Content-Length");
            if (header != null) {
                innerState.mHeaderContentLength = header.getValue();
                state.mTotalBytes = mInfo.mTotalBytes =
                        Long.parseLong(innerState.mHeaderContentLength);
            }
        } else {
            // Ignore content-length with transfer-encoding - 2616 4.4 3
            if (Constants.LOGVV) {
                Log.v(Constants.TAG,
                        "ignoring content-length because of xfer-encoding");
            }
        }
        if (Constants.LOGVV) {
            Log.v(Constants.TAG, "Content-Disposition: " +
                    innerState.mHeaderContentDisposition);
            Log.v(Constants.TAG, "Content-Length: " + innerState.mHeaderContentLength);
            Log.v(Constants.TAG, "Content-Location: " + innerState.mHeaderContentLocation);
            Log.v(Constants.TAG, "Content-Type: " + state.mMimeType);
            Log.v(Constants.TAG, "ETag: " + state.mHeaderETag);
            Log.v(Constants.TAG, "Transfer-Encoding: " + headerTransferEncoding);
        }

        boolean noSizeInfo = innerState.mHeaderContentLength == null
                && (headerTransferEncoding == null
                    || !headerTransferEncoding.equalsIgnoreCase("chunked"));
        if (!mInfo.mNoIntegrity && noSizeInfo) {
            throw new StopRequestException(Downloads.Impl.STATUS_HTTP_DATA_ERROR,
                    "can't know size of download, giving up");
        }
    }

真正讀取響應首部的地方。作過Http應用的人或者稍徽瞭然Http協議的人,對上面的代碼確定是似曾相識的。關於上面的各個字段,能夠經過網上找到相應的資料,不必一個一個的解釋。重點講講ETAG和Content-Length。其中,ETAG值是用於向服務器發送一個命令,問它所下載的資源是否已經有改動了。若是沒有改動則正常返回200,若是有改動則返回304,意味着下載將失敗.想一想也是這個道理,資源都發生改動了,後面的下載就沒有意義了。Content-Length ,則是用於獲取文件的大小的。這能夠幫助咱們計算下載的進度,以及判斷下載是否完成等工做。

再來看看

addRequestHeaders(state, request);

的實現吧。

    /**
     * Add custom headers for this download to the HTTP request.
     */
    private void addRequestHeaders(State state, HttpGet request) {
    
       // 添加用戶設定的請求首部
        for (Pair<String, String> header : mInfo.getHeaders()) {
            request.addHeader(header.first, header.second);
        }
        //判斷是不是繼續下載,也就是斷點的意思啦 
        if (state.mContinuingDownload) {
           // 發送If-Match,而其值爲ETAG
            if (state.mHeaderETag != null) {
                request.addHeader("If-Match", state.mHeaderETag);
            }
            //Range請求首部,向服務器請求了從當前字節所指的位置開始,直到文件尾,也就是Content-lenght所返回的大小
            request.addHeader("Range", "bytes=" + state.mCurrentBytes + "-");
            if (Constants.LOGV) {
                Log.i(Constants.TAG, "Adding Range header: " +
                        "bytes=" + state.mCurrentBytes + "-");
                Log.i(Constants.TAG, "  totalBytes = " + state.mTotalBytes);
            }
        }
    }

斷點續傳是否是很簡單呢。第一步,向服務器發送了If-match命令,比較了ETAG的值,至關因而一次校驗了。不過有的服務器爲了加速,並不返回ETAG值。我在開發過程當中,就遇到過這麼一檔子事兒。而後發送Rang命令,其中bytes=XXX-,就表示從XXX開始下載。關於其具體含義能夠查找相關的資料。


寫在後面的話:

第一次寫博客,我也知道寫的很爛。語言啊,組織啊,各類很差,還貼了那麼多的代碼。但我想這樣會加深對其更深的理解吧。

相關文章
相關標籤/搜索