Android大文件上传秒传之MD5篇

HilPardey 4年前
   <p style="text-align:center"><img src="https://simg.open-open.com/show/ef8b9615381dca69284e75866fdbcf56.png"></p>    <h2><strong>前言</strong></h2>    <p>现在越来越多的应用开始有上传大文件的需求,以及秒传,续传功能。由于最近学习大文件分隔上传,以及秒传的实现,给予分享的这种精神,我想将自己的学习过程,以及遇到的问题做一个总结,希望对有这方面需求的小伙伴有一定的帮助。</p>    <h2><strong>分析</strong></h2>    <p>说到大文件上传,我们可能首先会想的一些网盘App,这些优秀的网盘除了上传大文件外,还可以实现秒传以及断点续传功能。说起断点续传也就明白了文章题目所说的大文件分片,由于网络的原因,一个大文件例如一百兆的文件,很有可能中途上传到一半或者50MB,或者上传到99MB时候失败了,如果下次再上传时还从头开始上传,这个体验很少人能接受的,如果你要真做成这样的话,那么客户一定会严重流失,所以我们需要对其分片或者说下次上传时从失败的地方开始上传。相信使用网盘较多的朋友都知道有一个很6的功能就是秒传,可能你很难相信为何我几百兆甚至几个G的文件,为何几秒内就上传成功了,为何这么神奇,其实原理也很简单,就是我们每次上传文件时每一个文件都会有一个独一无二的特征值,当我们上传文件时,他首先会检测服务器是否有该特征值的文件,,如果有的话,就不需要占用网络带宽,直接复制一份到你的网盘。今天分享的这篇文章便是为秒传打下坚实基础的,获取大文件的特征值-MD5.</p>    <p>MD5消息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。MD5由罗纳德·李维斯特设计,于1992年公开,用以替换MD4算法</p>    <h2><strong>MessageDigest</strong></h2>    <p>在java.security这个包下有一个类MessageDigest ,通过名字我们就知道是消息摘要的意思,那么本篇文章也是有MessageDigest 这个类展开讨论。</p>    <pre>  <code class="language-java">//方法1:返回MessageDigest实例 algorithm算法名称  public static MessageDigest getInstance(String algorithm)              throws NoSuchAlgorithmException {}  //方法2:更新计算消息摘要的数据内容  public void update(byte[] input) {}    //方法3:计算消息摘要并重置   public byte[] digest(){}</code></pre>    <p>对于计算文件的MD5,我们主要用的上面的几个方法。方法1主要是进行初始化操作,需要指定算法,方法2是进行消息摘要内容的更新。而方法3就是最重要的一步,计算消息摘要的值并返回。</p>    <h2><strong>读取文件</strong></h2>    <p>对于文件的读取有很多种方式,例如通过FileInputStream读取字节流,也可以包装成InputStreamReader读取字节流,也可以包装成BufferedInputStream进行带缓冲区的读取,以及RandomAccessFile或者nio 包中FileChannel加内存映射的方式。当然各种方式的性能不言而喻(对流不熟悉的自行补脑)。</p>    <h2><strong>具体实现</strong></h2>    <h2><strong>FileInputStream字节流方式</strong></h2>    <pre>  <code class="language-java">/**       * 获取文件的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 "";      }</code></pre>    <h2><strong>FileChannel +MappedByteBuffer 方式</strong></h2>    <pre>  <code class="language-java">/**       * 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 "";      }</code></pre>    <h2><strong>RandomAccessFile 方式</strong></h2>    <pre>  <code class="language-java">/**       * 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 "";      }</code></pre>    <h2><strong>性能对比</strong></h2>    <p>我们选了一个小的文件,大概1M左右,观察执行时间</p>    <pre>  <code class="language-java">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</code></pre>    <p>但是我选择大概10M的文件FileChannel+MappedByteBuffer性能并不明显,最后通过查询资料学习发现MappedByteBuffer这个东西很可怕,这个回收是不确定的,在手机上测试FileChannel效率并不是最好的。如果要计算一个几百兆的大文件,发现FileChannel+MappedByteBuffer还很容易OOM,原因就是MappedByteBuffer内存占用、文件关闭不确定,被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的。当文件达到100M时就出现OOM如下</p>    <pre>  <code class="language-java">FATAL EXCEPTION: main  java.lang.OutOfMemoryError  at java.security.MessageDigestSpi.engineUpdate(MessageDigestSpi.java:85)  at java.security.MessageDigest.update(MessageDigest.java:369)</code></pre>    <p>所以在Android设备上尽量不要使用nio中的内存映射。在官方文档中有这样的一句话:A mapped byte buffer and the file mapping that it represents remain valid until the buffer itself is garbage-collected.</p>    <p>那么我们来计算一个大文件的MD5,此时我测试的文件是300多兆</p>    <pre>  <code class="language-java">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</code></pre>    <p>通过日志发现RandomAccessFile的效率还是很明显的,此时使用FileChannel+MappedByteBuffer就OOM了,虽然使用了分段映射 也调用了MappedByteBuffer的clear()方法。当然通过日志你肯定明白 计算文件MD5值是一个比较耗时的操作,不要再主线程中计算 。</p>    <h2><strong>计算MD5</strong></h2>    <p>我们需要注意对于较大的文件计算MD5,我们不要一次将文件读取然后调用update方法。不然执行update方法时就会出现OOM。我们可以分段读取多次调用update方法,如下</p>    <pre>  <code class="language-java">while ((len = fis.read(buffer)) > 0) {                  //该对象通过使用 update()方法处理数据                  messageDigest.update(buffer, 0, len);              }</code></pre>    <p>你要明白调用执行update并没有计算MD5的值,真正计算的MD5值是调用digest(),该方法返回的是一个byte数组</p>    <pre>  <code class="language-java">byte[] bytes = messageDigest.digest();</code></pre>    <p>通常我们一般将MD5用16进制也就是32位表示,所以我们可以将byte数组转化为16进制,此时我们可以使用BigInteger类,他的构造方法可以接收byte数组参数,如下,1表示符号为正数。</p>    <pre>  <code class="language-java">BigInteger bigInt = new BigInteger(1, bytes );</code></pre>    <p>BigInteger这个类还提供了一个toString方法该参数可以指定转化数据格式,由于我们转化为16进制,所以参数可以写16,如下</p>    <pre>  <code class="language-java">String md5 = bigInt.toString(16);</code></pre>    <p>OK了,MD5的值已经出现了,不过你可能会疑问了,转化为16进制的话,MD5值应该是32位,为何有时候计算的值不是32位,而是31位呢?甚至还可能更少,原因就是digest()返回值的高位包含了0,当然高位0是不写的,所以就出现少位的情况,这也就有了下面的代码,如果不到32位我们再高位补0就好了。</p>    <pre>  <code class="language-java">while (md5.length() < 32) {                  md5 = "0" + md5;              }</code></pre>    <p>至此,本篇文章结束,若有不足的地方欢迎指正,谢谢。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/1e5ff0e4b700</p>    <p> </p>