DownloadManager之DownloadService淺析

用粗糙而簡陋的語言描述完了Andriod系統的DownloadManager的DownloadThread類,那麼,繼續咱們的粗糙吧。如今就來描述一下DownloadManager的生命——DownloadService類。一如前面的思路,如今假設咱們都沒看過DownloadService的實現。咱們打算怎麼弄懂它呢,或者,對於這個神奇的類,你有哪些疑問呢。如下是我能想到的問題: java

一、它完成了什麼功能,最主要作了什麼事兒,這確定是咱們第一個關心的。 數據庫

二、系統服務與咱們所用的應用層Service有什麼不一樣嗎? app

三、對於一個Service,它實現了哪些接口? ide

四、這裏是否是就是下載的開始點呢? 學習

先看看第三個問題吧,第三個問題最簡單,爲何呢,看看源碼,看它Override 了幾個接口就知道了。 this

/**
 * 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的不一樣點呢。 spa

那麼它是怎麼通訊的,簡單點說,就是經過DownloadProvider來進行的。其實,這樣作也能很容易就理解到。由於若是是採用接口的方式的話,服務可能還沒發完全部回調,結果本身掛調了,那麼就有可能它的客戶端就得不到正常的更新。從而致使狀態錯誤。 線程

接下來,能夠看第一個問題了。它都作了些什麼。從面向對象的角度來講。一個類能不能作什麼,是否是先要看看它有哪些成員屬性以及成員方法呢。那就行看其成員屬性吧。 rest

 /** 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。 code

    簡單的過了一遍其成員接口,以及其它所實現的接口。應該對其一些感性的認識了,至少不該該那麼陌生了。好了,那麼真正去分析這個類吧。

    對於一個服務,最讓人容易想到的就是其生命週期。因此,引導咱們的頭,確定是其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大概是講完了的。固然不少細節地方都沒有講到。由於細節的話須要結束整個代碼來說纔有可能講的透徹些。


寫在後面的話:

    關於前面的四個問題,三、4應該是沒問題了。而第2個問題,這個服務有什麼特別的,看完源碼後,會發現,其實也沒什麼特別的,不過它倒是沒有AIDL的。而第1個問題,它幹了什麼事兒。總結下來至少有如下三點:

一、接收更新,即updateFromProvider,一切都從這裏開始,不論是新增長的下載或者是其餘好比刪除、暫停等操做。注意系統下載是支持斷點續傳的,也有暫停和恢復的方法,只是它沒提供出接口出來。

二、構造一個本地下載列表。把更新獲得的數據都構形成DownloadInfo,保存在這裏列表裏,並經過DownloadInfo自身來啓動下載。(PS:下載就啓動了嗎?)

三、清理被刪除的DownloadInfo。其實這也算一個比較大的功能,由於其實一個下載涉及到了三方面的數據,媒體庫,下載數據庫,還有下載的文件。三個都是須要同步刪除的。

相關文章
相關標籤/搜索