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开始下载。关于其具体含义能够查找相关的资料。


写在后面的话:

第一次写博客,我也知道写的很烂。语言啊,组织啊,各类很差,还贴了那么多的代码。但我想这样会加深对其更深的理解吧。

相关文章
相关标签/搜索