流行框架源碼解析(17)-英語流利說文件下載器源碼解析

主目錄見:Android高級進階知識(這是總目錄索引)
下載器Github地址:FileDownloader

?文件下載在Android的開發(fā)中應(yīng)該可以說是都會用到,所以一個完善的好的下載工具應(yīng)該是不可或缺的,第一次看這個框架還是感覺類比較多的,應(yīng)該是作者為了類的職責(zé)劃分更加明確吧,但是個人感覺適當(dāng)為好。正如介紹所說,這個框架是多任務(wù),多線程,支持斷點恢復(fù),高并發(fā),簡單易用且支持多進程的。確實是個不錯的框架,值得我們來讀讀代碼,不過我也只是看過一下午,如果有理解不到位的大家諒解一下。

一.目標(biāo)

這篇文章應(yīng)該來說也是比較實用的,而且本庫的wiki應(yīng)該也有部分的介紹,但是我們今天的主要目標(biāo)如下:
1.學(xué)習(xí)該庫中優(yōu)秀的思維;
2.懂得自己開發(fā)或者修改和改進該庫。

二.源碼解析

首先當(dāng)你拿到一個庫不知道從哪開始的時候,往往就是從使用上入手,所以我們先來看看怎么使用該庫,因為使用方式有幾種,我們挑其中一種來說明:
1.添加gradle依賴

dependencies {
    compile 'com.liulishuo.filedownloader:library:1.6.8'
}

2.在Application初始化

  FileDownloader.setupOnApplicationOnCreate(this)
                .connectionCreator(new FileDownloadUrlConnection
                        .Creator(new FileDownloadUrlConnection.Configuration()
                        .connectTimeout(15_000) // set connection timeout.
                        .readTimeout(15_000) // set read timeout.
                        .proxy(Proxy.NO_PROXY) // set proxy
                ))
                .commit();

3.開始下載(這里我挑一個使用任務(wù)隊列來下載的方式)

 final FileDownloadQueueSet queueSet = new FileDownloadQueueSet(downloadListener);

        final List<BaseDownloadTask> tasks = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            tasks.add(FileDownloader.getImpl().create(Constant.URLS[i]).setTag(i + 1));
        }
        queueSet.disableCallbackProgressTimes(); // do not want each task's download progress's callback,
        // we just consider which task will completed.

        // auto retry 1 time if download fail
        queueSet.setAutoRetryTimes(1);

        if (serialRbtn.isChecked()) {
            // start download in serial order
            queueSet.downloadSequentially(tasks);
        } else {
            // start parallel download
            queueSet.downloadTogether(tasks);
        }
        queueSet.start();

那么我們就從初始化入手,我們先來看看FileDownloader#setupOnApplicationOnCreate()干了什么工作。

1.setupOnApplicationOnCreate

 public static DownloadMgrInitialParams.InitCustomMaker setupOnApplicationOnCreate(Application application) {
        final Context context = application.getApplicationContext();
        //將context緩存在FileDownloadHelper中
        FileDownloadHelper.holdContext(context);

        //實例化InitCustomMaker
        DownloadMgrInitialParams.InitCustomMaker customMaker = new DownloadMgrInitialParams.InitCustomMaker();
       //賦值給DownloadMgrInitialParams然后保存在CustomComponentHolder中
        CustomComponentHolder.getImpl().setInitCustomMaker(customMaker);

        return customMaker;
    }

我們看到這個方法沒有做什么工作,這也是合理的,因為在Application#onCreate避免做過多的工作導(dǎo)致啟動速度變慢。這個方法會返回一個InitCustomMaker類的實例,所以程序會調(diào)用這個類connectionCreator()方法:

 public InitCustomMaker connectionCreator(FileDownloadHelper.ConnectionCreator creator) {
            this.mConnectionCreator = creator;
            return this;
        }

這個方法就是將一個連接創(chuàng)建器賦值給InitCustomMaker類中的變量,返回this是為了鏈?zhǔn)讲僮餍枰驗?code>InitCustomMaker這里還有很多可以自定義的方法。這樣我們的初始化工作就完成了,我們來看我們的開始下載部分。

2.FileDownloadQueueSet的設(shè)置

這個類是用來配置下載任務(wù)和啟動下載任務(wù)的,首先我們看到構(gòu)造函數(shù)里面給他設(shè)置了一個FileDownloadListener實例用于回調(diào)。然后會在串行下載或者并行下載的時候會給他傳一個任務(wù)集合,我們首先來看看任務(wù)集合的創(chuàng)建和添加。任務(wù)的創(chuàng)建是在FileDownloader#create中創(chuàng)建的:

  public BaseDownloadTask create(final String url) {
        return new DownloadTask(url);
    }

通過這個我們可以知道,一個下載任務(wù)對應(yīng)一個DownloadTask,我們看下它的構(gòu)造函數(shù):

    DownloadTask(final String url) {
        this.mUrl = url;
        mPauseLock = new Object();
        final DownloadTaskHunter hunter = new DownloadTaskHunter(this, mPauseLock);

        mHunter = hunter;
        mMessageHandler = hunter;
    }

