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