文档章节

DownloadManager之DownloadThread浅析

仰简
 仰简
发布于 2014/03/17 00:28
字数 2954
阅读 342
收藏 1

第一次写博客,希望给力,我知道会有很多错误,但我会继续努力,望大家共勉。     

DownloadThread是DownloadManager里最核心的类,整个下载的框架中,其他的类都是围绕着这个类在打转。这个类完成的工作大概有:

1、记录当前的状态

2、计算下载速度

3、获取数据流、处理重定向、处理断点续传

4、更新状态、进度等

5、最重要的当然是下载完整个文件了

    下面就带着这几个问题去分析这个类,这样会比较有针对性。首先,介绍一下它里面的内部类的作用,明白内部类的作用,可以更加深入的理解里面的一些思路和设计方法。当然,如果是初看,不要想着一下子全部弄懂,先看懂自己最感兴趣的地方。如果是第一次看,建议从它的run开始看起,因为线程是从run开始的嘛,万事开头难,抓住头了就好办了。说了废话后,还是先看看几个类的作用吧。

    1、State,作用于整个run方法,众所周知,thread就这么一个run是主体,那就是作用于此次下载了。再看其成员变量,也可以很明白的看出,就是记录了一次下载的过程数据。系统这一点做的很好,没有和Downloadnfo

合在一起用,DownloadInfo只用于读取数据,之后就没它啥事了,之后所有与下载有关的各种情况以及数据都被记录了在此

/**
* 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;
}
}


2、InnerState,作用于executeDownload方法,亦即真正执行下载的方法,这也告诉我们run里面还做了其他事儿。从三个属性成员来看,都是记录head的,与http协议相关

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

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


写在后面的话:

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












© 著作权归作者所有

仰简
粉丝 1
博文 14
码字总数 14547
作品 0
广州
高级程序员
私信 提问
加载中

评论(1)

桃园小七
桃园小七
很好 很强大13
Android系统下载管理DownloadManager功能介绍及使用示例

本文主要结合源码介绍 Android系统下载管理DownloadManager的强大功能及使用。 建议直接查看原文Android系统下载管理DownloadManager功能介绍及使用示例。 另推荐下载管理如何进行功能增强和...

Trinea
2013/05/29
6.1K
3
调用android自带的下载功能,进度在消息通知栏上显示

public class CompleteReceiver extends BroadcastReceiver { private DownloadManager downloadManager = null; @Override public void onReceive(Context context, Intent intent) { try {......

jeremy_C
2014/03/11
2.4K
0
如何单元测试一个有依赖的生成功能jar包的代码?

最近在移植android的下载部分代码,顺便也把相关的单元测试代码整了 出来。具体架构是这样的:DownloadProvider 维护着下载服务和数据库操作,生成一个DownloadProvider.apk;DownloadManag...

桃园小七
2014/07/18
649
1
android 默认浏览器 无法下载,此手机不支持此内容(自定义文件or APK文件看过了)

如果你是apk或者android系统可以识别的问题,那么一定是服务器MIME文件类型没有配置正确 APK文件配置如下: <mime-mapping> </mime-mapping> 其他文件请参考MIME配置对照表 下面是android不能...

补全
2014/01/19
4.4K
0
Android downloadmanagr 下载无SD卡时路径设置问题

现象: 使用downloadmanager下载,没有sd卡的情况下未设置路径,默认存储到/data/data/com.android.providers.downloads/cache/目录下,但是文件如果大于100M就不能下载,小于100M则可以。 ...

三哥最帅
2015/02/02
985
0

没有更多内容

加载失败,请刷新页面

加载更多

Java 文件类操作API与IO编程基础知识

阅读目录: https://www.w3cschool.cn/java/java-io-file.html Java 文件 Java 文件 Java 文件操作 Java 输入流 Java 输入流 Java 文件输入流 Java 缓冲输入流 Java 推回输入流 Java 数据输入...

boonya
19分钟前
2
0
SDKMAN推荐一个好

是在大多数基于Unix的系统上管理多个软件开发工具包的并行版本的工具。它提供了一个方便的命令行界面(CLI)和API来安装,切换,删除和列出sdk相关信息。以下是一些特性: By Developers, fo...

hotsmile
44分钟前
8
0
什么是 HDFS

是什么? HDFS 是基于 Java 的分布式文件系统,允许您在 Hadoop 集群中的多个节点上存储大量数据。 起源: 单机容量往往无法存储大量数据,需要跨机器存储。统一管理分布在集群上的文件系统称...

Garphy
47分钟前
5
0
一起来学Java8(四)——复合Lambda

在一起来学Java8(二)——Lambda表达式中我们学习了Lambda表达式的基本用法,现在来了解下复合Lambda。 Lambda表达式的的书写离不开函数式接口,复合Lambda的意思是在使用Lambda表达式实现函...

猿敲月下码
今天
10
0
debian10使用putty配置交换机console口

前言:Linux的推广普及,需要配合解决实际应用方能有成效! 最近强迫自己用linux进行实际工作,过程很痛苦,还好通过网络一一解决,感谢各位无私网友博客的帮助! 系统:debian10 桌面:xfc...

W_Lu
今天
12
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部