這里除了給DownloadTask的mUrl賦值之外,還實例化了一個DownloadTaskHunter實例,我們看到這里參數(shù)將DownloadTask實例傳給了它的構(gòu)造函數(shù),然后傳了mPauseLock用來做線程鎖操作。我們看下DownloadTaskHunter構(gòu)造函數(shù):

 DownloadTaskHunter(ICaptureTask task, Object pauseLock) {
        mPauseLock = pauseLock;
        mTask = task;
        final DownloadSpeedMonitor monitor = new DownloadSpeedMonitor();
        mSpeedMonitor = monitor;
        mSpeedLookup = monitor;
        mMessenger = new FileDownloadMessenger(task.getRunningTask(), this);
    }

可以看到除了傳進來的兩個參數(shù)賦值之外,還是實例化了DownloadSpeedMonitor類,這個類主要是用來監(jiān)測下載進度的。同時還實例化了FileDownloadMessenger類,這個類是用來給回調(diào)接口FileDownloadListener發(fā)送消息的。這樣我們大致看了下載任務(wù)DownloadTask的創(chuàng)建過程。

接下來就是FileDownloadQueueSet類中一些方法的設(shè)置,例如我們這里使用到的disableCallbackProgressTimes()方法,這個方法是設(shè)置了的話,那么我們就監(jiān)聽不到一個任務(wù)的下載進度了,只有任務(wù)下載完成的回調(diào)。與這個方法對應(yīng)的還有setCallbackProgressTimes()設(shè)置更新進度回調(diào)次數(shù),setCallbackProgressMinInterval()設(shè)置了更新進度的最小間隔時間。setAutoRetryTimes()方法是設(shè)置下載失敗重試的次數(shù)。然后會把上面創(chuàng)建的任務(wù)集合賦值給FileDownloadQueueSet類,這里調(diào)用的方法是downloadSequentially()downloadTogether(),這兩個方法唯一的不同就是會設(shè)置標(biāo)志位isSerial,來說明是串行下載還是并行下載。最后最重要的當(dāng)然就是start()方法了,這里就真正啟動開始下載了。

3.FileDownloadQueueSet start

 public void start() {
        //遍歷任務(wù)集合
        for (BaseDownloadTask task : tasks) {
            //設(shè)置任務(wù)的回調(diào)監(jiān)聽為傳進來的FileDownloadListener實例
            task.setListener(target);

            if (autoRetryTimes != null) {
                //設(shè)置失敗重試次數(shù)
                task.setAutoRetryTimes(autoRetryTimes);
            }

            if (syncCallback != null) {
              //設(shè)置是否進行同步回調(diào)
                task.setSyncCallback(syncCallback);
            }

            if (isForceReDownload != null) {
               //是否強制重新下載
                task.setForceReDownload(isForceReDownload);
            }

            if (callbackProgressTimes != null) {
                //設(shè)置進度回調(diào)次數(shù)
                task.setCallbackProgressTimes(callbackProgressTimes);
            }

            if (callbackProgressMinIntervalMillis != null) {
              //設(shè)置進度回調(diào)最小間隔時間
                task.setCallbackProgressMinInterval(callbackProgressMinIntervalMillis);
            }

            if (tag != null) {
                task.setTag(tag);
            }

            if (taskFinishListenerList != null) {
                //任務(wù)結(jié)束監(jiān)聽器,只有在非UI現(xiàn)場中調(diào)用
                for (BaseDownloadTask.FinishListener finishListener : taskFinishListenerList) {
                    task.addFinishListener(finishListener);
                }
            }

            if (this.directory != null) {
                //設(shè)置下載下來的文件存放的路徑
                task.setPath(this.directory, true);
            }

            if (this.isWifiRequired != null) {
                //是否需要只在wifi情況下才下載
                task.setWifiRequired(this.isWifiRequired);
            }

           //這個方法主要是標(biāo)識下是在隊列中的任務(wù),而且會將任務(wù)添加到FileDownloadList的任務(wù)集合中
            task.asInQueueTask().enqueue();
        }

      //啟動下載任務(wù)
        FileDownloader.getImpl().start(target, isSerial);
    }

我們看到這個方法主要是給任務(wù)DownloadTask設(shè)置一些參數(shù)值,然后將任務(wù)添加到FileDownloadList中的集合里。然后調(diào)用FileDownloader#start()開始下載任務(wù):

  public boolean start(final FileDownloadListener listener, final boolean isSerial) {

        if (listener == null) {
            FileDownloadLog.w(this, "Tasks with the listener can't start, because the listener " +
                    "provided is null: [null, %B]", isSerial);
            return false;
        }


        return isSerial ?
                getQueuesHandler().startQueueSerial(listener) :
                getQueuesHandler().startQueueParallel(listener);
    }

