第一次寫博客,但願給力,我知道會有不少錯誤,但我會繼續努力,望你們共勉。 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開始下載。關於其具體含義能夠查找相關的資料。
寫在後面的話:
第一次寫博客,我也知道寫的很爛。語言啊,組織啊,各類很差,還貼了那麼多的代碼。但我想這樣會加深對其更深的理解吧。