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;
            }複製代碼

至此,本篇文章結束,如有不足的地方歡迎指正,謝謝。

相關文章
相關標籤/搜索