這個方法很簡單,這里isSerial 標(biāo)志我們在前面已經(jīng)設(shè)置了,我們這里就走并行下載這條道吧,所以isSerial 為false,所以這里會調(diào)用getQueuesHandler().startQueueParallel()方法來啟動并行下載任務(wù),我們這里跟蹤到QueuesHandler#startQueueParallel()方法:

  @Override
    public boolean startQueueParallel(FileDownloadListener listener) {
        final int attachKey = listener.hashCode();

        //這里主要防止在添加了下載任務(wù)之后又有新的任務(wù)到達,所以這里會重新組裝任務(wù)隊列
        final List<BaseDownloadTask.IRunningTask> list = FileDownloadList.getImpl().
                assembleTasksToStart(attachKey, listener);

        //這里會驗證是否任務(wù)隊列里面有任務(wù),然后會調(diào)用全局監(jiān)聽器的onRequestStart(),如果有打點/統(tǒng)計等需求可以考慮設(shè)置這個下載監(jiān)聽器
        if (onAssembledTasksToStart(attachKey, list, listener, false)) {
            return false;
        }

       //遍歷任務(wù)集合,然后分別啟動
        for (BaseDownloadTask.IRunningTask task : list) {
            task.startTaskByQueue();
        }

        return true;
    }

我們看到前面兩步就是重新組裝一下任務(wù)隊列,然后我們會調(diào)用任務(wù)task中的startTaskByQueue()方法進行啟動任務(wù):

 @Override
    public void startTaskByQueue() {
        startTaskUnchecked();
    }

  private int startTaskUnchecked() {
        if (isUsing()) {
            if (isRunning()) {
                throw new IllegalStateException(
                        FileDownloadUtils.formatString("This task is running %d, if you" +
                                " want to start the same task, please create a new one by" +
                                " FileDownloader.create", getId()));
            } else {
                throw new IllegalStateException("This task is dirty to restart, If you want to " +
                        "reuse this task, please invoke #reuse method manually and retry to " +
                        "restart again." + mHunter.toString());
            }
        }

        if (!isAttached()) {
            setAttachKeyDefault();
        }

        mHunter.intoLaunchPool();

        return getId();
    }

前面是判斷這個任務(wù)是否在執(zhí)行或者占用,最后會調(diào)用DownloadTaskHunter#intoLaunchPool()方法:

  @Override
    public void intoLaunchPool() {
        synchronized (mPauseLock) {
            //如果任務(wù)是在空閑狀態(tài)則現(xiàn)在設(shè)置狀態(tài)為toLaunchPool,否則返回
            if (mStatus != FileDownloadStatus.INVALID_STATUS) {
                FileDownloadLog.w(this, "High concurrent cause, this task %d will not input " +
                                "to launch pool, because of the status isn't idle : %d",
                        getId(), mStatus);
                return;
            }

            mStatus = FileDownloadStatus.toLaunchPool;
        }

       //獲取下載任務(wù)
        final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
        final BaseDownloadTask origin = runningTask.getOrigin();

        //如果設(shè)置了全局監(jiān)聽就調(diào)用他的onRequestStart
        if (FileDownloadMonitor.isValid()) {
            FileDownloadMonitor.getMonitor().onRequestStart(origin);
        }

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.v(this, "call start " +
                            "Url[%s], Path[%s] Listener[%s], Tag[%s]",
                    origin.getUrl(), origin.getPath(), origin.getListener(), origin.getTag());
        }

      //設(shè)置任務(wù)準(zhǔn)備就緒
        boolean ready = true;

        try {
            //前面給任務(wù)設(shè)置了path,也就是下載文件存放路徑,這里就是創(chuàng)建這個路徑
            prepare();
        } catch (Throwable e) {
            ready = false;

            FileDownloadList.getImpl().add(runningTask);
            FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
        }

        if (ready) {
            //調(diào)用FileDownloadTaskLauncher的launch啟動任務(wù)
            FileDownloadTaskLauncher.getImpl().launch(this);
        }

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.v(this, "the task[%d] has been into the launch pool.", getId());
        }
    }

前面的步驟已經(jīng)注釋的很清楚了,最后我們看到會調(diào)用FileDownloadTaskLauncher#launch()方法來啟動任務(wù),傳的參數(shù)就是本類DownloadTaskHunter實例,我們看下launch()方法:

 synchronized void launch(final ITaskHunter.IStarter taskStarter) {
        mLaunchTaskPool.asyncExecute(taskStarter);
    }

這里的mLaunchTaskPool是一個LaunchTaskPool類實例,所以會調(diào)用LaunchTaskPool#asyncExecute()方法:

public void asyncExecute(final ITaskHunter.IStarter taskStarter) {
            mPool.execute(new LaunchTaskRunnable(taskStarter));
}

