• 网络断开后重连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列表中了。
  • 相关阅读:
    设计模式复习笔记08
    Docker Dockerfile 指令详解与实战案例
    Docker数据管理与挂载管理
    Docker简介与安装
    Xshell如何配置并远程连接Linux服务器详解
    如何VMware创建Linux虚拟机并设置虚拟机网络
    自动化运维工具Ansible之LNMP实践环境部署
    自动化运维工具Ansible之Roles角色详解
    自动化运维工具Ansible之Tests测验详解
    Ansible Jinja2 模板使用
  • 原文地址:https://www.cnblogs.com/yangwubo/p/5102770.html
Copyright © 2020-2023  润新知