網絡斷開后重連downloadProvider繼續下載問題調試分析


最近在安卓4.4上遇到一個斷開wifi后重新連接wifi, downloadProvider繼續下載文件失敗的問題。於是開始了解下載管理模塊的斷點續載功能:
 
 
1、首先,分析android log, 當將網絡斷開之后,下載會中止,出現如下信息:
W/DownloadManager(29473): Aborting request for download 5: Failed reading response: java.net.SocketException: recvfrom failed: ETIMEDOUT (Connection timed out)
I/DownloadManager(29473): Download 5 finished with status WAITING_FOR_NETWORK
 
在代碼中搜索Failed reading response, 發現是在下載數據中不斷讀取網絡數據流時拋出的異常:
    /**
     * Transfer as much data as possible from the HTTP response to the
     * destination file.
     */
    private void transferData(State state, InputStream in, OutputStream out)
            throws StopRequestException {
        final byte data[] = new byte[Constants.BUFFER_SIZE];
         for (;;) {
            int bytesRead =  readFromResponse(state, data, in);
 
            if (bytesRead == -1) { // success, end of stream already reached
                handleEndOfStream(state);
                return;
            }
 
            state.mGotData = true;
            writeDataToDestination(state, data, bytesRead, out);
            state.mCurrentBytes += bytesRead;
            reportProgress(state);
            checkPausedOrCanceled(state);
        }
 
 
在循環中不停讀取網絡那邊的響應,當網絡斷開后,InputStream的讀接口應該就會拋出異常,代碼中進行捕捉,並且判斷之后是否能夠斷點續載,然后拋出相應信息:
 
 /**
     * Read some data from the HTTP response stream, handling I/O errors.
     * @param data buffer to use to read data
     * @param entityStream stream for reading the HTTP response entity
     * @return the number of bytes actually read or -1 if the end of the stream has been reached
     */
    private int readFromResponse(State state, byte[] data, InputStream entityStream)
            throws StopRequestException {
        try {
            return entityStream.read(data);
        } catch (IOException ex) {
            ContentValues values = new ContentValues();
            values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, state.mCurrentBytes);
            mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), values, null, null);
 
            if (cannotResume(state)) {
                throw new StopRequestException(STATUS_CANNOT_RESUME,
                        "Failed reading response: " + ex + "; unable to resume", ex);
            } else {
                throw new StopRequestException( STATUS_HTTP_DATA_ERROR,
                        " Failed reading response: " + ex, ex);
            }
        }
    }
 
這里的判斷是否能夠續載,有很多條件, 主要是兩個方面,下載字節數是否大於0 或者 是否DRM 下載需要轉換:
D/DownloadManager( 9658): state.mCurrentBytes=5257536 state.mHeaderETag=69b8155f8ae29636cec71afb21637c92 mInfo.mNoIntegrity=false state.mMimeType=application/vnd.android.package-archive
 
 
導出數據庫,查看此時下載管理該文件狀態:
這個狀態 status = 195 是怎么來的呢?
 
