Android大文件上传秒传之MD5篇

前言

如今愈来愈多的应用开始有上传大文件的需求,以及秒传,续传功能。因为最近学习大文件分隔上传,以及秒传的实现,给予分享的这种精神,我想将本身的学习过程,以及遇到的问题作一个总结,但愿对有这方面需求的小伙伴有必定的帮助。javascript


源码传送门[不当心点进去了给个star( ^_^ )]java


分析

说到大文件上传,咱们可能首先会想的一些网盘App,这些优秀的网盘除了上传大文件外,还能够实现秒传以及断点续传功能。提及断点续传也就明白了文章题目所说的大文件分片,因为网络的缘由,一个大文件例如一百兆的文件,颇有可能中途上传到一半或者50MB,或者上传到99MB时候失败了,若是下次再上传时还从头开始上传,这个体验不多人能接受的,若是你要真作成这样的话,那么客户必定会严重流失,因此咱们须要对其分片或者说下次上传时从失败的地方开始上传。相信使用网盘较多的朋友都知道有一个很6的功能就是秒传,可能你很难相信为什么我几百兆甚至几个G的文件,为什么几秒内就上传成功了,为什么这么神奇,其实原理也很简单,就是咱们每次上传文件时每个文件都会有一个独一无二的特征值,当咱们上传文件时,他首先会检测服务器是否有该特征值的文件,,若是有的话,就不须要占用网络带宽,直接复制一份到你的网盘。今天分享的这篇文章即是为秒传打下坚实基础的,获取大文件的特征值-MD5.git

MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被普遍使用的密码散列函数,能够产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由罗纳德·李维斯特设计,于1992年公开,用以替换MD4算法 github

MessageDigest

在java.security这个包下有一个类MessageDigest ,经过名字咱们就知道是消息摘要的意思,那么本篇文章也是有MessageDigest 这个类展开讨论。算法

//方法1:返回MessageDigest实例 algorithm算法名称
public static MessageDigest getInstance(String algorithm)
            throws NoSuchAlgorithmException {}
//方法2:更新计算消息摘要的数据内容
public void update(byte[] input) {}

//方法3:计算消息摘要并重置
 public byte[] digest(){}复制代码

对于计算文件的MD5,咱们主要用的上面的几个方法。方法1主要是进行初始化操做,须要指定算法,方法2是进行消息摘要内容的更新。而方法3就是最重要的一步,计算消息摘要的值并返回。数组

读取文件

对于文件的读取有不少种方式,例如经过FileInputStream读取字节流,也能够包装成InputStreamReader读取字节流,也能够包装成BufferedInputStream进行带缓冲区的读取,以及RandomAccessFile或者nio 包中FileChannel加内存映射的方式。固然各类方式的性能不言而喻(对流不熟悉的自行补脑)。服务器

具体实现

FileInputStream字节流方式

/** * 获取文件的MD5值 * * @param file 文件路径 * @return md5 */
    public static String getFileMd5(File file) {
        MessageDigest messageDigest;
        //MappedByteBuffer byteBuffer = null;
        FileInputStream fis = null;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
            if (file == null) {
                return "";
            }
            if (!file.exists()) {
                return "";
            }
            int len = 0;
            fis = new FileInputStream(file);
            //普通流读取方式
            byte[] buffer = new byte[1024 * 1024 * 10];
            while ((len = fis.read(buffer)) > 0) {
                //该对象经过使用 update()方法处理数据
                messageDigest.update(buffer, 0, len);
            }
            BigInteger bigInt = new BigInteger(1, messageDigest.digest());
            String md5 = bigInt.toString(16);
            while (md5.length() < 32) {
                md5 = "0" + md5;
            }
            return md5;
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                    fis = null;
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return "";
    }复制代码

FileChannel +MappedByteBuffer 方式

/** * FileChannel 获取文件的MD5值 * * @param file 文件路径 * @return md5 */
    public static String getFileMd52(File file) {
        MessageDigest messageDigest;
        FileInputStream fis = null;
        FileChannel ch=null;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
            if (file == null) {
                return "";
            }
            if (!file.exists()) {
                return "";
            }
            fis = new FileInputStream(file);
            ch = fis.getChannel();
            int size = 1024 * 1024 * 10;
            long part = file.length() / size + (file.length() % size > 0 ? 1 : 0);
            System.err.println("文件分片数" + part);
            for (int j = 0; j < part; j++) {
                MappedByteBuffer byteBuffer = ch.map(FileChannel.MapMode.READ_ONLY, j * size, j == part - 1 ? file.length() : (j + 1) * size);
                messageDigest.update(byteBuffer);
                byteBuffer.clear();
            }
            BigInteger bigInt = new BigInteger(1, messageDigest.digest());
            String md5 = bigInt.toString(16);
            while (md5.length() < 32) {
                md5 = "0" + md5;
            }
            return md5;
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                    fis = null;
                }
                if (ch!=null){
                    ch.close();
                    ch=null;
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return "";
    }复制代码