mPool是一個自定義的線程池ThreadPoolExecutor實例,所以調(diào)用execute會執(zhí)行LaunchTaskRunnable#run()方法:

       LaunchTaskRunnable(final ITaskHunter.IStarter taskStarter) {
            this.mTaskStarter = taskStarter;
            this.mExpired = false;
        }

        @Override
        public void run() {
            if (mExpired) {
                return;
            }

            mTaskStarter.start();
        }

我們看到run方法中會調(diào)用mTaskStarterstart()方法,從上面我們可以知道mTaskStarterDownloadTaskHunter實例,所以這里會調(diào)用DownloadTaskHunterstart()方法,這個方法現(xiàn)在是在子線程中執(zhí)行的:

  @Override
    public void start() {
        //在前面我們已經(jīng)設(shè)置過標(biāo)識為toLaunchPool
        if (mStatus != FileDownloadStatus.toLaunchPool) {
            FileDownloadLog.w(this, "High concurrent cause, this task %d will not start," +
                            " because the of status isn't toLaunchPool: %d",
                    getId(), mStatus);
            return;
        }

        //獲取下載任務(wù)
        final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
        final BaseDownloadTask origin = runningTask.getOrigin();

        //這個類是用來檢查服務(wù)是否連接的,因為默認是跨進程的,所以這里會有跨進程的調(diào)用檢查是否
        //連接,如果沒有連接的話那么會bindService連接
        final ILostServiceConnectedHandler lostConnectedHandler = FileDownloader.getImpl().
                getLostConnectedHandler();
        try {

            //檢查服務(wù)有沒有連接,沒有連接就連接,如果是跨進程就啟動SeparateProcessService
            //服務(wù),不是跨進程就啟動SharedMainProcessService服務(wù)
            if (lostConnectedHandler.dispatchTaskStart(runningTask)) {
                return;
            }

            synchronized (mPauseLock) {
                if (mStatus != FileDownloadStatus.toLaunchPool) {
                    FileDownloadLog.w(this, "High concurrent cause, this task %d will not start," +
                                    " the status can't assign to toFileDownloadService, because the status" +
                                    " isn't toLaunchPool: %d",
                            getId(), mStatus);
                    return;
                }
                //將任務(wù)狀態(tài)設(shè)置為toFileDownloadService
                mStatus = FileDownloadStatus.toFileDownloadService;
            }

            //這里會通知FileDownloadMonitor的onTaskBegin說任務(wù)要開始下載了,同時會重新
            //組裝一下任務(wù)列表
            FileDownloadList.getImpl().add(runningTask);
            if (FileDownloadHelper.inspectAndInflowDownloaded(
                    origin.getId(), origin.getTargetFilePath(), origin.isForceReDownload(), true)
                    ) {
                // Will be removed when the complete message is received in #update
                return;
            }

           //啟動任務(wù)
            final boolean succeed = FileDownloadServiceProxy.getImpl().
                    start(
                            origin.getUrl(),
                            origin.getPath(),
                            origin.isPathAsDirectory(),
                            origin.getCallbackProgressTimes(), origin.getCallbackProgressMinInterval(),
                            origin.getAutoRetryTimes(),
                            origin.isForceReDownload(),
                            mTask.getHeader(),
                            origin.isWifiRequired());

          //如果任務(wù)是暫停的話那么這里就會執(zhí)行暫停操作
            if (mStatus == FileDownloadStatus.paused) {
                FileDownloadLog.w(this, "High concurrent cause, this task %d will be paused," +
                        "because of the status is paused, so the pause action must be applied", getId());
                if (succeed) {
                    FileDownloadServiceProxy.getImpl().pause(getId());
                }
                return;
            }

            if (!succeed) {//如果任務(wù)啟動失敗
                //noinspection StatementWithEmptyBody
                //如果啟動服務(wù)失敗則進入如下代碼
                if (!lostConnectedHandler.dispatchTaskStart(runningTask)) {
                    final MessageSnapshot snapshot = prepareErrorMessage(
                            new RuntimeException("Occur Unknown Error, when request to start" +
                                    " maybe some problem in binder, maybe the process was killed in " +
                                    "unexpected."));
                    //任務(wù)列表沒有當(dāng)前任務(wù)則添加
                    if (FileDownloadList.getImpl().isNotContains(runningTask)) {
                        lostConnectedHandler.taskWorkFine(runningTask);
                        FileDownloadList.getImpl().add(runningTask);
                    }
                    //有的話就刪除,說明執(zhí)行任務(wù)出錯了
                    FileDownloadList.getImpl().remove(runningTask, snapshot);

                } else {
                    // the FileDownload Service host process was killed when request stating and it
                    // will be restarted by LostServiceConnectedHandler.
                }
            } else {
                //任務(wù)執(zhí)行成功
                lostConnectedHandler.taskWorkFine(runningTask);
            }

        } catch (Throwable e) {
            e.printStackTrace();

            FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
        }
    }

