xUtils3 源码解析 -- 实现断点续传

在通过网络进行图片或者文件的下载时,为保证内存和磁盘资源的合理利用,我们一般会对此次请求进行断点续传。断点续传,顾名思义就是在一次图片或文件下载的网络请求过程中,因异常情况此次操作被迫中断,那么下一次请求相同资源的网络请求会继续上一次的进度继续下载图片或文件资源。

1. 如何保证两次请求的内容为同一资源

为保证这一前提,xUtils3 实现了两种策略:

  1. RequestParams#saveFilePath不为空时, 目标文件保存在saveFilePath;
  2. 否则由Cache策略分配文件下载路径.
saveFilePath = params.getSaveFilePath();
if (TextUtils.isEmpty(saveFilePath)) {
    if (progressHandler != null && !progressHandler.updateProgress(0, 0, false)) {
        throw new Callback.CancelledException("download stopped!");
    }
    // 保存路径为空, 存入磁盘缓存.
    initDiskCacheFile(request);
} else {
    tempSaveFilePath = saveFilePath + ".tmp";
}

private void initDiskCacheFile(final UriRequest request) throws Throwable {

        DiskCacheEntity entity = new DiskCacheEntity();
        entity.setKey(request.getCacheKey());
        diskCacheFile = LruDiskCache.getDiskCache(params.getCacheDirName()).createDiskCacheFile(entity);

        if (diskCacheFile != null) {
            saveFilePath = diskCacheFile.getAbsolutePath();
            // diskCacheFile is a temp path, diskCacheFile.commit() return the dest file.
            tempSaveFilePath = saveFilePath;
            isAutoRename = false;
        } else {
            throw new IOException("create cache file error:" + request.getCacheKey());
        }
    }

这两种策略最终都是最终都是生成后缀为 .tmp 的临时文件,这样就保证了在下载完成之前所下载的文件资源在同一位置(当然分为自定义下载位置和缓存自分配位置)。

2. 文件校验

为了保证两次下载的文件资源相同,我们要进行文件校验工作,此步骤发生在第二次网络请求时。

2.1 校验第一步:判断文件可用性

若第一次对文件的下载进度小于 512(CHECK_SIZE) 字节,那么该次下载被忽略,对相应的文件进行删除操作。

FileLoad#load(UriRequest urirequest)

params = request.getParams();
{
    long range = 0;
    if (isAutoResume) {
        File tempFile = new File(tempSaveFilePath);
        long fileLen = tempFile.length();
        if (fileLen <= CHECK_SIZE) {
            IOUtil.deleteFileOrDir(tempFile);
            range = 0;
        } else {
            range = fileLen - CHECK_SIZE;
        }
    }
    // retry 时需要覆盖RANGE参数
    params.setHeader("RANGE", "bytes=" + range + "-");
}

.....

request.sendRequest();

2.2 校验第二步:服务器支持

网络请求的实质其实是对网络请求的发送和网络数据的传输,那么我们要想实现网络的断点续传,那必须是需要服务端进行支持的,不然的我们所做的工作到头来不过也是一场空。我们怎样将这次断点续传的请求告知服务端呢,当然是通过 Header,客户端对网络请求的一些参数大部分都是通过 Header 来实现的,我们需要做的是发送我们想要的文件长度。具体的关于 Range 的请参看 -- HTTP之Range理解关于Rang 的stack overflow 的回答。那么我们对本次的 Header 的设置如下

//设置本次请求的 Header
params.setHeader("RANGE", "bytes=" + range + "-");
//发送本次请求
request.sendRequest();
//获取本次网络请求的文件字节数
contentLength = request.getContentLength();

2.3 校验第三步:文件校验

断点续传功能的最终实现当然不能少的就是对文件的校验,不然你最终实现的文件是两个文件的拼接,那岂不是很尴尬。

针对文件校验的代码如下:

long targetFileLen = targetFile.length();
if (isAutoResume && targetFileLen > 0) {
    FileInputStream fis = null;
    try {
        long filePos = targetFileLen - CHECK_SIZE;
        /**
         * 完成断点续传的校验 6666 学到了
         */
        if (filePos > 0) {
            fis = new FileInputStream(targetFile);
            byte[] fileCheckBuffer = IOUtil.readBytes(fis, filePos, CHECK_SIZE);
            byte[] checkBuffer = IOUtil.readBytes(in, 0, CHECK_SIZE);
            if (!Arrays.equals(checkBuffer, fileCheckBuffer)) {
                IOUtil.closeQuietly(fis); // 先关闭文件流, 否则文件删除会失败.
                IOUtil.deleteFileOrDir(targetFile);
                throw new RuntimeException("need retry");
            } else {
                contentLength -= CHECK_SIZE;
            }
        } else {
            IOUtil.deleteFileOrDir(targetFile);
            throw new RuntimeException("need retry");
        }
    } finally {
        IOUtil.closeQuietly(fis);
    }
}

