3. OkDownload源码分析——DownloadCall类分析

291 阅读18分钟

Author: istyras
Date: 2024-10-14
Update: 2024-10-14


本文是对 OkDownload源码分析 的第二篇。

上一篇分析,分析到 DownloadTask.enqueue/execute 都是调用了 DownloadDispatcher.enqueue/execute 方法,而在其内部的处理中,添加/启动任务,均创建了 DownloadCall 对象,所以我们分析这个类。

1. DownloadCall 类的定义分析

DownloadCall 源码看到,其继承了 NamedRunnable,而 NamedRunnable 继承 Runnable 接口。

看一下 NamedRunnable 的实现

public abstract class NamedRunnable implements Runnable {

    protected final String name;

    public NamedRunnable(String name) {
        this.name = name;
    }

    @Override
    public final void run() {
        String oldName = Thread.currentThread().getName();
        Thread.currentThread().setName(name);
        try {
            execute();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            interrupted(e);
        } finally {
            Thread.currentThread().setName(oldName);
            finished();
        }
    }

    protected abstract void execute() throws InterruptedException;

    protected abstract void interrupted(InterruptedException e);

    protected abstract void finished();
}

NamedRunnable 的实现中可以看到 run 方法内真正触发流程的执行是 execute 接口,所以我们对 DownloadCall 类的流程分析,应该要从 execute 接口实现开始。

2. DownloadCall.create 方法分析

根据上一篇对 DownloadTask 最后的分析发现,触发 DownloadCall 的方式为调用了 DownloadCall.create 方法创建了 DownloadCall 对象。


    public static DownloadCall create(DownloadTask task, boolean asyncExecuted,
                                      @NonNull DownloadStore store) {
        return new DownloadCall(task, asyncExecuted, store);
    }

    private DownloadCall(DownloadTask task, boolean asyncExecuted, @NonNull DownloadStore store) {
        this(task, asyncExecuted, new ArrayList<DownloadChain>(), store);
    }

    DownloadCall(DownloadTask task, boolean asyncExecuted,
                 @NonNull ArrayList<DownloadChain> runningBlockList,
                 @NonNull DownloadStore store) {
        super("download call: " + task.getId());
        this.task = task;
        this.asyncExecuted = asyncExecuted;
        this.blockChainList = runningBlockList;
        this.store = store;
    }

DownloadCall 对象是通过 create 方法进行创建的,需要3个参数:

  • task: DownloadTask
  • asyncExecuted: 用于区分本对象运行的时候放入在async队列还是在sync队列中
  • store: 用于存储task信息的存储实现

3. DownloadCall.execute 方法分析