上面的代碼可以看到我已經(jīng)注釋的非常清楚了,像服務(wù)的啟動,我這里就不會展開講了,因為挺簡單的。FileDownloadHelper#inspectAndInflowDownloaded()方法我們看到?jīng)]有注釋,有機會會拿出來講下,我們先來看看主線邏輯,我們來看看啟動任務(wù)做了什么工作FileDownloadServiceProxy#start()

 @Override
    public boolean start(String url, String path, boolean pathAsDirectory, int callbackProgressTimes,
                         int callbackProgressMinIntervalMillis,
                         int autoRetryTimes, boolean forceReDownload, FileDownloadHeader header,
                         boolean isWifiRequired) {
        return handler.start(url, path, pathAsDirectory, callbackProgressTimes,
                callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
                isWifiRequired);
    }

這里的handler是在FileDownloadServiceProxy的構(gòu)造函數(shù)里面初始化的,這里我們可以來看下:

  private FileDownloadServiceProxy() {
        handler = FileDownloadProperties.getImpl().PROCESS_NON_SEPARATE ?
                new FileDownloadServiceSharedTransmit() :
                new FileDownloadServiceUIGuard();
    }

我們看到這里會在FileDownloadProperties類中去獲取PROCESS_NON_SEPARATE 標(biāo)志。如果你去看FileDownloadProperties構(gòu)造函數(shù)就會知道,這些值是從filedownloader.properties這個文件配置的,這里還有很多其他的標(biāo)志,這里的PROCESS_NON_SEPARATE是為了標(biāo)識是否下載服務(wù)是在單獨的進程中的,我們這里就默認設(shè)置為在單獨進程中下載,在單獨的進程中執(zhí)行下載服務(wù)的好處是可以減少應(yīng)用進程占用的內(nèi)存,且使應(yīng)用更加穩(wěn)定。所以當(dāng)PROCESS_NON_SEPARATE為false的情況下,我們的handler會是FileDownloadServiceUIGuard類實例,所以這里的start()方法就會是FileDownloadServiceUIGuard#start()

    public boolean start(final String url, final String path, final boolean pathAsDirectory,
                         final int callbackProgressTimes,
                         final int callbackProgressMinIntervalMillis,
                         final int autoRetryTimes, final boolean forceReDownload,
                         final FileDownloadHeader header, final boolean isWifiRequired) {
        if (!isConnected()) {
            return DownloadServiceNotConnectedHelper.start(url, path, pathAsDirectory);
        }

        try {
            getService().start(url, path, pathAsDirectory, callbackProgressTimes,
                    callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
                    isWifiRequired);
        } catch (RemoteException e) {
            e.printStackTrace();

            return false;
        }

        return true;
    }

這里的getService()方法返回的是遠程的服務(wù)代理,因為這里是跨進程的,所以遠程的服務(wù)是SeparateProcessService,為什么說這個方法獲取到的是遠程的服務(wù)代理呢?因為是FileDownloadServiceUIGuard實現(xiàn)了ServiceConnection,所以在綁定遠程服務(wù)的時候會回調(diào)onServiceConnected()方法,我們在這里可以看到:

 @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        this.service = asInterface(service);

        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "onServiceConnected %s %s", name, this.service);
        }

        try {
            registerCallback(this.service, this.callback);
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        @SuppressWarnings("unchecked") final List<Runnable> runnableList =
                (List<Runnable>) connectedRunnableList.clone();
        connectedRunnableList.clear();
        for (Runnable runnable : runnableList) {
            runnable.run();
        }

        FileDownloadEventPool.getImpl().
                asyncPublishInNewThread(new DownloadServiceConnectChangedEvent(
                        DownloadServiceConnectChangedEvent.ConnectStatus.connected, serviceClass));

    }

可以看到第一句就是賦值service,所以我們直接看遠程服務(wù)SeparateProcessService#start()方法,這個SeparateProcessService是繼承FileDownloadService類的,它本身是一個空實現(xiàn),我們來看看FileDownloadService中的start()方法,熟悉AIDL的人都知道,這些實現(xiàn)都在Binder實體中實現(xiàn),我們來看下onBind方法:

 @Override
    public IBinder onBind(Intent intent) {
        return handler.onBind(intent);
    }

這里又調(diào)用了handleronBind方法,我們看下這里的handler是個什么東西?

 if (FileDownloadProperties.getImpl().PROCESS_NON_SEPARATE) {
            handler = new FDServiceSharedHandler(new WeakReference<>(this), manager);
        } else {
            handler = new FDServiceSeparateHandler(new WeakReference<>(this), manager);
        }

可以看到,我們這里PROCESS_NON_SEPARATE是為false的,所以我們的handlerFDServiceSeparateHandler實例。所以我們看FDServiceSeparateHandler#onBind()方法:

  @Override
    public IBinder onBind(Intent intent) {
        return this;
    }

public class FDServiceSeparateHandler extends IFileDownloadIPCService.Stub
        implements MessageSnapshotFlow.MessageReceiver, IFileDownloadServiceHandler {
}

