2. OkDownload源码分析——DownloadTask类分析

387 阅读9分钟

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


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

下面我们先来看看 OkDownload 组件的初始化配置。

// 如果使用者有自己的特殊需求,可以通过这里对 OkDownload 内部的各种组件进行自定义配置
// 按照下面 Builder 模式,配置相应的组件模块。
// 当然,OkDownload 组件内部也提供了一整套默认的组件实现。
OkDownload.Builder builder = new OkDownload.Builder(context)
        // 断点信息存储的位置,默认是SQLite数据库 
        .downloadStore(downloadStore)
        // 监听回调分发器,默认在主线程回调 
        .callbackDispatcher(callbackDispatcher)
        // 下载管理机制,最大下载任务数、同步异步执行下载任务的处理
        .downloadDispatcher(downloadDispatcher)
        // 选择网络请求框架,默认是OkHttp 
        .connectionFactory(connectionFactory)
        // 构建文件输出流DownloadOutputStream,是否支持随机位置写入
        .outputStreamFactory(outputStreamFactory)
        // 下载策略,文件分为几个线程下载
        .downloadStrategy(downloadStrategy)
        // 多文件写文件的方式,默认是根据每个线程写文件的不同位置,支持同时写入
        .processFileStrategy(processFileStrategy)
        // 下载状态监听 
        .monitor(monitor); 
OkDownload.setSingletonInstance(builder.build());

OkDownload 组件提供了自定义配置,同时也提供了一整套默认的组件实现。

对于我们分析组件源码来说,我们直接使用默认的组件实现来进行分析即可。

下面就开始我们的源码分析

1. DownloadTask 的创建

DownloadTask 的创建使用 Builder模式 进行创建的。从其源码中可以找到 Builder 类的定义。

1.1. 我们先看一看 DownloadTask 的构造函数

// 参数非常多,很多参数都是可以使用默认值,有几个参数是必须的
public DownloadTask(
    String url, 
    Uri uri, 
    int priority, 
    int readBufferSize, 
    int flushBufferSize,
    int syncBufferSize, 
    int syncBufferIntervalMills,
    boolean autoCallbackToUIThread, 
    int minIntervalMillisCallbackProcess,
    Map<String, List<String>> headerMapFields, 
    @Nullable String filename,
    boolean passIfAlreadyCompleted, 
    boolean wifiRequired,
    Boolean filenameFromResponse,
    @Nullable Integer connectionCount,
    @Nullable Boolean isPreAllocateLength
) 

1.2. 再来看看 DownloadTask.Builder 类

Builder 类提供了三个构造函数分别如下:

        /**
         * Create the task builder through {@code url} and the file's parent path and the filename.
         *
         * @param url        the url for the task.
         * @param parentPath the parent path of the file for store download data.
         * @param filename   the filename of the file for store download data.
         */
        public Builder(@NonNull String url, @NonNull String parentPath, @Nullable String filename) {
            this(url, Uri.fromFile(new File(parentPath)));
            if (Util.isEmpty(filename)) {
                this.isFilenameFromResponse = true;
            } else {
                this.filename = filename;
            }
        }

        /**
         * Create the task builder through {@code url} and the store path {@code file}.
         *
         * @param url  the url for the task.
         * @param file the file is used for store download data of the task.
         */
        public Builder(@NonNull String url, @NonNull File file) {
            this.url = url;
            this.uri = Uri.fromFile(file);
        }

        /**
         * Create the task builder through {@code url} and the store path {@code uri}.
         *
         * @param url the url for the task.
         * @param uri the uri indicate the file path of the task.
         */
        public Builder(@NonNull String url, @NonNull Uri uri) {
            this.url = url;
            this.uri = uri;
            if (Util.isUriContentScheme(uri)) {
                this.filename = Util.getFilenameFromContentUri(uri);
            }
        }

Builder 的构造函数中可以看到,DownloadTask 的创建需要两个必须的参数 urluri 。 其中,url 是需要下载的资源地址,uri 是资源下载的存储路径。

filename 是存储的文件名,如果有特殊要求,可以特别指定,没有特殊要求的话,就忽略,有内部自动计算。