为更好的理解这一过程我们看一下我自己针对代码绘制一份流程图:
图一、文件字节码校验

通过上图我们要做的工作是比对 A 字节码和 B 字节码是否相同

3、续写入文件

// 开始下载
            long current = 0;
            FileOutputStream fileOutputStream = null;
            if (isAutoResume) {
                current = targetFileLen;
                fileOutputStream = new FileOutputStream(targetFile, true);
            } else {
                fileOutputStream = new FileOutputStream(targetFile);
            }

此处存在一个无比大的坑:按照图一我们可以看到:第一次和第二次下载的文件长度有大小为 Cache_Size 长度的重合字节,通过fileOutputStream = new FileOutputStream(targetFile, true); 我们可以知道的是第二次下载的字节会直接拼在第一次下载的文件之后,这样来说的话岂不是多了 Cache_Size 个长度数据的字节,这破坏了文件的完整性啊,那 xUtils 的断点续传功能不是报废了吗?不对,于是自己就一遍又一遍的看相关代码,根本没有发现相应功能的代码,一时间懵逼、急躁。自己静下心了重新顺了几遍流程,发现分析过程没错,于是着手排查,发现对 第二次下载字节码的进行字节数为Cache_Size 操作的地方只有一个:文件校验,代码如下:

byte[] checkBuffer = IOUtil.readBytes(in, 0, CHECK_SIZE);

马上跳转相应代码查看了IOUtil#readBytes()相关代码

public static byte[] readBytes(InputStream in, long skip, int size) throws IOException {
        byte[] result = null;
        if (skip > 0) {
            long skipped = 0;
            while (skip > 0 && (skipped = in.skip(skip)) > 0) {
                skip -= skipped;
            }
        }
        result = new byte[size];
        for (int i = 0; i < size; i++) {
            result[i] = (byte) in.read();
        }
        return result;
    }

这里有一个对InputStream字节流的操作 --skip(),此 api 的最终实现了对 InputStream丢弃 Cache_Size 个字节的数据,一切流程终于走通了。关于 InputStream#skip()的相关内容可以查看 InputStream方法详解

4. 文件重命名和 CacheFile#commit() 写入内存

通过以上步骤我们已经完成了断点续传功能,但是最终我们下载的文件是以 .tmp 为名的临时文件,我们最终要实现的是将临时文件重名为指定文件名的文件,具体实现如下:

  private File autoRename(File loadedFile) {
        if (isAutoRename && loadedFile.exists() && !TextUtils.isEmpty(responseFileName)) {
            File newFile = new File(loadedFile.getParent(), responseFileName);
            while (newFile.exists()) {
                newFile = new File(loadedFile.getParent(), System.currentTimeMillis() + responseFileName);
            }
            return loadedFile.renameTo(newFile) ? newFile : loadedFile;
        } else if (!saveFilePath.equals(tempSaveFilePath)) {
            File newFile = new File(saveFilePath);
            return loadedFile.renameTo(newFile) ? newFile : loadedFile;
        } else {
            return loadedFile;
        }
    }

获取文件名:

    private static String getResponseFileName(UriRequest request) {
        if (request == null) return null;
        String disposition = request.getResponseHeader("Content-Disposition");
        if (!TextUtils.isEmpty(disposition)) {
            int startIndex = disposition.indexOf("filename=");
            if (startIndex > 0) {
                startIndex += 9; // "filename=".length()
                int endIndex = disposition.indexOf(";", startIndex);
                if (endIndex < 0) {
                    endIndex = disposition.length();
                }
                if (endIndex > startIndex) {
                    try {
                        String name = URLDecoder.decode(
                                disposition.substring(startIndex, endIndex),
                                request.getParams().getCharset());
                        if (name.startsWith("\"") && name.endsWith("\"")) {
                            name = name.substring(1, name.length() - 1);
                        }
                        return name;
                    } catch (UnsupportedEncodingException ex) {
                        LogUtil.e(ex.getMessage(), ex);
                    }
                }
            }
        }
        return null;
    }

5. 总结

断点续传需要的基本步骤:

  1. 保证下载的目标文件的相同(目标文件的唯一性)
  2. 文件字节的校验(字节码的唯一性)
  3. 继续上次下载位置下载(字节的连续性)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容