RandomAccessFile 方式

/** * RandomAccessFile 获取文件的MD5值 * * @param file 文件路径 * @return md5 */
    public static String getFileMd53(File file) {
        MessageDigest messageDigest;
        RandomAccessFile randomAccessFile = null;
        try {
            messageDigest = MessageDigest.getInstance("MD5");
            if (file == null) {
                return "";
            }
            if (!file.exists()) {
                return "";
            }
            randomAccessFile=new RandomAccessFile(file,"r");
            byte[] bytes=new byte[1024*1024*10];
            int len=0;
            while ((len=randomAccessFile.read(bytes))!=-1){
                messageDigest.update(bytes,0, len);
            }
            BigInteger bigInt = new BigInteger(1, messageDigest.digest());
            String md5 = bigInt.toString(16);
            while (md5.length() < 32) {
                md5 = "0" + md5;
            }
            return md5;
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } finally {
            try {
                if (randomAccessFile != null) {
                    randomAccessFile.close();
                    randomAccessFile = null;
                }
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        return "";
    }复制代码

性能对比

咱们选了一个小的文件,大概1M左右,观察执行时间网络

11-09 11:49:20.210 12678-12678/com.example.xh I/System.out: FileInputStream执行时间:179
11-09 11:49:20.266 12678-12678/com.example.xh I/System.out: FileChannel执行时间:55
11-09 11:49:20.322 12678-12678/com.example.xh I/System.out: RandomAccessFile执行时间:58复制代码

可是我选择大概10M的文件FileChannel+MappedByteBuffer性能并不明显,最后经过查询资料学习发现MappedByteBuffer这个东西很可怕,这个回收是不肯定的,在手机上测试FileChannel效率并非最好的。若是要计算一个几百兆的大文件,发现FileChannel+MappedByteBuffer还很容易OOM,缘由就是MappedByteBuffer内存占用、文件关闭不肯定,被其打开的文件只有在垃圾回收的才会被关闭,并且这个时间点是不肯定的。当文件达到100M时就出现OOM以下app

FATAL EXCEPTION: main
java.lang.OutOfMemoryError
at java.security.MessageDigestSpi.engineUpdate(MessageDigestSpi.java:85)
at java.security.MessageDigest.update(MessageDigest.java:369)复制代码

因此在Android设备上尽可能不要使用nio中的内存映射。在官方文档中有这样的一句话:A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.
那么咱们来计算一个大文件的MD5,此时我测试的文件是300多兆dom

11-09 16:06:49.930 3101-3101/com.example.xh I/System.out: FileInputStream执行时间:4219
11-09 16:06:54.490 3101-3101/com.example.xh I/System.out: RandomAccessFile执行时间:2162复制代码

经过日志发现RandomAccessFile的效率仍是很明显的,此时使用FileChannel+MappedByteBuffer就OOM了,虽然使用了分段映射 也调用了MappedByteBuffer的clear()方法。固然经过日志你确定明白计算文件MD5值是一个比较耗时的操做,不要再主线程中计算

计算MD5

咱们须要注意对于较大的文件计算MD5,咱们不要一次将文件读取而后调用update方法。否则执行update方法时就会出现OOM。咱们能够分段读取屡次调用update方法,以下

while ((len = fis.read(buffer)) > 0) {
                //该对象经过使用 update()方法处理数据
                messageDigest.update(buffer, 0, len);
            }复制代码

你要明白调用执行update并无计算MD5的值,真正计算的MD5值是调用digest(),该方法返回的是一个byte数组

byte[] bytes = messageDigest.digest();复制代码

一般咱们通常将MD5用16进制也就是32位表示,因此咱们能够将byte数组转化为16进制,此时咱们可使用BigInteger类,他的构造方法能够接收byte数组参数,以下,1表示符号为正数。

BigInteger bigInt = new BigInteger(1, bytes );复制代码

BigInteger这个类还提供了一个toString方法该参数能够指定转化数据格式,因为咱们转化为16进制,因此参数能够写16,以下

String md5 = bigInt.toString(16);复制代码

OK了,MD5的值已经出现了,不过你可能会疑问了,转化为16进制的话,MD5值应该是32位,为什么有时候计算的值不是32位,而是31位呢?甚至还可能更少,缘由就是digest()返回值的高位包含了0,固然高位0是不写的,因此就出现少位的状况,这也就有了下面的代码,若是不到32位咱们再高位补0就行了。

while (md5.length() < 32) {
                md5 = "0" + md5;
            }复制代码

至此,本篇文章结束,如有不足的地方欢迎指正,谢谢。

相关文章
相关标签/搜索