一个下载任务,就从 DownloadCall.execute 开始了它的艰辛历程。

    /**
     * 下载任务真正开始执行的位置
     *  DownloadDispatcher.enqueueIgnorePriority 调用 getExecutorService().execute(call); 触发进来
     * 或者是 execute(DownloadTask) 中的 DownloadCall.run 方法触发进来。
     */
    @Override
    public void execute() throws InterruptedException {
        currentThread = Thread.currentThread();

        boolean retry;
        int retryCount = 0;

        // ready param
        final OkDownload okDownload = OkDownload.with();
        final ProcessFileStrategy fileStrategy = okDownload.processFileStrategy();

        // inspect task start
        // 分发下载任务开始的通知
        inspectTaskStart();
        do {
            // url 校验
            // 0. check basic param before start
            if (task.getUrl().length() <= 0) {
                this.cache = new DownloadCache.PreError(
                        new IOException("unexpected url: " + task.getUrl()));
                break;
            }

            // 判断一下任务是否已经被取消
            if (canceled) break;

            // 1. create basic info if not exist
            @NonNull final BreakpointInfo info;
            try {
                // 根据 任务ID 获取缓存中的断点续传的数据信息,如果没有就新建一个
                // 取:组件初始化的时候(BreakpointStoreOnSQLite),会使用 helper.loadToCache() 加载出所有的BreakpointInfo 信息
                // 存:???
                BreakpointInfo infoOnStore = store.get(task.getId());
                if (infoOnStore == null) {
                    // 新建的 BreakpointInfo 信息,只有简单的信息,还没有分块信息
                    info = store.createAndInsert(task);
                } else {
                    // 如果是从 数据库中取出来的,那么有可能有分块信息
                    info = infoOnStore;
                }
                // 这里将 断点续传的信息 存到 DownloadTask 中,后续操作的时候,
                // 能够从 Task 中再取出 BreakpointInfo 信息来操作
                setInfoToTask(info);
            } catch (IOException e) {
                this.cache = new DownloadCache.PreError(e);
                break;
            }
            if (canceled) break;

            // ready cache.
            // 创建DownloadCache;
            // DownloadCache 是一个中间类,用于存放下载过程中产生的一些状态,
            // 以及在完成下载任务过程中的一些操作对象存储,就是将这些信息包装在一起方便传递与管理
            @NonNull final DownloadCache cache = createCache(info);
            this.cache = cache;

            // 执行预先检测操作,同时可以获取到文件大小、重定向信息、处理文件分块等,
            // 同时可以判断一下的网络状态异常抛出异常就结束了
            // 2. remote check.
            final BreakpointRemoteCheck remoteCheck = createRemoteCheck(info);
            try {
                remoteCheck.check();
            } catch (IOException e) {
                cache.catchException(e);
                break;
            }
            // 这里从上面的 BreakpointRemoteCheck 中,获取到重定向信息,设置到 task 中,
            // 然后从 task 中取出来,设置到 cache 中。
            cache.setRedirectLocation(task.getRedirectLocation());

            // 确认文件路径后,等待文件锁释放。具体实现是通过 fileStrategy.getFileLock().waitForRelease 
            // 方法等待指定文件路径(task.getFile().getAbsolutePath())的文件锁被释放。
            // 确保并发操作时的文件安全
            // 3. waiting for file lock release after file path is confirmed.
            fileStrategy.getFileLock().waitForRelease(task.getFile().getAbsolutePath());

            // 4. reuse another info if another info is idle and available for reuse.
            OkDownload.with().downloadStrategy()
                    .inspectAnotherSameInfo(task, info, remoteCheck.getInstanceLength());

            try {
                // 使用上面的 BreakpointRemoteCheck 信息,判断是否可以断点续传
                if (remoteCheck.isResumable()) {
                    // 5. local check
                    // 可以断点续传,还需要对本地文件进行校验
                    final BreakpointLocalCheck localCheck = createLocalCheck(info,
                            remoteCheck.getInstanceLength());
                    localCheck.check();
                    // 如果 localCheck.check 校验后,是 dirty 的,那么就重新下载
                    if (localCheck.isDirty()) {
                        Util.d(TAG, "breakpoint invalid: download from beginning because of "
                                + "local check is dirty " + task.getId() + " " + localCheck);
                        // 6. assemble block data
                        // 丢弃(删除)task对应的本地文件,准备重新下载
                        fileStrategy.discardProcess(task);
                        // 重新组装断点续传的分块数据
                        assembleBlockAndCallbackFromBeginning(info, remoteCheck,
                                localCheck.getCauseOrThrow());
                    } else {
                        // 如果 localCheck.check 校验后,不是 dirty 的,那么就直接从断点续传的位置开始下载
                        // 这里执行一下相关的回调分发
                        okDownload.callbackDispatcher().dispatch()
                                .downloadFromBreakpoint(task, info);
                    }
                } else {
                    Util.d(TAG, "breakpoint invalid: download from beginning because of "
                            + "remote check not resumable " + task.getId() + " " + remoteCheck);
                    // 6. assemble block data
                    // 不能恢复下载,有异常
                    // 丢弃(删除)task对应的本地文件,准备重新下载
                    fileStrategy.discardProcess(task);
                    // 重新组装断点续传的分块数据
                    assembleBlockAndCallbackFromBeginning(info, remoteCheck,
                            remoteCheck.getCauseOrThrow());
                }
            } catch (IOException e) {
                cache.setUnknownError(e);
                break;
            }

            // 7. start with cache and info.
            // DownloadCache / BreakpointInfo
            // 上面处理完成后,都是在这里统一开始任务下载
            start(cache, info);

            // 再判断 本 Runnable 是否被取消了,因为上面的操作很多都是耗时的任务
            if (canceled) break;

            // 8. retry if precondition failed.
            if (cache.isPreconditionFailed()
                    && retryCount++ < MAX_COUNT_RETRY_FOR_PRECONDITION_FAILED) {
                store.remove(task.getId());
                retry = true;
            } else {
                retry = false;
            }
        } while (retry);

        // finish
        finishing = true;
        blockChainList.clear();

        final DownloadCache cache = this.cache;
        if (canceled || cache == null) return;

        final EndCause cause;
        Exception realCause = null;
        if (cache.isServerCanceled() || cache.isUnknownError()
                || cache.isPreconditionFailed()) {
            // error
            cause = EndCause.ERROR;
            realCause = cache.getRealCause();
        } else if (cache.isFileBusyAfterRun()) {
            cause = EndCause.FILE_BUSY;
        } else if (cache.isPreAllocateFailed()) {
            cause = EndCause.PRE_ALLOCATE_FAILED;
            realCause = cache.getRealCause();
        } else {
            cause = EndCause.COMPLETED;
        }
        inspectTaskEnd(cache, cause, realCause);
    }

