Android系统中有提供一个下载工具给第三方开发者使用,开发者只需要简单的几个步骤就可以完成下载文件的功能。那就是DownloadManager,为了更好地使用这个工具,得先理解它的工作原理、工作流程。下面就使用DownloadManager进行文件下载的流程进行源码的分析。
下面是整个工作流程的一个时序图:
从上面的时序图我们可以大致了解整个流程。从添加请求,到最后开启下载线程进行文件的下载。为了更好的理解这个下载工具的思想,下面会从源码上对一些重要的函数进行分析。
**********************************************************************************************************************************************************************
一开始,调用DownloadManager的enqueue()方法进行下载请求的添加,然后就会调用DownloadProvider的insert()方法进行数据库的数据的插入,insert()方法不单单是把数据插入到数据库,还会启动DownloadService这个服务。
代码如下:
@Override
public Uri insert(final Uri uri, final ContentValues values) {
checkInsertPermissions(values);
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
// note we disallow inserting into ALL_DOWNLOADS
if (pckg != null && (clazz != null || isPublicApi)) {
int uid = Binder.getCallingUid();
try {
if (uid == 0 || mSystemFacade.userOwnsPackage(uid, pckg)) {
filteredValues.put(Downloads.COLUMN_NOTIFICATION_PACKAGE,
pckg);
if (clazz != null) {
filteredValues.put(Downloads.COLUMN_NOTIFICATION_CLASS,
clazz);
}
}
} catch (PackageManager.NameNotFoundException ex) {
/* ignored for now */
}
}
copyString(Downloads.COLUMN_NOTIFICATION_EXTRAS, values, filteredValues);
copyString(Downloads.COLUMN_COOKIE_DATA, values, filteredValues);
copyString(Downloads.COLUMN_USER_AGENT, values, filteredValues);
copyString(Downloads.COLUMN_REFERER, values, filteredValues);
if (getContext().checkCallingPermission(
Downloads.PERMISSION_ACCESS_ADVANCED) == PackageManager.PERMISSION_GRANTED) {
copyInteger(Downloads.COLUMN_OTHER_UID, values, filteredValues);
}
filteredValues.put(Constants.UID, Binder.getCallingUid());
if (Binder.getCallingUid() == 0) {
copyInteger(Constants.UID, values, filteredValues);
}
copyStringWithDefault(Downloads.COLUMN_TITLE, values, filteredValues,
"");
copyStringWithDefault(Downloads.COLUMN_DESCRIPTION, values,
filteredValues, "");
filteredValues.put(Downloads.COLUMN_TOTAL_BYTES, -1);
filteredValues.put(Downloads.COLUMN_CURRENT_BYTES, 0);
Context context = getContext();
context.startService(new Intent(context, DownloadService.class));
long rowID = db.insert(DB_TABLE, null, filteredValues);
if (rowID == -1) {
Log.d(Constants.TAG, "couldn't insert into downloads database");
return null;
}
insertRequestHeaders(db, rowID, values);
context.startService(new Intent(context, DownloadService.class));
notifyContentChanged(uri, match);
return ContentUris.withAppendedId(Downloads.CONTENT_URI, rowID);
}
可以看到上面的代码很长,但是我们只需要关注一些核心的代码。
long rowID = db.insert(DB_TABLE, null, filteredValues);
这行代码的作用主要是把下载的任务信息保存到数据库中,包括下载的URL、下载的控制状态、下载状态、总的文件大小、已下载的文件大小等默认的数据更新到数据库中。
context.startService(new Intent(context, DownloadService.class));
这行代码的作用就是启动DownloadService服务。
************************************************************************************************************************************************************************************
当我们启动DownloadService之后,DownloadService服务的onCreate()函数就会被调用。
public void onCreate() {
super.onCreate();
if (Constants.LOGVV) {
Log.v(Constants.TAG, "Service onCreate");
}
if (mSystemFacade == null) {
mSystemFacade = new RealSystemFacade(this);
}
mObserver = new DownloadManagerContentObserver();
getContentResolver().registerContentObserver(
Downloads.ALL_DOWNLOADS_CONTENT_URI, true, mObserver);
mNotifier = new DownloadNotification(this, mSystemFacade);
mSystemFacade.cancelAllNotifications();
updateFromProvider();
}
可以看到,在onCreate()函数中,会注册一个数据库变化监听器DownloadManagerContentObserver,就是说Downloads.ALL_DOWNLOADS_CONTENT_URI这个数据库的数据发生变化的时候,该监听器的监听函数onChange()就会被调用。
public void onChange(final boolean selfChange) {
if (Constants.LOGVV) {
Log.v(Constants.TAG,
"Service ContentObserver received notification");
}
updateFromProvider();
}
可以看到onChange()函数会调用updateFromProvider()这个函数,从上面可以看到,onCreate()函数也会调到这个函数。
private void updateFromProvider() {
synchronized (this) {
mPendingUpdate = true;
if (mUpdateThread == null) {
mUpdateThread = new UpdateThread();
mSystemFacade.startThread(mUpdateThread);
}
}
}
可以看到,updateFormProvider()函数其实就是会启动UpdateThread()这个线程。
*********************************************************************************************************************************************************************************
下面就进入到UpdateThread这个线程中。
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
trimDatabase();
removeSpuriousFiles();
boolean keepService = false;
// for each update from the database, remember which download is
// supposed to get restarted soonest in the future
long wakeUp = Long.MAX_VALUE;
for (;;) {
synchronized (DownloadService.this) {
if (mUpdateThread != this) {
throw new IllegalStateException(
"multiple UpdateThreads in DownloadService");
}
if (!mPendingUpdate) {
mUpdateThread = null;
if (!keepService) {
stopSelf();
}
if (wakeUp != Long.MAX_VALUE) {
scheduleAlarm(wakeUp);
}
return;
}
mPendingUpdate = false;
}
long now = mSystemFacade.currentTimeMillis();
keepService = false;
wakeUp = Long.MAX_VALUE;
Set idsNoLongerInDatabase = new HashSet(
mDownloads.keySet());
Cursor cursor = getContentResolver().query(
Downloads.ALL_DOWNLOADS_CONTENT_URI, null, null, null,
null);
if (cursor == null) {
continue;
}
try {
DownloadInfo.Reader reader = new DownloadInfo.Reader(
getContentResolver(), cursor);
int idColumn = cursor.getColumnIndexOrThrow(Downloads._ID);
for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor
.moveToNext()) {
long id = cursor.getLong(idColumn);
idsNoLongerInDatabase.remove(id);
DownloadInfo info = mDownloads.get(id);
if (info != null) {
updateDownload(reader, info, now);
} else {
info = insertDownload(reader, now);
}
if (info.hasCompletionNotification()) {
keepService = true;
}
long next = info.nextAction(now);
if (next == 0) {
keepService = true;
} else if (next > 0 && next < wakeUp) {
wakeUp = next;
}
}
} finally {
cursor.close();
}
for (Long id : idsNoLongerInDatabase) {
deleteDownload(id);
}
// is there a need to start the DownloadService? yes, if there
// are rows to be deleted.
for (DownloadInfo info : mDownloads.values()) {
if (info.mDeleted) {
keepService = true;
break;
}
}
mNotifier.updateNotification(mDownloads.values());
// look for all rows with deleted flag set and delete the rows
// from the database
// permanently
for (DownloadInfo info : mDownloads.values()) {
if (info.mDeleted) {
Helpers.deleteFile(getContentResolver(), info.mId,
info.mFileName, info.mMimeType);
}
}
}
}
可以看到UpdateThread这个线程也是很长,我们大概分析一下它的作用。
可以看到这里有一个for(;;)的死循环,它的作用是保证数据库中的下载任务都会被加载出来,然后启动所有的下载任务,同时会更新下载任务,包括更新下载任务的状态,删除一些下载任务。
它会从数据库中取出所有的下载任务,然后根据id从mDownloads集合中找到对应的下载任务,如果该任务已存在,则调用updateDownload()进行下载信息的更新,并启动下载任务;如果该任务没有执行过,则会调用insertDownload()方法插入下载信息,并启动下载任务。
private void updateDownload(DownloadInfo.Reader reader, DownloadInfo info,
long now) {
int oldVisibility = info.mVisibility;
int oldStatus = info.mStatus;
reader.updateFromDatabase(info);
boolean lostVisibility = oldVisibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& info.mVisibility != Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
&& Downloads.isStatusCompleted(info.mStatus);
boolean justCompleted = !Downloads.isStatusCompleted(oldStatus)
&& Downloads.isStatusCompleted(info.mStatus);
if (lostVisibility || justCompleted) {
mSystemFacade.cancelNotification(info.mId);
}
info.startIfReady(now);
}
可以看到该函数调用startIfReady()进行下载任务的启动。
*********************************************************************************************************************************************************
void startIfReady(long now) {
if (!isReadyToStart(now)) {
return;
}
if (Constants.LOGV) {
Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
}
if (mHasActiveThread) {
throw new IllegalStateException("Multiple threads on same download");
}
if (mStatus != Downloads.STATUS_RUNNING) {
mStatus = Downloads.STATUS_RUNNING;
ContentValues values = new ContentValues();
values.put(Downloads.COLUMN_STATUS, mStatus);
mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
return;
}
DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
mHasActiveThread = true;
mSystemFacade.startThread(downloader);
}
该函数是一个挺重要的函数,它会根据不同的情况判断下载任务是否需要启动。
判断函数是isReadyToStart。这个函数十分关键,在我们要实现暂停下载,继续下载这个功能,都是在这里起作用的。
private boolean isReadyToStart(long now) {
if (mHasActiveThread) {
// already running
return false;
}
if (mControl == Downloads.CONTROL_PAUSED) {
// the download is paused, so it's not going to start
return false;
}
switch (mStatus) {
case 0: // status hasn't been initialized yet, this is a new download
case Downloads.STATUS_PENDING: // download is explicit marked as ready to start
case Downloads.STATUS_RUNNING: // download interrupted (process killed etc) while
// running, without a chance to update the database
return true;
case Downloads.STATUS_WAITING_FOR_NETWORK:
case Downloads.STATUS_QUEUED_FOR_WIFI:
return checkCanUseNetwork() == NETWORK_OK;
case Downloads.STATUS_WAITING_TO_RETRY:
// download was waiting for a delayed restart
return restartTime(now) <= now;
}
return false;
}
可以看到,当下载任务正在进行,或者下载任务状态为暂停状态,或者网络状态是否正常,这时会返回false,就是没有准备好,就不会启动下载任务。
当返回true的时候,就会把当前下载任务的状态刷新为Downloads.STATUS_RUNNING,同时会启动DownloadThread下载线程。
*********************************************************************************************************************************************************************
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
State state = new State(mInfo);
PowerManager.WakeLock wakeLock = null;
int finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
try {
PowerManager pm = (PowerManager) mContext
.getSystemService(Context.POWER_SERVICE);
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
Constants.TAG);
wakeLock.acquire();
if (Constants.LOGV) {
Log.v(Constants.TAG, "initiating download for " + mInfo.mUri);
}
boolean finished = false;
while (!finished) {
Log.i(Constants.TAG, "Initiating request for download "
+ mInfo.mId);
Request.Builder requestBuilder = new Request.Builder();
InnerState innerState = new InnerState();
setupDestinationFile(state, innerState);
addRequestHeaders(innerState, requestBuilder);
requestBuilder.url(state.mRequestUri);
Request request = requestBuilder.build();
Call call = mOkHttpClient.newCall(request);
try {
executeDownload(innerState, state, call);
finished = true;
} catch (RetryDownload exc) {
// fall through
} finally {
call.cancel();
}
}
if (Constants.LOGV) {
Log.v(Constants.TAG, "download completed for " + mInfo.mUri);
}
finalizeDestinationFile(state);
finalStatus = Downloads.STATUS_SUCCESS;
} catch (StopRequest error) {
// remove the cause before printing, in case it contains PII
Log.w(Constants.TAG, "Aborting request for download " + mInfo.mId
+ ": " + error.getMessage());
finalStatus = error.mFinalStatus;
// fall through to finally block
} catch (Throwable ex) { // sometimes the socket code throws unchecked
// exceptions
Log.w(Constants.TAG, "Exception for id " + mInfo.mId + ": " + ex);
finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
// falls through to the code that reports an error
} finally {
if (wakeLock != null) {
wakeLock.release();
wakeLock = null;
}
if (mOkHttpClient != null) {
mOkHttpClient.cancel(null);
}
cleanupDestination(state, finalStatus);
notifyDownloadCompleted(finalStatus, state.mCountRetry,
state.mRetryAfter, state.mGotData, state.mFilename,
state.mNewUri, state.mMimeType);
mInfo.mHasActiveThread = false;
}
}
setupDestinationFile(state, innerState);
这个方法是实现断点续传的关键点。
private void setupDestinationFile(State state, InnerState innerState)
throws StopRequest {
if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already
// run a thread for this
// download
if (!Helpers.isFilenameValid(state.mFilename)) {
// this should never happen
throw new StopRequest(Downloads.STATUS_FILE_ERROR,
"found invalid internal destination filename");
}
// We're resuming a download that got interrupted
File f = new File(state.mFilename);
if (f.exists()) {
long fileLength = f.length();
if (fileLength == 0) {
// The download hadn't actually started, we can restart from
// scratch
f.delete();
state.mFilename = null;
} else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
// This should've been caught upon failure
f.delete();
throw new StopRequest(Downloads.STATUS_CANNOT_RESUME,
"Trying to resume a download that can't be resumed");
} else {
// All right, we'll be able to resume this download
try {
state.mStream = new FileOutputStream(state.mFilename,
true);
} catch (FileNotFoundException exc) {
throw new StopRequest(Downloads.STATUS_FILE_ERROR,
"while opening destination for resuming: "
+ exc.toString(), exc);
}
innerState.mBytesSoFar = (int) fileLength;
if (mInfo.mTotalBytes != -1) {
innerState.mHeaderContentLength = Long
.toString(mInfo.mTotalBytes);
}
innerState.mHeaderETag = mInfo.mETag;
innerState.mContinuingDownload = true;
}
}
}
if (state.mStream != null
&& mInfo.mDestination == Downloads.DESTINATION_EXTERNAL) {
closeDestination(state);
}
}
方法的流程大概是:先根据文件名建立一个文件对象,判断文件对象是否存在,如果存在再判断文件的大小,当文件大小为0的时候,把文件删除。
同时,会把当前的文件的输出流保存到state.mStream,把当前文件的长度、要下载文件的总长度、文件继续下载状态保存到innerState中。
再分析addRequestHeader()方法,该方法也是实现断点续传的关键。
private void addRequestHeaders(InnerState innerState, Request.Builder requestBuilder) {
for (Pair header : mInfo.getHeaders()) {
requestBuilder.addHeader(header.first, header.second);
}
if (innerState.mContinuingDownload) {
if (innerState.mHeaderETag != null) {
requestBuilder.addHeader("If-Match", mInfo.mETag);
}
requestBuilder.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-");
}
}
该方法会把请求头添加到请求中,最重要的是:如果是断点续传的话,会把当前的文件大小也放到请求头中,这样服务器就会知道当前的文件已经下载了多少。
下面来分析最重要的executeDownload方法。
private void executeDownload(InnerState innerState, State state, Call call) throws StopRequest, RetryDownload, IOException {
byte data[] = new byte[Constants.BUFFER_SIZE];
// check just before sending the request to avoid using an invalid
// connection at all
checkConnectivity(state);
Response response = call.execute();
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);
}
可以看到这个下载工具用到okhttp这个开源库进行网络的请求。使用okhttp得到了Response对象。
先分析processResponseHeaders()这个方法,这个方法中会获取Http请求的header,同时,根据这次下载是否为断点下载,如果是则返回,如果不是,则会把要下载的文件的输入流对象保存到state.mStream变量中。
再分析openResponseEntity()这个方法。
private InputStream openResponseEntity(State state, Response response)
throws StopRequest {
try {
return response.body().byteStream();
} catch (IOException ex) {
logNetworkState();
throw new StopRequest(getFinalStatusForHttpError(state),
"while getting entity: " + ex.toString(), ex);
}
}
该方法是获取Response对象的输出流变量。
最后是transferData()方法。
private void transferData(State state, InnerState innerState, byte[] data,
InputStream entityStream) throws StopRequest {
for (; ; ) {
int bytesRead = readFromResponse(state, innerState, data,
entityStream);
if (bytesRead == -1) { // success, end of stream already reached
handleEndOfStream(state, innerState);
return;
}
state.mGotData = true;
writeDataToDestination(state, data, bytesRead);
innerState.mBytesSoFar += bytesRead;
reportProgress(state, innerState);
if (Constants.LOGVV) {
Log.v(Constants.TAG, "downloaded " + innerState.mBytesSoFar
+ " for " + mInfo.mUri);
}
checkPausedOrCanceled(state);
}
}
可以看到,这是一个for(;;)死循环,用于读取下载的文件流。
先调用readFromResponse()函数,从文件输出流中读取数据,保存到data字节数组中。
然后调用writeDataToDestination()函数,把data字节数组中的数据写到本地的文件中。
然后调用reportProgress()函数,把已下载的文件的大小更新到数据库中。用于更新进度条的显示。
可以看到checkPauseOrCanceled()函数。这是实现暂停下载的关键函数。
private void checkPausedOrCanceled(State state) throws StopRequest {
synchronized (mInfo) {
if (mInfo.mControl == Downloads.CONTROL_PAUSED) {
throw new StopRequest(Downloads.STATUS_PAUSED_BY_APP,
"download paused by owner");
}
}
if (mInfo.mStatus == Downloads.STATUS_CANCELED) {
throw new StopRequest(Downloads.STATUS_CANCELED,
"download canceled");
}
}
可以分析,这里会根据下载任务的当前状态进行判断,如果当前的任务状态被更改为Downloads.CONTROL_PAUSED时,就会抛出StopRequest的异常,当前的文件下载就会被终止,这样就可以实现暂停下载了。
到此为止,DownloadManager下载的整个流程就分析完了。
**************************************************************************************************************************************************************
DownloadManager的扩展使用
通过上面的分析,我们几乎理解了DownloadManager的整个工作流程。在我们下载文件的时候,我们几乎都是需要暂停下载和继续下载还有断点续传的功能。DownloadProvider代码是可以让我们能够实现这个功能了。
实现断点续传的原理其实就是我们每次添加下载任务,都会把任务的信息保存到数据库中,包括下载的URL,已下载的文件大小,总的文件大小。下次我们再进行下载的时候,把已下载的大小传到服务器中,就可以从上一次已下载的文件的基础上继续下载,就可以实现断点下载了。
暂停下载和继续下载的实现,其实只需要更新下载任务的状态就可以实现了。因为从上面的下载可以知道,在下载文件的过程中,都会检验当前的下载任务的状态,若是暂停状态,就会停止下载,跳出死循环。当我们再次改变状态为继续下载时,下载任务会被再次启动。
具体实现可以参考这个博客:
http://www.trinea.cn/android/android-downloadmanager-pro/