至于其他的参数都可以使用默认值,无须额外设置,我们来看一下其他参数的默认值配置。


        /**
         * Build the task through the builder.
         *
         * @return a new task is built from this builder.
         */
        public DownloadTask build() {
            return new DownloadTask(url, uri, priority, readBufferSize, flushBufferSize,
                    syncBufferSize, syncBufferIntervalMillis,
                    autoCallbackToUIThread, minIntervalMillisCallbackProcess,
                    headerMapFields, filename, passIfAlreadyCompleted, isWifiRequired,
                    isFilenameFromResponse, connectionCount, isPreAllocateLength);
        }

        // 给资源地址配置特殊的请求头信息,默认不配置
        private volatile Map<String, List<String>> headerMapFields;


        // 下载任务优先级参数,由于是 int 类型,默认值为 0
        // More larger more high.
        private int priority;

        public static final int DEFAULT_READ_BUFFER_SIZE = 4096/* byte **/;
        // 下载过程中从返回的响应数据中,从输入流中一次读取数据的buffer大小,默认为 4096
        private int readBufferSize = DEFAULT_READ_BUFFER_SIZE;
        public static final int DEFAULT_FLUSH_BUFFER_SIZE = 16384/* byte **/;
        // 下载过程中对从输入流中读取的数据,写入本地文件时写入数据的buffer大小,默认为 16384 (4096*4)
        private int flushBufferSize = DEFAULT_FLUSH_BUFFER_SIZE;

        /**
         * Make sure sync to physical filesystem.
         */
        public static final int DEFAULT_SYNC_BUFFER_SIZE = 65536/* byte **/;
        // 同步写入到物理文件时的缓存大小,默认为 65536
        private int syncBufferSize = DEFAULT_SYNC_BUFFER_SIZE;
        public static final int DEFAULT_SYNC_BUFFER_INTERVAL_MILLIS = 2000/* millis **/;
        // 同步写入到物理文件时使用的间隔时间,默认为 2000
        private int syncBufferIntervalMillis = DEFAULT_SYNC_BUFFER_INTERVAL_MILLIS;

        public static final boolean DEFAULT_AUTO_CALLBACK_TO_UI_THREAD = true;
        // 回调是否发送到UI线程,默认为是
        private boolean autoCallbackToUIThread = DEFAULT_AUTO_CALLBACK_TO_UI_THREAD;

        public static final int DEFAULT_MIN_INTERVAL_MILLIS_CALLBACK_PROCESS = 3000/* millis **/;
        // 最小的进度回调间隔时间
        private int minIntervalMillisCallbackProcess = DEFAULT_MIN_INTERVAL_MILLIS_CALLBACK_PROCESS;
        // 特殊指定的下载文件保存名称
        private String filename;

        public static final boolean DEFAULT_PASS_IF_ALREADY_COMPLETED = true;
        /**
         * if this task has already completed judged by
         * {@link StatusUtil.Status#isCompleted(DownloadTask)}, callback completed directly instead
         * of start download.
         */
        // 是否对已经下载完成的重复任务直接跳过,不跳过的话会执行重新覆盖下载
        private boolean passIfAlreadyCompleted = DEFAULT_PASS_IF_ALREADY_COMPLETED;

        public static final boolean DEFAULT_IS_WIFI_REQUIRED = false;
        // 是否需要在WiFi网络下才能下载
        private boolean isWifiRequired = DEFAULT_IS_WIFI_REQUIRED;
        // 文件名是否从响应数据的头部信息中获取
        private Boolean isFilenameFromResponse;
        // 设置该任务的 connection establish 数量,如果该任务已经对过去进行了 split block 并等待恢复,则这个设置的连接数不会真正产生影响。
        private Integer connectionCount;
        // 设置从 trial-connection 获取 resource-length 后是否需要预先分配文件的长度。
        private Boolean isPreAllocateLength;

2. 开始任务下载 DownloadTask.enqueue/execute

任务创建完成后,就可以启动任务下载了

    DownloadTask task = new DownloadTask.Builder(url, file).build();
    // 异步执行,放到下载任务的队列里面进行排队
    task.enqueue(listener)
    // or
    // 立即执行,直接插入到排队任务的前面开始执行
    task.execute(listener)

下面我们分别对这两个过程先进行简单的分析:

2.1. DownloadTask.enqueue 流程分析

DownloadTask.enqueue 调用 DownloadDispatcher.enqueue 方法。

    /**
     * Enqueue the task with the {@code listener} to the downloader dispatcher, what means it will
     * be run when resource is available and on the dispatcher thread pool.
     * <p>
     * If there are more than one task need to enqueue please using
     * {@link #enqueue(DownloadTask[], DownloadListener)} instead, because the performance is
     * optimized to handle bunch of tasks enqueue.
     *
     * @param listener the listener is used for listen the whole lifecycle of the task.
     */
    public void enqueue(DownloadListener listener) {
        this.listener = listener;
        OkDownload.with().downloadDispatcher().enqueue(this);
    }