从这个 execute 方法执行中,我们发现其内部有以下几个重要的逻辑点:

  1. remoteCheck.check();
  2. cache.setRedirectLocation(task.getRedirectLocation());
  3. localCheck.check();
  4. assembleBlockAndCallbackFromBeginning(info, remoteCheck, localCheck.getCauseOrThrow());
  5. okDownload.callbackDispatcher().dispatch().downloadFromBreakpoint(task, info);
  6. assembleBlockAndCallbackFromBeginning(info, remoteCheck, remoteCheck.getCauseOrThrow());
  7. start(cache, info);

在这个过程中,我们先不管1-6点,我们直接看第7点 start(cache, info); 方法

4. DownloadCall.start 方法分析

经过一些列的检查、信息收集、数据准备后,开始执行 start(cache, info); 方法。

/**
     * 使用给定的下载缓存和断点信息启动下载任务
     *
     * @param cache 下载缓存对象,用于存储和检索下载过程中的缓存数据
     * @param info 断点信息对象,包含下载任务的断点及相关信息
     * @throws InterruptedException 如果在下载过程中线程被中断,则抛出此异常
     */
    void start(final DownloadCache cache, BreakpointInfo info) throws InterruptedException {
        // 从 BreakpointInfo 中获取 block 信息,并创建 DownloadChain
        final int blockCount = info.getBlockCount();
        final List<DownloadChain> blockChainList = new ArrayList<>(info.getBlockCount());
        final List<Integer> blockIndexList = new ArrayList<>();
        // 这里对分块下载的每一块数据状态进行检查:
        // 如果已经完成下载,则跳过;
        // 如果数据对不上,则删掉重新下载;
        // 最终生成一个需要下载的分块下载任务清单,同时给每个分块下载任务创建一个DownloadChain对象
        for (int i = 0; i < blockCount; i++) {
            final BlockInfo blockInfo = info.getBlock(i);
            // 检查当前 block 是否已经下载完整且正确,如果已经完整则跳过
            if (Util.isCorrectFull(blockInfo.getCurrentOffset(), blockInfo.getContentLength())) {
                continue;
            }

            // 如果 block 数据不干净,则重置 block 数据
            // 通过 getCurrentOffset 和 getContentLength 判断当前这个block是否脏数据,
            // 如果是,则重置,然后重新下载
            Util.resetBlockIfDirty(blockInfo);
            // 创建 DownloadChain 对象并添加到列表中
            final DownloadChain chain = DownloadChain.createChain(i, task, info, cache, store);
            blockChainList.add(chain);
            blockIndexList.add(chain.getBlockIndex());
        }

        // 如果任务已被取消,则直接返回
        if (canceled) {
            return;
        }

        // 设置缓存的输出流需要的 block 索引列表
        cache.getOutputStream().setRequireStreamBlocks(blockIndexList);

        // 开始处理 block 列表,在这个方法里面就会触发 block 列表中的每个任务开始下载直到完成
        // 当然,这里只是将任务提交到线程池,真正的下载任务会在 DownloadChain.run 中触发完成
        startBlocks(blockChainList);
    }

5. DownloadCall.startBlocks 方法分析