我們看到這里onBind方法返回this,FDServiceSeparateHandler 實現(xiàn)了IFileDownloadIPCService.Stub,所以FDServiceSeparateHandler就是Binder實體,我們看這個類里面的start()方法:

 @Override
    public void start(String url, String path, boolean pathAsDirectory, int callbackProgressTimes,
                      int callbackProgressMinIntervalMillis, int autoRetryTimes, boolean forceReDownload,
                      FileDownloadHeader header, boolean isWifiRequired) throws RemoteException {
        downloadManager.start(url, path, pathAsDirectory, callbackProgressTimes,
                callbackProgressMinIntervalMillis, autoRetryTimes, forceReDownload, header,
                isWifiRequired);
    }

這里調(diào)用了downloadManager的start方法,這里的downloadManagerFileDownloadManager實例,所以程序會調(diào)用FileDownloadManager#start()方法。

4.FileDownloadManager start

這里就是真正開始啟動下載任務(wù)了,我們完整地看下代碼:

    // synchronize for safe: check downloading, check resume, update data, execute runnable
    public synchronized void start(final String url, final String path, final boolean pathAsDirectory,
                                   final int callbackProgressTimes,
                                   final int callbackProgressMinIntervalMillis,
                                   final int autoRetryTimes, final boolean forceReDownload,
                                   final FileDownloadHeader header, final boolean isWifiRequired) {
        if (FileDownloadLog.NEED_LOG) {
            FileDownloadLog.d(this, "request start the task with url(%s) path(%s) isDirectory(%B)",
                    url, path, pathAsDirectory);
        }

        //根據(jù)請求鏈接,文件存儲路徑,已經(jīng)一個boolean值來生成一次請求的唯一id
        final int id = FileDownloadUtils.generateId(url, path, pathAsDirectory);
        //從數(shù)據(jù)庫中獲取這個id對應(yīng)的請求,封裝在FileDownloadModel 中,因為有可能前面已經(jīng)有下載過了,只是由于不可抗性終止了
        FileDownloadModel model = mDatabase.find(id);

        List<ConnectionModel> dirConnectionModelList = null;

      //如果pathAsDirectory 為false也就是說是文件,而且獲取到的model為null,那么進入如下邏輯
        if (!pathAsDirectory && model == null) {
            // try dir data.
            //根據(jù)這個文件的上級目錄來生成一個唯一id
            final int dirCaseId = FileDownloadUtils.generateId(url, FileDownloadUtils.getParent(path),
                    true);
            //尋找這個id對應(yīng)的model
            model = mDatabase.find(dirCaseId);
            if (model != null && path.equals(model.getTargetFilePath())) {
                if (FileDownloadLog.NEED_LOG) {
                    FileDownloadLog.d(this, "task[%d] find model by dirCaseId[%d]", id, dirCaseId);
                }
                //獲取該id下的所有連接,即對應(yīng)的FileDownloadModel集合
                dirConnectionModelList = mDatabase.findConnectionModel(dirCaseId);
            }
        }

        if (FileDownloadHelper.inspectAndInflowDownloading(id, model, this, true)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "has already started download %d", id);
            }
            return;
        }

        //獲取文件路徑,沒有則生成一個
        final String targetFilePath = model != null ? model.getTargetFilePath() :
                FileDownloadUtils.getTargetFilePath(path, pathAsDirectory, null);
        if (FileDownloadHelper.inspectAndInflowDownloaded(id, targetFilePath, forceReDownload,
                true)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "has already completed downloading %d", id);
            }
            return;
        }

        //獲取文件下載了多少字節(jié),因為有可能之前下了一半斷掉了,然后獲取暫存文件路徑
        final long sofar = model != null ? model.getSoFar() : 0;
        final String tempFilePath = model != null ? model.getTempFilePath() :
                FileDownloadUtils.getTempPath(targetFilePath);
        if (FileDownloadHelper.inspectAndInflowConflictPath(id, sofar, tempFilePath, targetFilePath,
                this)) {
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.d(this, "there is an another task with the same target-file-path %d %s",
                        id, targetFilePath);
                // because of the file is dirty for this task.
                if (model != null) {
                    mDatabase.remove(id);
                    mDatabase.removeConnections(id);
                }
            }
            return;
        }

        // real start
        // - create model
        //創(chuàng)建連接model,如果數(shù)據(jù)庫已經(jīng)存在這個model,而且狀態(tài)是下面狀態(tài)中的一種,
       //那么說明真的是意外退出了,則根據(jù)下面情況看要不要更新數(shù)據(jù)庫,否則就創(chuàng)建一個新的model請求
        boolean needUpdate2DB;
        if (model != null &&
                (model.getStatus() == FileDownloadStatus.paused ||
                        model.getStatus() == FileDownloadStatus.error ||
                        model.getStatus() == FileDownloadStatus.pending ||
                        model.getStatus() == FileDownloadStatus.started ||
                        model.getStatus() == FileDownloadStatus.connected) // FileDownloadRunnable invoke
            // #isBreakpointAvailable to determine whether it is really invalid.
                ) {
            if (model.getId() != id) {
                // in try dir case.
                mDatabase.remove(model.getId());
                mDatabase.removeConnections(model.getId());

                model.setId(id);
                model.setPath(path, pathAsDirectory);
                if (dirConnectionModelList != null) {
                    for (ConnectionModel connectionModel : dirConnectionModelList) {
                        connectionModel.setId(id);
                        mDatabase.insertConnectionModel(connectionModel);
                    }
                }

                needUpdate2DB = true;
            } else {
                if (!TextUtils.equals(url, model.getUrl())) {
                    // for cover the case of reusing the downloaded processing with the different url( using with idGenerator ).
                    model.setUrl(url);
                    needUpdate2DB = true;
                } else {
                    needUpdate2DB = false;
                }
            }
        } else {
            if (model == null) {
                model = new FileDownloadModel();
            }
            model.setUrl(url);
            model.setPath(path, pathAsDirectory);

            model.setId(id);
            model.setSoFar(0);
            model.setTotal(0);
            model.setStatus(FileDownloadStatus.pending);
            model.setConnectionCount(1);
            needUpdate2DB = true;
        }

        // - update model to db
        if (needUpdate2DB) {
            mDatabase.update(model);
        }

        final DownloadLaunchRunnable.Builder builder = new DownloadLaunchRunnable.Builder();
        //利用建造者模式創(chuàng)建DownloadLaunchRunnable 對象
        final DownloadLaunchRunnable runnable =
                builder.setModel(model)
                        .setHeader(header)
                        .setThreadPoolMonitor(this)
                        .setMinIntervalMillis(callbackProgressMinIntervalMillis)
                        .setCallbackProgressMaxCount(callbackProgressTimes)
                        .setForceReDownload(forceReDownload)
                        .setWifiRequired(isWifiRequired)
                        .setMaxRetryTimes(autoRetryTimes)
                        .build();

        // - execute
        //執(zhí)行這個Runnable
        mThreadPool.execute(runnable);
    }