跟踪 DownloadDispatcher.enqueue 方法,进行分析

enqueue 方法:将任务放入到下载任务队列中进行排队,同时会根据任务优先级 priority 参数,进行对任务的排序处理。

    /**
     * 将下载任务添加到队列中
     * 此方法用于在执行某些操作前后增加和减少跳过调用计数,以控制并发访问
     *
     * @param task 要添加到队列的下载任务
     */
    public void enqueue(DownloadTask task) {
        // 在添加任务前后增加和减少跳过调用计数,以控制并发访问
        skipProceedCallCount.incrementAndGet();
        enqueueLocked(task);
        skipProceedCallCount.decrementAndGet();
    }

    /**
     * 同步方法,用于将下载任务添加到队列中
     * 该方法在执行前会检查任务是否已经完成和是否存在冲突
     * 如果队列中已存在相同任务或任务之间存在冲突,则不会添加
     *
     * @param task 要添加到队列的下载任务
     */
    private synchronized void enqueueLocked(DownloadTask task) {
        // 打印调试信息,表示正在将单个任务添加到队列中
        Util.d(TAG, "enqueueLocked for single task: " + task);

        // 如果任务已经完成,则直接返回,不再添加到队列中
        if (inspectCompleted(task)) return;

        // 如果任务与队列中其他任务存在冲突,则直接返回,不添加到队列中
        if (inspectForConflict(task)) return;

        // 记录当前准备异步调用的集合大小,用于后续判断是否有变化
        final int originReadyAsyncCallSize = readyAsyncCalls.size();

        // 将任务添加到队列中,忽略优先级
        enqueueIgnorePriority(task);

        // 如果准备异步调用的集合大小发生变化,则对集合进行排序
        if (originReadyAsyncCallSize != readyAsyncCalls.size()) {
            Collections.sort(readyAsyncCalls);
        }
    }

    /**
     * 将下载任务加入合适的队列,忽略其优先级
     * 此方法主要用于在下载队列已满时,将新来的下载任务加入队列,而不考虑其优先级
     * 它通过创建一个下载调用对象来代表下载任务,并根据当前运行中的异步下载数量决定是否立即执行该任务
     * 如果运行中的异步下载数量小于最大并行运行数量,则新任务会被立即执行;
     * 否则,新任务将被加入待处理队列中等待执行
     *
     * @param task 要加入队列的下载任务
     */
    private synchronized void enqueueIgnorePriority(DownloadTask task) {
        // 创建下载调用对象,设置为异步执行
        final DownloadCall call = DownloadCall.create(task, true, store);
        // 如果当前运行中的异步下载数量小于最大并行运行数量,则将新任务加入运行队列并执行
        if (runningAsyncSize() < maxParallelRunningCount) {
            runningAsyncCalls.add(call);
            getExecutorService().execute(call);
        } else {
            // 如果当前运行中的异步下载数量等于最大并行运行数量,则将新任务加入待处理队列
            readyAsyncCalls.add(call);
        }
    }

2.2. DownloadTask.execute 流程分析

DownloadTask.execute 调用 DownloadDispatcher.execute 方法。

    /**
     * Execute the task with the {@code listener} on the invoke thread.
     *
     * @param listener the listener is used for listen the whole lifecycle of the task.
     */
    public void execute(DownloadListener listener) {
        this.listener = listener;
        OkDownload.with().downloadDispatcher().execute(this);
    }

跟踪 DownloadDispatcher.execute 方法,进行分析

execute 方法:将任务直接加入到正在下载任务队列中,然后立即启动下载流程。


    /**
     * 执行下载任务
     *
     * 此方法首先检查任务是否已完成或是否存在冲突如果任务未完成且无冲突,则创建一个DownloadCall实例并将其添加到运行中的同步调用列表中
     * 最后,调用syncRunCall方法来执行实际的下载操作
     *
     * @param task 要执行的下载任务
     */
    public void execute(DownloadTask task) {
        // 打印执行任务的日志信息
        Util.d(TAG, "execute: " + task);
        final DownloadCall call;

        synchronized (this) {
            // 检查任务是否已完成,如果已完成则直接返回
            if (inspectCompleted(task)) return;
            // 检查任务是否与正在运行的任务有冲突,如果有冲突则直接返回
            if (inspectForConflict(task)) return;

            // 创建DownloadCall实例,准备执行下载任务
            call = DownloadCall.create(task, false, store);
            // 将新创建的DownloadCall实例添加到运行中的同步调用列表中
            runningSyncCalls.add(call);
        }

        // 执行DownloadCall实例,开始下载任务
        syncRunCall(call);
    }

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

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