其实 startBlocks 方法很简单,就是将需要执行下载的 DownloadChain 任务列表,逐个交给线程池进行任务调度,然后通过线程池调度放回的 Future 对象进行结果的等待。


    /**
     * 启动一组下载任务链
     * 该方法负责提交下载任务链到线程池执行,并处理任务完成或异常情况
     *
     * @param tasks 下载任务链的列表,每个任务链代表一个需要下载的文件及其相关元数据
     * @throws InterruptedException 如果线程在等待任务完成时被中断
     */
    void startBlocks(List<DownloadChain> tasks) throws InterruptedException {
        // 存储所有任务的Future对象,以便后续管理和等待任务完成
        ArrayList<Future> futures = new ArrayList<>(tasks.size());
        try {
            // 遍历任务列表,提交每个任务链到线程池执行
            for (DownloadChain chain : tasks) {
                futures.add(submitChain(chain));
            }

            // 将当前处理的任务链添加到阻塞链列表,以便后续管理和查询
            blockChainList.addAll(tasks);

            // 遍历所有提交的任务,等待每个任务完成
            for (Future future : futures) {
                if (!future.isDone()) {
                    try {
                        // 主动等待并获取任务执行结果,此处处理了任务可能被取消或执行异常的情况
                        // 触发执行 Runnable 的 run 方法,然后阻塞等待执行完成(任务正常完成/任务执行抛出异常)
                        future.get();
                    } catch (CancellationException | ExecutionException ignore) { }
                }
            }
        } catch (Throwable t) {
            // 如果遇到异常,尝试取消所有已提交的任务
            for (Future future : futures) {
                future.cancel(true);
            }
            // 重新抛出遇到的异常
            throw t;
        } finally {
            // 无论成功还是异常,最终需要从阻塞链列表中移除所有处理的任务
            // 对应的是上面代码中的 addAll(tasks)
            blockChainList.removeAll(tasks);
        }
    }

    Future<?> submitChain(DownloadChain chain) {
        // DownloadChain 是一个 Runnable ,这里最终会调用 run 方法
        // ExecutorService 是 Java 并发工具包中的一部分,用于管理和控制线程执行。它提供了一种将任务提交与任务执行
        // 机制解耦的方式。ExecutorService 接口中的 submit 方法允许你提交一个 Runnable 或 Callable 任务,
        // 并返回一个 Future 对象,这个对象可以用来检查任务的状态、获取结果(如果有的话)或者取消任务。
        //
        // submit(Runnable task) 方法
        // 当你调用 ExecutorService 的 submit(Runnable task) 方法时,会创建一个新的异步任务并立即开始执行它。
        // 该方法返回一个 Future<?> 对象,这个对象表示异步计算的结果。对于 Runnable 类型的任务,由于它们不返回结果,
        // 所以 Future 实际上只能用来检查任务是否完成或取消任务。
        return EXECUTOR.submit(chain);
    }

那么 DownloadChain 是什么呢?这里不难知道它肯定是一个 Runnable 。但是其内部的具体实现是怎么样的,我们下一篇再进行分析。

接下来我们分析一下在 第3节 中故意忽略掉的1-6点。

6. DownloadCall.execute 方法中的重要节点分析

从这个 execute 方法执行中,我们发现其内部有以下几个重要的逻辑点:

  1. remoteCheck.check();
  2. cache.setRedirectLocation(task.getRedirectLocation());
  3. localCheck.check();
  4. assembleBlockAndCallbackFromBeginning(info, remoteCheck, localCheck.getCauseOrThrow());
  5. okDownload.callbackDispatcher().dispatch().downloadFromBreakpoint(task, info);
  6. assembleBlockAndCallbackFromBeginning(info, remoteCheck, remoteCheck.getCauseOrThrow());
  7. start(cache, info);(该点上面已经分析过了)

6.1. remoteCheck.check() 分析