我們可以繼續跟蹤代碼,前面說了,當網絡斷開后,代碼開始拋出異常StopRequestException, 並且帶有錯誤碼,仔細閱讀代碼,這個異常是各個方法,
一層一層網上拋出,最后達到下載管理線程 DownloadThread 類中的 run中, 它在catch這個異常后,也會打印出log信息,並且增加了處理:
catch (StopRequestException error) {
            // remove the cause before printing, in case it contains PII
            errorMsg = error.getMessage();
             String msg = "Aborting request for download " + mInfo.mId + ": " + errorMsg;
            Log.w(Constants.TAG, msg);
            if (Constants.LOGV) {
                Log.w(Constants.TAG, msg, error);
            }
            finalStatus = error.getFinalStatus();
 
 
從代碼中可以看出其增加了下載文件在數據庫中存放的Id信息,然后在加上出錯新消息,也就我們最終看到的log:
W/DownloadManager(29473):  Aborting request for download 5: Failed reading response: java.net.SocketException: recvfrom failed: ETIMEDOUT (Connection timed out)
 
在輸出完信息之后,其會對錯誤碼判斷進行處理,想斷網這種問題,會有個繼續嘗試,然后確定最終的錯誤碼。最初拋出異常的錯誤碼是 STATUS_HTTP_DATA_ERROR , 即495.
 
W/DownloadManager(11584): Aborting request for download 5: Failed reading response: java.net.SocketException: recvfrom failed: ETIMEDOUT (Connection timed out)
D/DownloadManager(11584): -----finalStatus=495
 
最后經過代碼轉換:
 // Some errors should be retryable, unless we fail too many times.
            if (isStatusRetryable(finalStatus)) {
                if (state.mGotData) {
                    numFailed = 1;
                } else {
                    numFailed += 1;
                }
 
                if (numFailed < Constants.MAX_RETRIES) {
                    final NetworkInfo info = mSystemFacade.getActiveNetworkInfo(mInfo.mUid);
                    if (info != null && info.getType() == state.mNetworkType
                            && info.isConnected()) {
                        // Underlying network is still intact, use normal backoff
                        finalStatus = STATUS_WAITING_TO_RETRY;
                    } else {
                        // Network changed, retry on any next available
                        finalStatus =  STATUS_WAITING_FOR_NETWORK;
                    }
                }
            }
 
會變成  STATUS_WAITING_FOR_NETWORK 195,然后在finally中處理,通過通知方法notifyDownloadCompleted將狀態值存儲到
數據庫中, 即我們最終看到了status = 195
 
 
 
之所以需要轉換,我覺得是最下層拋出來的錯誤碼是 http網絡那邊定義的, 而我們儲存到數據庫中的狀態值是給下載管理模塊用的, 兩者的
定義和使用詳細程度是有區別的,因為管理方式不同。
 
 
 
 
 
 
2、網絡重連后的log信息分析:
 
I/DownloadManager(11584):  Download 5 starting
state.mRequestUri= http://w.gdown.baidu.com/data/wisegame/8ae29636cec71afb/17173shouyou_3300.apk?f=m1101
I/DownloadManager(11584): have run thread before for id: 5, and state.mFilename: /storage/emulated/0/Download/17173shouyou_3300.apk
I/DownloadManager(11584): resuming download for id: 5, and state.mFilename: /storage/emulated/0/Download/17173shouyou_3300.apk
I/DownloadManager(11584): resuming download for id: 5, and starting with file of length: 5367618
I/DownloadManager(11584): resuming download for id: 5, state.mCurrentBytes: 5367618, and setting mContinuingDownload to true:
D/DownloadManager(11584): userAgent: AndroidDownloadManager/4.4.2 (Linux; U; Android 4.4.2; A11w Build/KOT49H)
D/DownloadManager(11584): mMimeType =application/vnd.android.package-archive, mIsPublicApi=true
I/DownloadManager(11584):  Download 5 finished with status SUCCESS
D/DownloadManager(11584): drm:requestScanFile:info.mFileName= /storage/emulated/0/Download/17173shouyou_3300.apk mimeType= application/vnd.android.package-archive
 
DownloadReceiver中會監聽網絡的變化,當網絡重新連接后,其會重新啟動下載管理服務:
 
 else if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
            final ConnectivityManager connManager = (ConnectivityManager) context
                    .getSystemService(Context.CONNECTIVITY_SERVICE);
            final NetworkInfo info = connManager.getActiveNetworkInfo();
            if (info != null && info.isConnected()) {
                 startService(context);
            }
 
這個時候在執行下載executeDownload時,檢測是否已經下載過該文件就起到作用了,也就是resuming download那一段的log信息,會地區文件路徑,已經下載大小等等信息。
 
不過此時需要注意從網絡端獲取的返回碼的情況,正常情況下不是 HTTP_OK 200了:
    final int responseCode = conn.getResponseCode();
    Log.i(Constants.TAG, "-----[executeDownload] responseCode="+responseCode);
 
   I/DownloadManager(11584): -----[executeDownload] responseCode=206
通過log信息我們可以看到此時返回的是 HTTP_PARTIAL  206 , 對比兩個case:
 
                    case HTTP_OK:
                        if (state.mContinuingDownload) {
                            throw new StopRequestException(
                                    STATUS_CANNOT_RESUME, "Expected partial, but received OK");
                        }
                         processResponseHeaders(state, conn);
                        transferData(state, conn);
                        return;
 
                    case HTTP_PARTIAL:
                        if (!state.mContinuingDownload) {
                            throw new StopRequestException(
                                    STATUS_CANNOT_RESUME, "Expected OK, but received partial");
                        }
                        transferData(state, conn);
                        return;
 
可以看出后者不再需要重新處理頭部信息,只需要直接傳輸數據就可以了。
以上的log信息是斷開網絡后,連接網絡成功下載文件的情況。
 
 
 
 
 
3、重新打開wifi后下載失敗的情況:
 
I/DownloadManager(11584): Download 6 starting
state.mRequestUri= http://w.gdown.baidu.com/data/wisegame/32ef8e3c0291add2/baidunuomi_153.apk?f=m1101
I/DownloadManager(11584): have run thread before for id: 6, and state.mFilename: /storage/emulated/0/Download/baidunuomi_153.apk
I/DownloadManager(11584): resuming download for id: 6, and state.mFilename: /storage/emulated/0/Download/baidunuomi_153.apk
I/DownloadManager(11584): resuming download for id: 6, and starting with file of length: 3128774
I/DownloadManager(11584): resuming download for id: 6, state.mCurrentBytes: 3128774, and setting mContinuingDownload to true:
D/DownloadManager(11584): userAgent: AndroidDownloadManager/4.4.2 (Linux; U; Android 4.4.2; A11w Build/KOT49H)
I/DownloadManager(11584): -----[executeDownload] responseCode=200
W/DownloadManager(11584): Aborting request for download 6: Expected partial, but received OK
D/DownloadManager(11584): mMimeType =application/vnd.android.package-archive, mIsPublicApi=true
I/DownloadManager(11584): Download 6 finished with status CANNOT_RESUME
 
從關鍵信息 Aborting request for download 6: Expected partial, but received OK
可以看出, 在重新啟動下載后,從網絡那邊的返回碼跟正常下載已經不同了,正常情況下回返回 206, 而這里的信息返回碼是200,然后代碼拋出異常,
即從信息也可以看出, 代碼期望得到返回值未partial, 但是實際得到的卻是 OK。
 
在網上查詢了一下HTTP的返回碼信息:
 

HTTP協議狀態碼表示的意思主要分為五類 ,大體是 :  
~~~~~~~~~~~~~~~~~~~~~~~~~~~~  
1××   保留   
2××   表示請求成功地接收   
3××   為完成請求客戶需進一步細化請求   
4××   客戶錯誤   
5××   服務器錯誤   

 

 

100 Continue
指示客戶端應該繼續請求。回送用於通知客戶端此次請求已經收到,並且沒有被服務器拒絕。
客戶端應該繼續發送剩下的請求數據或者請求已經完成,或者忽略回送數據。服務器必須發送
最后的回送在請求之后。

101 Switching Protocols 
服務器依照客服端請求,通過Upgrade頭信息,改變當前連接的應用協議。服務器將根據Upgrade頭立刻改變協議
在101回送以空行結束的時候。

 

Successful 
=================================
200 OK 
指示客服端的請求已經成功收到,解析,接受。

201 Created 
請求已經完成並一個新的返回資源被創建。被創建的資源可能是一個URI資源,通常URI資源在Location頭指定。回送應該包含一個實體數據
並且包含資源特性以及location通過用戶或者用戶代理來選擇合適的方法。實體數據格式通過煤體類型來指定即content-type頭。最開始服務 器
必須創建指定的資源在返回201狀態碼之前。如果行為沒有被立刻執行,服務器應該返回202。

202 Accepted 
請求已經被接受用來處理。但是處理並沒有完成。請求可能或者根本沒有遵照執行,因為處理實際執行過程中可能被拒絕。

203 Non-Authoritative Information

204 No Content 
服務器已經接受請求並且沒必要返回實體數據,可能需要返回更新信息。回送可能包含新的或更新信息由entity-headers呈現。

205 Reset Content 
服務器已經接受請求並且用戶代理應該重新設置文檔視圖。

206 Partial Content 
服務器已經接受請求GET請求資源的部分。請求必須包含一個Range頭信息以指示獲取范圍可能必須包含If-Range頭信息以成立請求條件。

 

Redirection 
==================================
300 Multiple Choices
請求資源符合任何一個呈現方式。

301 Moved Permanently 
請求的資源已經被賦予一個新的URI。

302 Found 
通過不同的URI請求資源的臨時文件。
303 See Other

304 Not Modified 
如果客服端已經完成一個有條件的請求並且請求是允許的,但是這個文檔並沒有改變,服務器應該返回304狀態碼。304
狀態碼一定不能包含信息主體,從而通常通過一個頭字段后的第一個空行結束。

305 Use Proxy
請求的資源必須通過代理(由Location字段指定)來訪問。Location資源給出了代理的URI。

306 Unused

307 Temporary Redirect

 

Client Error 
=====================
400 Bad Request 
因為錯誤的語法導致服務器無法理解請求信息。

401 Unauthorized 
如果請求需要用戶驗證。回送應該包含一個WWW-Authenticate頭字段用來指明請求資源的權限。

402 Payment Required 
保留狀態碼

403 Forbidden 
服務器接受請求,但是被拒絕處理。

404 Not Found 
服務器已經找到任何匹配Request-URI的資源。

405 Menthod Not Allowed 
Request-Line 請求的方法不被允許通過指定的URI。

406 Not Acceptable

407 Proxy Authentication Required

408 Reqeust Timeout 
客服端沒有提交任何請求在服務器等待處理時間內。

409 Conflict

410 Gone

411 Length Required 
服務器拒絕接受請求在沒有定義Content-Length字段的情況下。

412 Precondition Failed

413 Request Entity Too Large 
服務器拒絕處理請求因為請求數據超過服務器能夠處理的范圍。服務器可能關閉當前連接來阻止客服端繼續請求。

414 Request-URI Too Long 
服務器拒絕服務當前請求因為URI的長度超過了服務器的解析范圍。

415 Unsupported Media Type 
服務器拒絕服務當前請求因為請求數據格式並不被請求的資源支持。

416 Request Range Not Satisfialbe

417 Expectation Failed

 

Server Error 
===================================
500 Internal Server Error 
服務器遭遇異常阻止了當前請求的執行

501 Not Implemented 
服務器沒有相應的執行動作來完成當前請求。

502 Bad Gateway

503 Service Unavailable 
因為臨時文件超載導致服務器不能處理當前請求。

504 Gateway Timeout

505 Http Version Not Supported

 
 
 
從如上信息來看猜想 206 是之前已經請求過了,接下來請求余下部分的內容,下載管理發送出去的請求信息應該和正常下載時是一致的。
仔細測試發現,從設置直接打開wifi后,並沒有真正連接上,還是需要登錄賬號和輸入密碼,這個可能和路由器的設置有關系。
 
代碼中對此類異常的處理同樣如上所述,上層捕獲,然后判斷處理,最終將狀態值存儲到數據庫:
 
                            throw new StopRequestException(
                                     STATUS_CANNOT_RESUME, "Expected partial, but received OK");
 
 
 
 
此問題應該不算是downloadProvider的問題,因為是沒有連接上網絡,所以獲取的返回值出問題了,導致最終下載失敗,因為下載管理中已經定義了這種情況
下是不能夠續載的。
 
 
 
 
 
4、另外再分析一下就是下載中途將網絡關掉后, 通知欄中的下載進度顯示也會被一起清掃掉,之前項目經理認為此處有問題,應該保留成下載暫停狀態。
我之前對下載管理的特性也不了解,只好繼續看代碼。
 
通知欄的更新主要是通過mNotifier來進行的,即類DownloadNotifier中的處理, 在下載服務的updateLocked中,通過獲取數據庫中目前的下載字節信息
來更新通知欄的進度:
     // Update notifications visible to user
        mNotifier. updateWith(mDownloads.values());
 
 
    private static final int TYPE_ACTIVE = 1;
    private static final int TYPE_WAITING = 2;
    private static final int TYPE_COMPLETE = 3;
   通知欄信息分為如上三類, 正在下載, 等待下載,下載完成。
 
每次更新通知欄,都會將數據庫中的每個下載文件的信息來構建一個tag:
    /**
     * Build tag used for collapsing several {@link DownloadInfo} into a single
     * {@link Notification}.
     */
    private static String buildNotificationTag(DownloadInfo info) {
        if (info.mStatus == Downloads.Impl.STATUS_QUEUED_FOR_WIFI) {
            return TYPE_WAITING + ":" + info.mPackage;
        } else if (isActiveAndVisible(info)) {
            return TYPE_ACTIVE + ":" + info.mPackage;
        } else if (isCompleteAndVisible(info)) {
            // Complete downloads always have unique notifs
            return TYPE_COMPLETE + ":" + info.mId;
        } else {
            return null;
        }
    }
 
 
再構建的過程數據庫有一個字段的信息也會被用到,就是Visibility屬性:
 
 
在我進行的調試中只出現了type為 TYPE_ACTIVE 和 TYPE_COMPLETE 兩種情況。
 
在更新通知欄的最后處理中,有一段代碼用來清理掉一些通知信息,其中就包括這種下載中斷的類型的:
 
        // Remove stale tags that weren't renewed
        final Iterator<String> it = mActiveNotifs.keySet().iterator();
        while (it.hasNext()) {
            final String tag = it.next();
            if ( !clustered.containsKey(tag)) {  //沒有包含在tag列表中的,需要清除
                mNotifManager.cancel(tag, 0);
                it.remove();
            }
        }
 
 
 
log信息, 構建好的tag形式就是type: id, 當然這是已經下載完成的:
D/DownloadManager(32155): =====tag=3:15
D/DownloadManager(32155): =====tag=3:14
D/DownloadManager(32155): =====tag=3:13
D/DownloadManager(32155): =====tag=3:12
D/DownloadManager(32155): =====tag=3:6
D/DownloadManager(32155): =====tag=3:19
D/DownloadManager(32155): =====tag=3:18
D/DownloadManager(32155): =====tag=3:17
D/DownloadManager(32155): =====tag=3:16
D/DownloadManager(32155): =====tag=3:20
D/DownloadManager(32155): =====tag=3:11
D/DownloadManager(32155): =====tag=3:10
D/DownloadManager(32155): =====tag=3:21
D/DownloadManager(32155): =====tag=1:com.android.browser
D/DownloadManager(32155): =====remove tag=1:com.android.browser
 
還有就是那種執行過一鍵清理后,那種更新信息也不會再顯示在通知欄中了,因為其tag為null, 也已經不包含在tag列表中了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM