用粗糙而简陋的语言描述完了Andriod系统的DownloadManager的DownloadThread类,那么,继续我们的粗糙吧。现在就来描述一下DownloadManager的生命——DownloadService类。一如前面的思路,现在假设我们都没看过DownloadService的实现。我们打算怎么弄懂它呢,或者,对于这个神奇的类,你有哪些疑问呢。以下是我能想到的问题:
1、它完成了什么功能,最主要做了什么事儿,这肯定是我们第一个关心的。
2、系统服务与我们所用的应用层Service有什么不同吗?
3、对于一个Service,它实现了哪些接口?
4、这里是不是就是下载的开始点呢?
先看看第三个问题吧,第三个问题最简单,为什么呢,看看源码,看它Override 了几个接口就知道了。
/** * Performs the background downloads requested by applications that use the Downloads provider. */ public class DownloadService extends Service { @Override public IBinder onBind(Intent i) { throw new UnsupportedOperationException("Cannot bind to Download Manager Service"); } @Override public void onCreate() { } @Override public int onStartCommand(Intent intent, int flags, int startId) { } @Override public void onDestroy() { } @Override protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) { } }
从实现的接口上来说,它只是实现了一个服务最基础的几个接口,并没有其他特殊接口。不过,这里有一个比较有意思的接口实现,对了,就是OnBind()。它告诉别人,你不能绑定我,只能启动我。一般来说,如果我们要绑定一个服务,可能会希望通过与服务注册一个远程回调,然后服务通过远程的回调将计算结果传给绑定服务的客户端。那么,可想而知,DownloadService是希望不与客户端直接通信的。这算不算与我们写的Service的不同点呢。
那么它是怎么通信的,简单点说,就是通过DownloadProvider来进行的。其实,这样做也能很容易就理解到。因为如果是采用接口的方式的话,服务可能还没发完所有回调,结果自己挂调了,那么就有可能它的客户端就得不到正常的更新。从而导致状态错误。
接下来,可以看第一个问题了。它都做了些什么。从面向对象的角度来说。一个类能不能做什么,是不是先要看看它有哪些成员属性以及成员方法呢。那就行看其成员属性吧。
/** amount of time to wait to connect to MediaScannerService before timing out */ // 等待扫描服务的超时时间 private static final long WAIT_TIMEOUT = 10 * 1000; /** Observer to get notified when the content observer's data changes */ //能够监听DownloadProvider的Observer private DownloadManagerContentObserver mObserver; /** Class to handle Notification Manager updates */ // 通知栏箮理 private DownloadNotifier mNotifier; //下载队列,通过从DownloadProvider里取出,并将每条下载记录与其生成的下载ID作为一个Map值。 private Map<Long, DownloadInfo> mDownloads = Maps.newHashMap(); /** * The thread that updates the internal download list from the content * provider. */ //更新线程,是DownloadService的一个内部类,同时,这个也是其功臣类,幕后的英雄。 @VisibleForTesting UpdateThread mUpdateThread; /** * Whether the internal download list should be updated from the content * provider. */ //判断是否需要更新mDownloads private boolean mPendingUpdate; /** * The ServiceConnection object that tells us when we're connected to and disconnected from * the Media Scanner */ //媒体扫描,不关的可以不管,目前我就没管它 private MediaScannerConnection mMediaScannerConnection; private boolean mMediaScannerConnecting; /** * The IPC interface to the Media Scanner */ private IMediaScannerService mMediaScannerService; //外观接口,其实就是通过这个接口去做一些公共的事 @VisibleForTesting SystemFacade mSystemFacade; //存储管理 private StorageManager mStorageManager;
以上基本就是DoiwnloadService的成员属性了。关于媒体扫描,暂时可以不管,因为它并不决定下载,初步理解阶段,我们只关心我们最关心的问题。这里,我们只关心最有用,关系也最紧密的两个成员,即mDonwnloads以及mUpdateThread。
简单的过了一遍其成员接口,以及其它所实现的接口。应该对其一些感性的认识了,至少不应该那么陌生了。好了,那么真正去分析这个类吧。
对于一个服务,最让人容易想到的就是其生命周期。所以,引导我们的头,肯定是其onCrreate与onStartCommand.大家都知道,onCreate只执行一次,而onStartCommand会随服务被startService而启动多次。下面依次来看这两个代码。
/** * Initializes the service when it is first created */ @Override public void onCreate() { super.onCreate(); //初始化外观接口 if (mSystemFacade == null) { mSystemFacade = new RealSystemFacade(this); } //实始化并注册一个监听DownloadProvider的Observer mObserver = new DownloadManagerContentObserver(); getContentResolver().registerContentObserver(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, true, mObserver); //媒体扫描相关 mMediaScannerService = null; mMediaScannerConnecting = false; mMediaScannerConnection = new MediaScannerConnection(); // 通知管理 mNotifier = new DownloadNotifier(this); mNotifier.cancelAll(); //初始化存储管理 mStorageManager = StorageManager.getInstance(getApplicationContext()); //从DownloadProvider更新下载。 updateFromProvider(); }
onCreate与我们的习惯写法应该差不多,就是初始化实例。当然,这里也做了一件很重要的事情,那就是从DownloadProvider更新下载。
@Override public int onStartCommand(Intent intent, int flags, int startId) { int returnValue = super.onStartCommand(intent, flags, startId); if (Constants.LOGVV) { Log.v(Constants.TAG, "Service onStart"); } updateFromProvider(); return returnValue; }
onStartCommand就做一件我们所认为的一件很重要的事儿,即更新下载列表。好吧,不防来看看这个方法的实现吧。
/** * Parses data from the content provider into private array */ private void updateFromProvider() { synchronized (this) { mPendingUpdate = true; if (mUpdateThread == null) { mUpdateThread = new UpdateThread(); mSystemFacade.startThread(mUpdateThread); } } }
哈哈,我最喜欢看这种实现了,简单明了。首先判断了这个线程是否为NULL,这就表明了这个线程的唯一性。然再通过外观接口启动这个线程。关于这个外观接口是如何启动线程的呢,暂时不管,还是坚持一个原则,关心我们关心的,从流程上看懂它。
除了onCrreate与onStartCommand两个生命周期的方法调用了updateFromProvider().还有两个地方调用了。即监听DownloadProvider的Observer和媒体扫描的回调接口里。它们的代码分别如下:
监听DownloadProvider的Observer /** * Receives notifications when the data in the content provider changes */ private class DownloadManagerContentObserver extends ContentObserver { public DownloadManagerContentObserver() { super(new Handler()); } /** * Receives notification when the data in the observed content * provider changes. */ @Override public void onChange(final boolean selfChange) { if (Constants.LOGVV) { Log.v(Constants.TAG, "Service ContentObserver received notification"); } updateFromProvider(); } }
/** * Gets called back when the connection to the media * scanner is established or lost. */ public class MediaScannerConnection implements ServiceConnection { public void onServiceConnected(ComponentName className, IBinder service) { if (Constants.LOGVV) { Log.v(Constants.TAG, "Connected to Media Scanner"); } synchronized (DownloadService.this) { try { mMediaScannerConnecting = false; mMediaScannerService = IMediaScannerService.Stub.asInterface(service); if (mMediaScannerService != null) { updateFromProvider(); } } finally { // notify anyone waiting on successful connection to MediaService DownloadService.this.notifyAll(); } } }
四个调用的地方 ,就说明了下载列表的更新,只能是这四种方式。其实在Android中,这四种方式是基本上包括了的。这里就说明白了第四个问题了吧,下载确实是从这里开始的。而关于UpdateThread是如何实现的,这属于细节问题。可看可不看,为什么。因为实现的方式千千万万,最重要的是下载的这一管理思想。当然,秉着学习的态度,我们还是要继续看下去的。那就继续看看这个UpdateThread类的实现吧。
private class UpdateThread extends Thread { public UpdateThread() { super("Download Service"); } @Override public void run() { //把自己设为后台线程 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 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"); } //永真循环的出口。这晨要明白的是mPendingUpdate是在两个地方被赋值,即在updateFromProvider里被设为true,表示准备更新或者正在更新。而在检查完mPendingUpdate之后,其值马上又会被置为false。这也就是说,如果没有触发新的事件调用到updateFromProvider,那么本次更新就结束了。 if (!mPendingUpdate) { mUpdateThread = null; if (!keepService) { stopSelf(); } if (wakeUp != Long.MAX_VALUE) { scheduleAlarm(wakeUp); } return; } mPendingUpdate = false; } synchronized (mDownloads) { long now = mSystemFacade.currentTimeMillis(); boolean mustScan = false; keepService = false; wakeUp = Long.MAX_VALUE; Set<Long> idsNoLongerInDatabase = new HashSet<Long>(mDownloads.keySet()); //查询出所有的下载 Cursor cursor = getContentResolver().query(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, null, null, null, null); if (cursor == null) { continue; } try { //初始化DownloadInfo的ContentResolver和Cursor,从而可以让DownloadInfo自己从数据库中取出数据填充到自己的属性成员中去。这是非常符合面向对象原则的。包括后面还会看到准备下载也是DownloadInfo自己完成的。 DownloadInfo.Reader reader = new DownloadInfo.Reader(getContentResolver(), cursor); int idColumn = cursor.getColumnIndexOrThrow(Downloads.Impl._ID); if (Constants.LOGVV) { Log.i(Constants.TAG, "number of rows from downloads-db: " + cursor.getCount()); } //循环遍历Cursor,再熟悉不过了。 for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { long id = cursor.getLong(idColumn); //取出一份数据后,在idsNoLongerInDatabase这个Set里标记下这个数据在数据库中是存在的。 idsNoLongerInDatabase.remove(id); DownloadInfo info = mDownloads.get(id); //判断队列里是否已经有这个DownloadInfo了,如果没有则通过insertDownloadLocked方法将其插入到下载队列,并通过DownloadInfo自身来启动下载。如果已经在下载列表里了,则更新其数据。 if (info != null) { updateDownload(reader, info, now); } else { info = insertDownloadLocked(reader, now); } if (info.shouldScanFile() && !scanFile(info, true, false)) { mustScan = true; keepService = true; } 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(); } //移除掉不在数据库中,但又在下载附表里的DownloadInfo,因为它们已经没用了。 for (Long id : idsNoLongerInDatabase) { deleteDownloadLocked(id); } //下面的一大段都与媒体扫描相关。主要判断某DownloadInfo是否已经被删除了。如果被删除了,在这里要把它媒体库中删除掉,还要把它从下载数据库中删除,以及还要删除其所对应的文件 // is there a need to start the DownloadService? yes, if there are rows to be // deleted. if (!mustScan) { for (DownloadInfo info : mDownloads.values()) { if (info.mDeleted && TextUtils.isEmpty(info.mMediaProviderUri)) { mustScan = true; keepService = true; break; } } } mNotifier.updateWith(mDownloads.values()); if (mustScan) { bindMediaScanner(); } else { mMediaScannerConnection.disconnectMediaScanner(); } // look for all rows with deleted flag set and delete the rows from the database // permanently for (DownloadInfo info : mDownloads.values()) { if (info.mDeleted) { // this row is to be deleted from the database. but does it have // mediaProviderUri? if (TextUtils.isEmpty(info.mMediaProviderUri)) { if (info.shouldScanFile()) { // initiate rescan of the file to - which will populate // mediaProviderUri column in this row if (!scanFile(info, false, true)) { throw new IllegalStateException("scanFile failed!"); } continue; } } else { // yes it has mediaProviderUri column already filled in. // delete it from MediaProvider database. getContentResolver().delete(Uri.parse(info.mMediaProviderUri), null, null); } // delete the file deleteFileIfExists(info.mFileName); // delete from the downloads db getContentResolver().delete(Downloads.Impl.ALL_DOWNLOADS_CONTENT_URI, Downloads.Impl._ID + " = ? ", new String[]{String.valueOf(info.mId)}); } } } } }
到这里,DownloadService大概是讲完了的。当然很多细节地方都没有讲到。因为细节的话需要结束整个代码来讲才有可能讲的透彻些。
写在后面的话:
关于前面的四个问题,3、4应该是没问题了。而第2个问题,这个服务有什么特别的,看完源码后,会发现,其实也没什么特别的,不过它却是没有AIDL的。而第1个问题,它干了什么事儿。总结下来至少有以下三点:
1、接收更新,即updateFromProvider,一切都从这里开始,不管是新增加的下载或者是其他比如删除、暂停等操作。注意系统下载是支持断点续传的,也有暂停和恢复的方法,只是它没提供出接口出来。
2、构造一个本地下载列表。把更新得到的数据都构造成DownloadInfo,保存在这里列表里,并通过DownloadInfo自身来启动下载。(PS:下载就启动了吗?)
3、清理被删除的DownloadInfo。其实这也算一个比较大的功能,因为其实一个下载涉及到了三方面的数据,媒体库,下载数据库,还有下载的文件。三个都是需要同步删除的。