remoteCheck.check() 方法,是 BreakpointRemoteCheck.check() 类的方法。

    /**
     * 执行检查以确定下载任务的状态和策略
     * 此方法涉及与服务器的连接和校验,以确保下载任务可以被正确地恢复或需要重新开始
     * 它检查了包括服务器响应、ETag、文件名、响应代码以及是否支持断点续传等信息
     *
     * @throws IOException 如果IO操作失败
     */
    public void check() throws IOException {
        // 下载策略
        final DownloadStrategy downloadStrategy = OkDownload.with().downloadStrategy();

        // 执行试探连接,这里是一个试探连接,用于获取服务器的响应信息
        // 这里发送的请求是使用了一个 Range=bytes=0-0 的请求,用于获取服务器的响应信息,而不会真实的下载文件流
        // 这里可以直接使用head的方式请求吗?用Range的方式更加的合理,能够检测出服务器是否支持Range参数,
        // 而head方式则无法检测
        ConnectTrial connectTrial = createConnectTrial();
        connectTrial.executeTrial();

        // 检查服务器是否支持断点续传
        final boolean isAcceptRange = connectTrial.isAcceptRange();
        // 检查服务器文件是否为分片下发,分片下发是由于内容是服务器实时生成的,服务器也无法知道具体的文件大小等信息,
        // 这类的文件,没法下载保存为完整的内容,因为不知道什么时候结束
        final boolean isChunked = connectTrial.isChunked();
        // 获取服务器响应的数据信息
        final long instanceLength = connectTrial.getInstanceLength();
        final String responseEtag = connectTrial.getResponseEtag();
        final String responseFilename = connectTrial.getResponseFilename();
        final int responseCode = connectTrial.getResponseCode();

        // 1. 组装基础数据
        // 根据服务器响应的数据组装文件名,以及 task中的fileName 确定用于最终保存下载文件的文件名
        // task.fileName 优先级最高,responseFilename 优先级第二,如果以上两个都没有,
        // 则使用url中解析的文件名,否则使用url全路径的md5值作为文件名
        downloadStrategy.validFilenameFromResponse(responseFilename, task, info);
        info.setChunked(isChunked);
        info.setEtag(responseEtag);

        // 检查文件是否在运行后被其他任务占用,则抛出异常
        if (OkDownload.with().downloadDispatcher().isFileConflictAfterRun(task)) {
            throw FileBusyAfterRunException.SIGNAL;
        }

        // 2. 收集检查结果,计算是否满足启动下载的条件
        final ResumeFailedCause resumeFailedCause = downloadStrategy
                .getPreconditionFailedCause(responseCode, info.getTotalOffset() != 0, info,
                        responseEtag);

        // 没有异常时,表示可以启动下载
        this.resumable = resumeFailedCause == null;
        this.failedCause = resumeFailedCause;
        this.instanceLength = instanceLength;
        this.acceptRange = isAcceptRange;

        // 3. 检查服务器是否取消了任务
        if (!isTrialSpecialPass(responseCode, instanceLength, resumable)
                && downloadStrategy.isServerCanceled(responseCode, info.getTotalOffset() != 0)) {
            throw new ServerCanceledException(responseCode, info.getTotalOffset());
        }
    }



    /**
     * 判断是否为特定的试用期通行证
     * 此方法用于处理特定条件下试用期的验证情况,如果满足条件,则认为通行证有效
     *
     * @param responseCode 后端返回的状态码,用于判断文件状态
     * @param instanceLength 实例长度,用于判断文件是否完整
     * @param isResumable 是否可恢复,表示是否可以恢复下载
     * @return 如果满足特定条件,则返回true,表示通行证有效;否则返回false
     */
    boolean isTrialSpecialPass(int responseCode, long instanceLength, boolean isResumable) {
        // 当状态码为416(范围不满足),实例长度大于等于0,且可恢复时
        if (responseCode == RANGE_NOT_SATISFIABLE && instanceLength >= 0 && isResumable) {
            // 提供有效的实例长度和可恢复性,但后端错误响应状态码416
            // 对于范围:0-0,由于响应头中的值有效,我们通过它。
            return true;
        }

        return false;
    }

6.2. cache.setRedirectLocation(task.getRedirectLocation()); 分析

为什么说这个点也是重要的点呢?

其原因在于,真实的资源地址,有可能被后端移动到其他地方了,产生资源重定向,而我们在做探测连接的时候,就可以知道资源是否有重定向。所以我们就可以在探测连接的时候,就可以直接把资源重定向后的地址存储下来,省去了真正开始下载时,还要再处理重定向逻辑的繁琐步骤。