我們看到上面的方法主要作用就是根據(jù)url,path與是否是目錄來標(biāo)識一個請求,如果符合意外退出的情況,那么會對SoFar(文件已經(jīng)下載了多少)這個值進行恢復(fù)。最后重新賦值給DownloadLaunchRunnable對象,進行執(zhí)行,DownloadLaunchRunnable是一個Runnable對象,所以會執(zhí)行他的run()方法:

 @Override
    public void run() {
        try {
          //設(shè)置線程優(yōu)先級為后臺,這樣當(dāng)多個線程并發(fā)后很多無關(guān)緊要的線程分配的CPU時間將會減少,有利于主線程的處理
            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

            // status checkout
          //檢查狀態(tài)
            if (model.getStatus() != FileDownloadStatus.pending) {
                if (model.getStatus() == FileDownloadStatus.paused) {
                    if (FileDownloadLog.NEED_LOG) {
                        FileDownloadLog.d(this, "High concurrent cause, start runnable but " +
                                "already paused %d", model.getId());
                    }

                } else {
                    onError(new RuntimeException(
                            FileDownloadUtils.formatString("Task[%d] can't start the download" +
                                            " runnable, because its status is %d not %d",
                                    model.getId(), model.getStatus(), FileDownloadStatus.pending)));
                }
                return;
            }

            //更新model的狀態(tài)為啟動狀態(tài)
            if (!paused) {
                statusCallback.onStartThread();
            }

            do {
              //如果是暫停狀態(tài)則返回不執(zhí)行,如日志所說,高并發(fā)可能導(dǎo)致這個結(jié)果
                if (paused) {
                    if (FileDownloadLog.NEED_LOG) {
                        FileDownloadLog.d(this, "High concurrent cause, start runnable but " +
                                "already paused %d", model.getId());
                    }
                    return;
                }

                FileDownloadConnection connection = null;
                try {


                    // 1. connect
                    //檢查權(quán)限和是否需要wifi才能下載條件
                    checkupBeforeConnect();

                    // the first connection is for: 1. etag verify; 2. first connect.
                    final List<ConnectionModel> connectionOnDBList = database.findConnectionModel(model.getId());
                    //檢查是否恢復(fù)之前的連接,如果是多線程連接且不支持seek的情況是不支持
                    //斷點恢復(fù)的,如果支持斷點恢復(fù),在多線程連接的情況下會把每個連接下載了
                    //多少計算賦值給model的SoFar,
                    final ConnectionProfile connectionProfile = buildFirstConnectProfile(connectionOnDBList);
                    final ConnectTask.Builder build = new ConnectTask.Builder();
                    //創(chuàng)建新的連接
                    final ConnectTask firstConnectionTask = build.setDownloadId(model.getId())
                            .setUrl(model.getUrl())
                            .setEtag(model.getETag())
                            .setHeader(userRequestHeader)
                            .setConnectionProfile(connectionProfile)
                            .build();
                    //調(diào)用FileDownloadUrlConnection進行連接請求,獲取文件長度
                    connection = firstConnectionTask.connect();
                    //會根據(jù)ETag來判斷遠端的文件是否有改變,改變的話就是從頭開始下載,不然就獲取文件總長度
                    handleFirstConnected(firstConnectionTask.getRequestHeader(),
                            firstConnectionTask, connection);

                    if (paused) {
                        model.setStatus(FileDownloadStatus.paused);
                        return;
                    }

                    // 2. fetch
                    //檢查是否有另外一個url請求任務(wù)跟這個任務(wù)目錄存儲路徑一樣,
                    //一樣的話判斷這個url下載的文件是否存在,如果其他進程正在下載這個文件,
                   //那么這里請求就停止,如果其他進程下載文件已經(jīng)停止了,那么這里就從斷點
                    //進行恢復(fù)下載
                    checkupBeforeFetch();
                    final long totalLength = model.getTotal();
                    // pre-allocate if need.
                    //預(yù)先分配文件的大小
                    handlePreAllocate(totalLength, model.getTempFilePath());

                    final int connectionCount;
                    // start fetching
                    //是否支持多線程即多個連接下載
                    if (isMultiConnectionAvailable()) {
                        if (isResumeAvailableOnDB) {
                            connectionCount = model.getConnectionCount();
                        } else {
                            connectionCount = CustomComponentHolder.getImpl()
                                    .determineConnectionCount(model.getId(), model.getUrl(), model.getPath(), totalLength);
                        }
                    } else {
                        connectionCount = 1;
                    }

                    if (connectionCount <= 0) {
                        throw new IllegalAccessException(FileDownloadUtils
                                .formatString("invalid connection count %d, the connection count" +
                                        " must be larger than 0", connection));
                    }

                    if (paused) {
                        model.setStatus(FileDownloadStatus.paused);
                        return;
                    }

                    isSingleConnection = connectionCount == 1;
                    if (isSingleConnection) {
                        // single connection
                        //單個子線程下載
                        fetchWithSingleConnection(firstConnectionTask.getProfile(), connection);
                    } else {
                        if (connection != null) {
                            connection.ending();
                            connection = null;
                        }
                        // multiple connection
                        statusCallback.onMultiConnection();
                        if (isResumeAvailableOnDB) {
                            //恢復(fù)多線程下載
                            fetchWithMultipleConnectionFromResume(connectionCount, connectionOnDBList);
                        } else {
                             //從頭開始多線程下載
                            fetchWithMultipleConnectionFromBeginning(totalLength, connectionCount);
                        }
                    }

                } catch (IOException | IllegalAccessException | InterruptedException | IllegalArgumentException | FileDownloadGiveUpRetryException e) {
                    if (isRetry(e)) {
                        onRetry(e, 0);
                        continue;
                    } else {
                        onError(e);
                    }
                } catch (DiscardSafely discardSafely) {
                    return;
                } catch (RetryDirectly retryDirectly) {
                    model.setStatus(FileDownloadStatus.retry);
                    continue;
                } finally {
                    if (connection != null) connection.ending();
                }

                break;
            } while (true);
        } finally {
            statusCallback.discardAllMessage();

            if (paused) {
                statusCallback.onPausedDirectly();
            } else if (error) {
                statusCallback.onErrorDirectly(errorException);
            } else {
                try {
                    statusCallback.onCompletedDirectly();
                } catch (IOException e) {
                    statusCallback.onErrorDirectly(e);
                }
            }

            alive.set(false);
        }
    }

