流行框架源码解析(17)-英语流利说文件下载器源码解析

作者: ZJ_Rocky | 来源:发表于2017-12-01 17:45 被阅读471次

    主目录见:Android高级进阶知识(这是总目录索引)
    下载器Github地址:FileDownloader

     文件下载在Android的开发中应该可以说是都会用到,所以一个完善的好的下载工具应该是不可或缺的,第一次看这个框架还是感觉类比较多的,应该是作者为了类的职责划分更加明确吧,但是个人感觉适当为好。正如介绍所说,这个框架是多任务,多线程,支持断点恢复,高并发,简单易用且支持多进程的。确实是个不错的框架,值得我们来读读代码,不过我也只是看过一下午,如果有理解不到位的大家谅解一下。

    一.目标

    这篇文章应该来说也是比较实用的,而且本库的wiki应该也有部分的介绍,但是我们今天的主要目标如下:
    1.学习该库中优秀的思维;
    2.懂得自己开发或者修改和改进该库。

    二.源码解析

    首先当你拿到一个库不知道从哪开始的时候,往往就是从使用上入手,所以我们先来看看怎么使用该库,因为使用方式有几种,我们挑其中一种来说明:
    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.开始下载(这里我挑一个使用任务队列来下载的方式)

     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避免做过多的工作导致启动速度变慢。这个方法会返回一个InitCustomMaker类的实例,所以程序会调用这个类connectionCreator()方法:

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

    这个方法就是将一个连接创建器赋值给InitCustomMaker类中的变量,返回this是为了链式操作需要,因为InitCustomMaker这里还有很多可以自定义的方法。这样我们的初始化工作就完成了,我们来看我们的开始下载部分。

    2.FileDownloadQueueSet的设置

    这个类是用来配置下载任务和启动下载任务的,首先我们看到构造函数里面给他设置了一个FileDownloadListener实例用于回调。然后会在串行下载或者并行下载的时候会给他传一个任务集合,我们首先来看看任务集合的创建和添加。任务的创建是在FileDownloader#create中创建的:

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

    通过这个我们可以知道,一个下载任务对应一个DownloadTask,我们看下它的构造函数:

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

    这里除了给DownloadTask的mUrl赋值之外,还实例化了一个DownloadTaskHunter实例,我们看到这里参数将DownloadTask实例传给了它的构造函数,然后传了mPauseLock用来做线程锁操作。我们看下DownloadTaskHunter构造函数:

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

    可以看到除了传进来的两个参数赋值之外,还是实例化了DownloadSpeedMonitor类,这个类主要是用来监测下载进度的。同时还实例化了FileDownloadMessenger类,这个类是用来给回调接口FileDownloadListener发送消息的。这样我们大致看了下载任务DownloadTask的创建过程。

    接下来就是FileDownloadQueueSet类中一些方法的设置,例如我们这里使用到的disableCallbackProgressTimes()方法,这个方法是设置了的话,那么我们就监听不到一个任务的下载进度了,只有任务下载完成的回调。与这个方法对应的还有setCallbackProgressTimes()设置更新进度回调次数,setCallbackProgressMinInterval()设置了更新进度的最小间隔时间。setAutoRetryTimes()方法是设置下载失败重试的次数。然后会把上面创建的任务集合赋值给FileDownloadQueueSet类,这里调用的方法是downloadSequentially()downloadTogether(),这两个方法唯一的不同就是会设置标志位isSerial,来说明是串行下载还是并行下载。最后最重要的当然就是start()方法了,这里就真正启动开始下载了。

    3.FileDownloadQueueSet start

     public void start() {
            //遍历任务集合
            for (BaseDownloadTask task : tasks) {
                //设置任务的回调监听为传进来的FileDownloadListener实例
                task.setListener(target);
    
                if (autoRetryTimes != null) {
                    //设置失败重试次数
                    task.setAutoRetryTimes(autoRetryTimes);
                }
    
                if (syncCallback != null) {
                  //设置是否进行同步回调
                    task.setSyncCallback(syncCallback);
                }
    
                if (isForceReDownload != null) {
                   //是否强制重新下载
                    task.setForceReDownload(isForceReDownload);
                }
    
                if (callbackProgressTimes != null) {
                    //设置进度回调次数
                    task.setCallbackProgressTimes(callbackProgressTimes);
                }
    
                if (callbackProgressMinIntervalMillis != null) {
                  //设置进度回调最小间隔时间
                    task.setCallbackProgressMinInterval(callbackProgressMinIntervalMillis);
                }
    
                if (tag != null) {
                    task.setTag(tag);
                }
    
                if (taskFinishListenerList != null) {
                    //任务结束监听器,只有在非UI现场中调用
                    for (BaseDownloadTask.FinishListener finishListener : taskFinishListenerList) {
                        task.addFinishListener(finishListener);
                    }
                }
    
                if (this.directory != null) {
                    //设置下载下来的文件存放的路径
                    task.setPath(this.directory, true);
                }
    
                if (this.isWifiRequired != null) {
                    //是否需要只在wifi情况下才下载
                    task.setWifiRequired(this.isWifiRequired);
                }
    
               //这个方法主要是标识下是在队列中的任务,而且会将任务添加到FileDownloadList的任务集合中
                task.asInQueueTask().enqueue();
            }
    
          //启动下载任务
            FileDownloader.getImpl().start(target, isSerial);
        }
    

    我们看到这个方法主要是给任务DownloadTask设置一些参数值,然后将任务添加到FileDownloadList中的集合里。然后调用FileDownloader#start()开始下载任务:

      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 标志我们在前面已经设置了,我们这里就走并行下载这条道吧,所以isSerial 为false,所以这里会调用getQueuesHandler().startQueueParallel()方法来启动并行下载任务,我们这里跟踪到QueuesHandler#startQueueParallel()方法:

      @Override
        public boolean startQueueParallel(FileDownloadListener listener) {
            final int attachKey = listener.hashCode();
    
            //这里主要防止在添加了下载任务之后又有新的任务到达,所以这里会重新组装任务队列
            final List<BaseDownloadTask.IRunningTask> list = FileDownloadList.getImpl().
                    assembleTasksToStart(attachKey, listener);
    
            //这里会验证是否任务队列里面有任务,然后会调用全局监听器的onRequestStart(),如果有打点/统计等需求可以考虑设置这个下载监听器
            if (onAssembledTasksToStart(attachKey, list, listener, false)) {
                return false;
            }
    
           //遍历任务集合,然后分别启动
            for (BaseDownloadTask.IRunningTask task : list) {
                task.startTaskByQueue();
            }
    
            return true;
        }
    

    我们看到前面两步就是重新组装一下任务队列,然后我们会调用任务task中的startTaskByQueue()方法进行启动任务:

     @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();
        }
    
    

    前面是判断这个任务是否在执行或者占用,最后会调用DownloadTaskHunter#intoLaunchPool()方法:

      @Override
        public void intoLaunchPool() {
            synchronized (mPauseLock) {
                //如果任务是在空闲状态则现在设置状态为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;
            }
    
           //获取下载任务
            final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
            final BaseDownloadTask origin = runningTask.getOrigin();
    
            //如果设置了全局监听就调用他的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());
            }
    
          //设置任务准备就绪
            boolean ready = true;
    
            try {
                //前面给任务设置了path,也就是下载文件存放路径,这里就是创建这个路径
                prepare();
            } catch (Throwable e) {
                ready = false;
    
                FileDownloadList.getImpl().add(runningTask);
                FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
            }
    
            if (ready) {
                //调用FileDownloadTaskLauncher的launch启动任务
                FileDownloadTaskLauncher.getImpl().launch(this);
            }
    
            if (FileDownloadLog.NEED_LOG) {
                FileDownloadLog.v(this, "the task[%d] has been into the launch pool.", getId());
            }
        }
    

    前面的步骤已经注释的很清楚了,最后我们看到会调用FileDownloadTaskLauncher#launch()方法来启动任务,传的参数就是本类DownloadTaskHunter实例,我们看下launch()方法:

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

    这里的mLaunchTaskPool是一个LaunchTaskPool类实例,所以会调用LaunchTaskPool#asyncExecute()方法:

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

    mPool是一个自定义的线程池ThreadPoolExecutor实例,所以调用execute会执行LaunchTaskRunnable#run()方法:

           LaunchTaskRunnable(final ITaskHunter.IStarter taskStarter) {
                this.mTaskStarter = taskStarter;
                this.mExpired = false;
            }
    
            @Override
            public void run() {
                if (mExpired) {
                    return;
                }
    
                mTaskStarter.start();
            }
    

    我们看到run方法中会调用mTaskStarterstart()方法,从上面我们可以知道mTaskStarterDownloadTaskHunter实例,所以这里会调用DownloadTaskHunterstart()方法,这个方法现在是在子线程中执行的:

      @Override
        public void start() {
            //在前面我们已经设置过标识为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;
            }
    
            //获取下载任务
            final BaseDownloadTask.IRunningTask runningTask = mTask.getRunningTask();
            final BaseDownloadTask origin = runningTask.getOrigin();
    
            //这个类是用来检查服务是否连接的,因为默认是跨进程的,所以这里会有跨进程的调用检查是否
            //连接,如果没有连接的话那么会bindService连接
            final ILostServiceConnectedHandler lostConnectedHandler = FileDownloader.getImpl().
                    getLostConnectedHandler();
            try {
    
                //检查服务有没有连接,没有连接就连接,如果是跨进程就启动SeparateProcessService
                //服务,不是跨进程就启动SharedMainProcessService服务
                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;
                    }
                    //将任务状态设置为toFileDownloadService
                    mStatus = FileDownloadStatus.toFileDownloadService;
                }
    
                //这里会通知FileDownloadMonitor的onTaskBegin说任务要开始下载了,同时会重新
                //组装一下任务列表
                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;
                }
    
               //启动任务
                final boolean succeed = FileDownloadServiceProxy.getImpl().
                        start(
                                origin.getUrl(),
                                origin.getPath(),
                                origin.isPathAsDirectory(),
                                origin.getCallbackProgressTimes(), origin.getCallbackProgressMinInterval(),
                                origin.getAutoRetryTimes(),
                                origin.isForceReDownload(),
                                mTask.getHeader(),
                                origin.isWifiRequired());
    
              //如果任务是暂停的话那么这里就会执行暂停操作
                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) {//如果任务启动失败
                    //noinspection StatementWithEmptyBody
                    //如果启动服务失败则进入如下代码
                    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."));
                        //任务列表没有当前任务则添加
                        if (FileDownloadList.getImpl().isNotContains(runningTask)) {
                            lostConnectedHandler.taskWorkFine(runningTask);
                            FileDownloadList.getImpl().add(runningTask);
                        }
                        //有的话就删除,说明执行任务出错了
                        FileDownloadList.getImpl().remove(runningTask, snapshot);
    
                    } else {
                        // the FileDownload Service host process was killed when request stating and it
                        // will be restarted by LostServiceConnectedHandler.
                    }
                } else {
                    //任务执行成功
                    lostConnectedHandler.taskWorkFine(runningTask);
                }
    
            } catch (Throwable e) {
                e.printStackTrace();
    
                FileDownloadList.getImpl().remove(runningTask, prepareErrorMessage(e));
            }
        }
    

    上面的代码可以看到我已经注释的非常清楚了,像服务的启动,我这里就不会展开讲了,因为挺简单的。FileDownloadHelper#inspectAndInflowDownloaded()方法我们看到没有注释,有机会会拿出来讲下,我们先来看看主线逻辑,我们来看看启动任务做了什么工作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的构造函数里面初始化的,这里我们可以来看下:

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

    我们看到这里会在FileDownloadProperties类中去获取PROCESS_NON_SEPARATE 标志。如果你去看FileDownloadProperties构造函数就会知道,这些值是从filedownloader.properties这个文件配置的,这里还有很多其他的标志,这里的PROCESS_NON_SEPARATE是为了标识是否下载服务是在单独的进程中的,我们这里就默认设置为在单独进程中下载,在单独的进程中执行下载服务的好处是可以减少应用进程占用的内存,且使应用更加稳定。所以当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()方法返回的是远程的服务代理,因为这里是跨进程的,所以远程的服务是SeparateProcessService,为什么说这个方法获取到的是远程的服务代理呢?因为是FileDownloadServiceUIGuard实现了ServiceConnection,所以在绑定远程服务的时候会回调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,所以我们直接看远程服务SeparateProcessService#start()方法,这个SeparateProcessService是继承FileDownloadService类的,它本身是一个空实现,我们来看看FileDownloadService中的start()方法,熟悉AIDL的人都知道,这些实现都在Binder实体中实现,我们来看下onBind方法:

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

    这里又调用了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 实现了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);
        }
    

    这里调用了downloadManager的start方法,这里的downloadManagerFileDownloadManager实例,所以程序会调用FileDownloadManager#start()方法。

    4.FileDownloadManager start

    这里就是真正开始启动下载任务了,我们完整地看下代码:

        // 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);
            }
    
            //根据请求链接,文件存储路径,已经一个boolean值来生成一次请求的唯一id
            final int id = FileDownloadUtils.generateId(url, path, pathAsDirectory);
            //从数据库中获取这个id对应的请求,封装在FileDownloadModel 中,因为有可能前面已经有下载过了,只是由于不可抗性终止了
            FileDownloadModel model = mDatabase.find(id);
    
            List<ConnectionModel> dirConnectionModelList = null;
    
          //如果pathAsDirectory 为false也就是说是文件,而且获取到的model为null,那么进入如下逻辑
            if (!pathAsDirectory && model == null) {
                // try dir data.
                //根据这个文件的上级目录来生成一个唯一id
                final int dirCaseId = FileDownloadUtils.generateId(url, FileDownloadUtils.getParent(path),
                        true);
                //寻找这个id对应的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下的所有连接,即对应的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;
            }
    
            //获取文件下载了多少字节,因为有可能之前下了一半断掉了,然后获取暂存文件路径
            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
            //创建连接model,如果数据库已经存在这个model,而且状态是下面状态中的一种,
           //那么说明真的是意外退出了,则根据下面情况看要不要更新数据库,否则就创建一个新的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();
            //利用建造者模式创建DownloadLaunchRunnable 对象
            final DownloadLaunchRunnable runnable =
                    builder.setModel(model)
                            .setHeader(header)
                            .setThreadPoolMonitor(this)
                            .setMinIntervalMillis(callbackProgressMinIntervalMillis)
                            .setCallbackProgressMaxCount(callbackProgressTimes)
                            .setForceReDownload(forceReDownload)
                            .setWifiRequired(isWifiRequired)
                            .setMaxRetryTimes(autoRetryTimes)
                            .build();
    
            // - execute
            //执行这个Runnable
            mThreadPool.execute(runnable);
        }
    

    我们看到上面的方法主要作用就是根据url,path与是否是目录来标识一个请求,如果符合意外退出的情况,那么会对SoFar(文件已经下载了多少)这个值进行恢复。最后重新赋值给DownloadLaunchRunnable对象,进行执行,DownloadLaunchRunnable是一个Runnable对象,所以会执行他的run()方法:

     @Override
        public void run() {
            try {
              //设置线程优先级为后台,这样当多个线程并发后很多无关紧要的线程分配的CPU时间将会减少,有利于主线程的处理
                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    
                // status checkout
              //检查状态
                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的状态为启动状态
                if (!paused) {
                    statusCallback.onStartThread();
                }
    
                do {
                  //如果是暂停状态则返回不执行,如日志所说,高并发可能导致这个结果
                    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
                        //检查权限和是否需要wifi才能下载条件
                        checkupBeforeConnect();
    
                        // the first connection is for: 1. etag verify; 2. first connect.
                        final List<ConnectionModel> connectionOnDBList = database.findConnectionModel(model.getId());
                        //检查是否恢复之前的连接,如果是多线程连接且不支持seek的情况是不支持
                        //断点恢复的,如果支持断点恢复,在多线程连接的情况下会把每个连接下载了
                        //多少计算赋值给model的SoFar,
                        final ConnectionProfile connectionProfile = buildFirstConnectProfile(connectionOnDBList);
                        final ConnectTask.Builder build = new ConnectTask.Builder();
                        //创建新的连接
                        final ConnectTask firstConnectionTask = build.setDownloadId(model.getId())
                                .setUrl(model.getUrl())
                                .setEtag(model.getETag())
                                .setHeader(userRequestHeader)
                                .setConnectionProfile(connectionProfile)
                                .build();
                        //调用FileDownloadUrlConnection进行连接请求,获取文件长度
                        connection = firstConnectionTask.connect();
                        //会根据ETag来判断远端的文件是否有改变,改变的话就是从头开始下载,不然就获取文件总长度
                        handleFirstConnected(firstConnectionTask.getRequestHeader(),
                                firstConnectionTask, connection);
    
                        if (paused) {
                            model.setStatus(FileDownloadStatus.paused);
                            return;
                        }
    
                        // 2. fetch
                        //检查是否有另外一个url请求任务跟这个任务目录存储路径一样,
                        //一样的话判断这个url下载的文件是否存在,如果其他进程正在下载这个文件,
                       //那么这里请求就停止,如果其他进程下载文件已经停止了,那么这里就从断点
                        //进行恢复下载
                        checkupBeforeFetch();
                        final long totalLength = model.getTotal();
                        // pre-allocate if need.
                        //预先分配文件的大小
                        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) {
                                //恢复多线程下载
                                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);
            }
        }
    

    我们看到这个方法就是下载的主要方法了,流程注释已经写得很清楚了,就是里面没有展开写,不然本篇文章的篇幅要很长了,到这里我们已经讲完请求的大体流程了,这里明确下一个请求对应一个FileDownloadModel实体,每个请求下面又可以有多个ConnectionModel实体,每个ConnectionModel实体对应一条请求线程。当然这里面的FileDownloadHelper#inspectAndInflowDownloaded()方法没有讲,如果大家看不懂可以给我留言,我会解答。

    总结:这个下载框架整体来说思维还是不错的,希望里面的编程思想会有帮助到大家,在以后自己编写框架的过程中能有所借鉴,最后祝大家源码之路愉快哈。

    相关文章

      网友评论

      • cwzqf:看不懂MessageSnapshot的作用,是回调onStart,onProgress那几个方法吗
      • 339ce42787a0:最近打算实现腾讯视频下载的功能,正好看到这篇文章,想问一下有demo嘛😊
        339ce42787a0:@ZJ_Rocky 楼主好,试了demo,如果下载全部怎么能获取所有下载文件的进度,并且刷新ui呢🤔
        339ce42787a0:@ZJ_Rocky 好的,我去看看,谢谢啦
        ZJ_Rocky:@厚德载物_e546 github下下来有

      本文标题:流行框架源码解析(17)-英语流利说文件下载器源码解析

      本文链接:https://www.haomeiwen.com/subject/cxyibxtx.html