task.getRedirectLocation() 的值,是在 connectTrial.executeTrial(); 中设置的。


    public void executeTrial() throws IOException {
        OkDownload.with().downloadStrategy().inspectNetworkOnWifi(task);
        OkDownload.with().downloadStrategy().inspectNetworkAvailable();

        DownloadConnection connection = OkDownload.with().connectionFactory().create(task.getUrl());
        boolean isNeedTrialHeadMethod;
        try {
            if (!Util.isEmpty(info.getEtag())) {
                connection.addHeader(IF_MATCH, info.getEtag());
            }
            connection.addHeader(RANGE, "bytes=0-0");
            // 可支持下载请求需要的headers params,但是 If-match 和 Range 只能由内部设置,会校验,
            // 其实可以直接内部剔除掉即可,增加容错能力
            final Map<String, List<String>> userHeader = task.getHeaderMapFields();
            if (userHeader != null)  Util.addUserRequestHeaderField(userHeader, connection);

            final DownloadListener listener = OkDownload.with().callbackDispatcher().dispatch();
            // 获取请求的 header params ,发送请求前完整的 header 信息
            final Map<String, List<String>> requestProperties = connection.getRequestProperties();
            listener.connectTrialStart(task, requestProperties);

            // 连接器开始执行请求
            final DownloadConnection.Connected connected = connection.execute();
            // 设置重定向url,如果有重定向的话,这样在真正开始下载任务的时候,就不需要再次重定向了,直接用这个重定向的地址下载即可
            task.setRedirectLocation(connected.getRedirectLocation());
            Util.d(TAG, "task[" + task.getId() + "] redirect location: "
                    + task.getRedirectLocation());

            this.responseCode = connected.getResponseCode();
            // 判断是否有 Accept-Range 返回头信息
            this.acceptRange = isAcceptRange(connected);
            // 从返回头信息中解析出 Content-Range 信息,没有的话返回 -1,
            this.instanceLength = findInstanceLength(connected);
            // 从返回头信息中解析出 Etag 信息
            // ETag: "37b1d5cf9ff53c20a10e092efc95c467"
            //
            // HTTP响应头中的ETag(Entity Tag)是一个标识符,用于表示特定资源的特定版本。
            // 它通常是由服务器生成的一串不透明的字符串,用来标识一个资源的状态。
            // ETag的主要用途是帮助浏览器和服务器进行缓存验证,以确定客户端是否需要重新下载资源还是可以使用本地缓存的副本。
            //
            // ETag 的主要作用:
            // 缓存验证:当客户端请求一个资源时,如果该资源之前已经被缓存过,
            // 那么客户端可以在后续的请求中发送一个If-None-Match头部,其中包含之前收到的ETag值。
            // 服务器会检查这个ETag值与当前资源的ETag值是否匹配。如果匹配,说明资源没有变化,
            // 服务器返回304 Not Modified状态码,告诉客户端可以继续使用缓存的资源;如果不匹配,
            // 则服务器会返回新的资源内容及200 OK状态码,并附带新的ETag值。
            // 处理并发更新:在某些情况下,ETag还可以用于防止多个客户端同时修改同一资源导致的数据冲突问题。
            // 例如,在执行PUT或DELETE操作时,客户端可以发送If-Match头部来携带ETag值。
            // 如果服务器发现资源的ETag值与请求中的ETag值不匹配,这可能意味着资源已经被其他客户端修改了,
            // 此时服务器可能会拒绝此次操作并返回412 Precondition Failed状态码。
            this.responseEtag = findEtag(connected);
            // 从返回头信息中解析出 Content-Disposition 信息,获取文件名
            this.responseFilename = findFilename(connected);
            Map<String, List<String>> responseHeader = connected.getResponseHeaderFields();
            if (responseHeader == null) responseHeader = new HashMap<>();
            // 回调请求结束,并且输出响应头信息和响应码
            listener.connectTrialEnd(task, responseCode, responseHeader);

            // 如果通过 transfer-encoding 和 content-range 无法确定实例长度,
            // 但 content-length 存在但不能使用,我们将请求 HEAD 方法请求以找出正确的实例长度。
            isNeedTrialHeadMethod = isNeedTrialHeadMethodForInstanceLength(instanceLength,
                    connected);
        } finally {
            connection.release();
        }

        if (isNeedTrialHeadMethod) {
            // 如果通过 transfer-encoding 和 content-range 无法确定实例长度,
            // 但 content-length 存在但不能使用,我们将请求 HEAD 方法请求以找出正确的实例长度。
            trialHeadMethodForInstanceLength();
        }
    }

6.3. localCheck.check(); 分析

localCheck.check(); 的调用,是在 remoteCheck.check() 执行完成后资源地址满足文件断点续传下载的各种条件后执行的本地检查。

