第一次写博客,希望给力,我知道会有很多错误,但我会继续努力,望大家共勉。
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开始下载。关于其具体含义可以查找相关的资料。
写在后面的话:
第一次写博客,我也知道写的很烂。语言啊,组织啊,各种不好,还贴了那么多的代码。但我想这样会加深对其更深的理解吧。