我們看到這個方法就是下載的主要方法了,流程注釋已經(jīng)寫得很清楚了,就是里面沒有展開寫,不然本篇文章的篇幅要很長了,到這里我們已經(jīng)講完請求的大體流程了,這里明確下一個請求對應(yīng)一個FileDownloadModel實體,每個請求下面又可以有多個ConnectionModel實體,每個ConnectionModel實體對應(yīng)一條請求線程。當(dāng)然這里面的FileDownloadHelper#inspectAndInflowDownloaded()方法沒有講,如果大家看不懂可以給我留言,我會解答。

總結(jié):這個下載框架整體來說思維還是不錯的,希望里面的編程思想會有幫助到大家,在以后自己編寫框架的過程中能有所借鑒,最后祝大家源碼之路愉快哈。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,710評論 18 399
  • AFHTTPRequestOperationManager 網(wǎng)絡(luò)傳輸協(xié)議UDP、TCP、Http、Socket、X...
    Carden閱讀 4,368評論 0 12
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,482評論 0 17
  • 1.螢火蟲之墓 珍惜眼前,珍惜當(dāng)下,誰知道明天和意外,哪一個先來 ——《螢火蟲之墓》 推薦理由:催淚神器,戰(zhàn)爭的殘...
    林強自媒體閱讀 1,630評論 7 73