本地检查本地已经存在的数据与信息,与前面探测连接之后构建的下载信息是否能够吻合。

    public void check() {
        fileExist = isFileExistToResume();
        infoRight = isInfoRightToResume();
        outputStreamSupport = isOutputStreamSupportResume();
        // 检查是否需要重新下载,并设置 dirty 标志
        dirty = !infoRight || !fileExist || !outputStreamSupport;
    }

    /**
     * 检查文件是否存在以继续任务
     *
     * 此方法用于判断一个文件是否存在,以便可以恢复未完成的任务它首先检查文件的URI方案,
     * 如果是ContentProvider方案,则检查对应的内容URI大小是否大于0;如果不是内容方案,则直接检查文件是否存在
     *
     * @return 如果文件存在并且可以用于恢复任务,则返回true;否则返回false
     */
    public boolean isFileExistToResume() {
        // 获取任务的URI
        final Uri uri = task.getUri();
        // 检查URI的方案是否为内容方案
        if (Util.isUriContentScheme(uri)) {
            // 如果是内容方案,检查对应的内容URI的大小是否大于0
            return Util.getSizeFromContentUri(uri) > 0;
        } else {
            // 如果不是内容方案,获取对应的文件对象
            final File file = task.getFile();
            // 检查文件对象是否非空且文件确实存在
            return file != null && file.exists();
        }
    }

    /**
     * 检查信息是否正确,以便继续执行任务
     * 此方法主要用于验证任务的信息是否完整和正确,确保任务可以正确地继续执行
     * 它检查了块的数量、是否分块、文件是否存在、文件是否匹配任务、文件大小是否合理,
     * 以及每个块的信息是否正确
     *
     * @return 如果所有检查都通过,则返回true;否则返回false
     */
    public boolean isInfoRightToResume() {
        // 获取任务块的数量
        final int blockCount = info.getBlockCount();

        // 检查块的数量是否大于0,如果块数量小于等于0,则返回false
        if (blockCount <= 0) return false;
        // 检查任务的下载资源是否为后端直接分块了,如果后端直接分块,则返回false
        if (info.isChunked()) return false;
        // 检查任务关联的文件是否存在,如果文件不存在,则返回false
        if (info.getFile() == null) return false;
        // 检查任务文件是否与任务对象关联的文件匹配,如果不匹配,则返回false
        final File fileOnTask = task.getFile();
        if (!info.getFile().equals(fileOnTask)) return false;
        // 检查文件大小是否小于等于任务的总长度,如果文件大小大于任务的总长度,则返回false
        if (info.getFile().length() > info.getTotalLength()) return false;

        // 检查任务的响应实例长度是否与任务的总长度一致,如果不一致,则返回false
        if (responseInstanceLength > 0 && info.getTotalLength() != responseInstanceLength) {
            return false;
        }

        // 遍历每个块,检查块的内容长度是否大于0,如果存在内容长度小于等于0的块,则返回false
        for (int i = 0; i < blockCount; i++) {
            BlockInfo blockInfo = info.getBlock(i);
            if (blockInfo.getContentLength() <= 0) return false;
        }

        // 如果所有检查都通过,则返回true
        return true;
    }

    /**
     * 检查输出流是否支持断点续传
     *
     * 此方法用于判断当前的输出流是否支持断点续传功能主要考虑了以下几点:
     * 1. 输出流是否支持随机访问(seek)
     * 2. 当前任务的块数是否为1,多块情况不支持断点续传
     * 3. 下载任务是否需要预分配文件大小,如果是,则不支持断点续传
     *
     * @return 如果输出流支持断点续传,则返回true;否则返回false
     */
    public boolean isOutputStreamSupportResume() {
        // 检查输出流是否支持随机访问
        final boolean supportSeek = OkDownload.with().outputStreamFactory().supportSeek();
        if (supportSeek) return true;

        // 不支持随机访问,但是任务的块数不为1,则不支持断点续传
        if (info.getBlockCount() != 1) return false;
        // 如果下载任务需要预分配文件大小,则不支持断点续传
        if (OkDownload.with().processFileStrategy().isPreAllocateLength(task)) return false;

        return true;
    }

6.4. assembleBlockAndCallbackFromBeginning 分析

如果本地文件有异常,或者与探测连接信息对不上,则删掉本地文件后,从头开始下载。

6.5. okDownload.callbackDispatcher().dispatch().downloadFromBreakpoint 分析

如果断点续传相关检测没有问题,则从断点位置开始继续下载。

分析到这里,我们知道了 DownloadCall 最后会转到 DownloadChain 调用执行。下一篇我们分析 DownloadChain 类。

到此为止,我们对 `DownloadTask` 分析完成。