Android大文件上傳秒傳之實戰篇


源碼傳送門


在上一篇文章我們介紹了獲取大文件的一個唯一的特徵值MD5,通過MD5我們可以唯一的標識一個文件,並可以實現秒傳效果,今天的這篇文章主要介紹大文件的上傳操作,當然談到上傳文件,網絡是必不可少的,現在也有很多較爲流行的網絡框架,如volley,OkHttp,Retrofit。而今天的這篇文章是採用最原始的上傳文件的方法,通過HttpClient上傳文件的方式。

HttpClient API

在API 23(6.0系統)之前,HttpClient類是Android API中本身自帶的方法,但是在23及以後的版本中谷歌放棄了HttpClient,如果想要使用需要在gradle文件中加上下面代碼

android {
    useLibrary 'org.apache.http.legacy'
    }

加入上面的代碼後,我們build一下就可以API23及以後版本中可以繼續使用HttpClient,在使用HttpClient上傳文件時可以使用MultipartEntity,FileBody,要使用這個類對象的話,我們需要導入相關jar包,在此我使用的是httpmine-4.1.3.jar。可能有些人說了,爲何廢棄了,還要用,不要問爲什麼,因爲我也不知道,哈哈,其實是懶,主要是公司老項目用的是這個,還沒準備大動,所以就在這基礎上做的。當然後期肯定要使用最新最流行的的技術,暫時未考慮(寫文章的時候正在學習Retrofit+RxJava,也學的已經差不多了,入了門道,準備開刀)。

Demo運行圖

這裏寫圖片描述

文件上傳分析

在分析文件分塊上傳之前我們先來介紹如何直接上傳單個文件。在Android中的apache包中有一個HttpClient的默認實現類DefaultHttpClient,在上傳的時候我們需要指定上傳方式如是GET,POST等請求方式,而在apache包中提供了了對應的HttpPost,HttpGet.在這裏我們使用POST請求。如下代碼

        MultipartEntity mpEntity=new MultipartEntity();
        try {
            mpEntity.addPart("md5", new StringBody(chunkInfo.getMd5()));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        FileBody fileBody = new FileBody(new File(chunkInfo.getFilePath()));
        mpEntity.addPart("file", fileBody);
        HttpPost post = new HttpPost(actionUrl);
        // 發送請求體
        post.setEntity(mpEntity);
        DefaultHttpClient dhc = new DefaultHttpClient();
        try {
            dhc.getParams().setParameter(
                    CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
            HttpResponse response = dhc.execute(post);
            int res = response.getStatusLine().getStatusCode();
            Log.e("圖片上傳返回響應碼", res + ",");
            switch (res) {
                case 200:
                    //流形式獲得
                    StringBuilder builder = new StringBuilder();
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
                    for (String s = bufferedReader.readLine(); s != null; s = bufferedReader.readLine()) {
                        builder.append(s);
                    }
                    retMsg = builder.toString();
                    break;
                case 404:
                    retMsg = "-1";
                    break;
                default:
                    retMsg = "500";
            }

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

很簡單,通過MultipartEntity,FileBody就可以實現文件上傳了。上面的代碼很簡單,當然如果想展示上傳進度的話,我們只需要寫個類繼承FilterOutputStream,就可以自己寫個監聽回調展示進度,然後再發個廣播更新UI,詳細代碼不貼了,可點擊一鍵直達查看。

在上傳整個文件的時候我們看到主要用到的是FileBody,那麼我們就可以從這個地方入手,實現文件分塊上傳。通過源碼寫文件主要是通過writeTo()方法實現的

    /** @deprecated */
    @Deprecated
    public void writeTo(OutputStream out, int mode) throws IOException {
        this.writeTo(out);
    }

    public void writeTo(OutputStream out) throws IOException {
        if(out == null) {
            throw new IllegalArgumentException("Output stream may not be null");
        } else {
            FileInputStream in = new FileInputStream(this.file);

            try {
                byte[] tmp = new byte[4096];

                int l;
                while((l = in.read(tmp)) != -1) {
                    out.write(tmp, 0, l);
                }

                out.flush();
            } finally {
                in.close();
            }
        }
    }

看到writeTo方法的具體實現後你就知道了,通過while((l = in.read(tmp)) != -1)判斷並循環讀取文件到輸出流。那麼既然我們是講文件分塊上傳,我們可以讀取文件的一部分就可以了這樣就可以實現分塊上傳了。

文件分塊分析

對於文件的從指定位置讀取指定大小數據,我用了RandomAccessFile對文件隨機讀取,通過seek()方法指定讀取的起始位置
假如我們我們的文件是長度大小fileLength,我們將分塊大小是chunkLength.那麼我們分塊數量計算爲

int chunks=(int)(fileLength/chunkLength+(fileLength%chunkLength>0?1:0));

這樣我們就計算了分塊總數,則我們可以計算我們每一次上傳的塊的起始位置如下

offset=chunk*chunkLength;//我們服務器將第一塊爲0塊,如果你的服務接口設的是從1開始,那就是offset就爲(chunk-1)*chunkLength;

計算出了offset,我們上傳每一塊只需要執行代碼randomAccessFile.seek(chunk*chunkLength);即可,然後讀取chunkLength長度的數據。
好了,代碼來了

自定義FileBody

/**
 * Created by xiehui on 2016/10/13.
 */
public class CustomFileBody extends AbstractContentBody {
    private File file = null;
    private int chunk = 0;  //第幾個分片
    private int chunks = 1;  //總分片數
    private int chunkLength = 1024 * 1024 * 1; //分片大小1MB
    public CustomFileBody(File file) {
        this(file, "application/octet-stream");
    }
    public CustomFileBody(ChunkInfo chunkInfo) {
        this(new File(chunkInfo.getFilePath()), "application/octet-stream");
        this.chunk = chunkInfo.getChunk();
        this.chunks = chunkInfo.getChunks();
        this.file = new File(chunkInfo.getFilePath());
        if (this.chunk == this.chunks) {
            //先不判斷,固定1M
            //this.chunkLength=this.file.length()-(this)
        }
    }
    public CustomFileBody(File file, String mimeType) {
        super(mimeType);
        if (file == null) {
            throw new IllegalArgumentException("File may not be null");
        } else {
            this.file = file;
        }
    }
    @Override
    public String getFilename() {
        return this.file.getName();
    }

    @Override
    public String getCharset() {
        return null;
    }

    public InputStream getInputStream() throws IOException {
        return new FileInputStream(this.file);
    }

    @Override
    public String getTransferEncoding() {
        return "binary";
    }

    @Override
    public long getContentLength() {
        return chunkLength;
    }

    @Override
    public void writeTo(OutputStream out) throws IOException {
        if (out == null) {
            throw new IllegalArgumentException("Output stream may not be null");
        } else {
            //不使用FileInputStream
            RandomAccessFile randomAccessFile = new RandomAccessFile(this.file, "r");
            try {
                //int size = 1024 * 1;//1KB緩衝區讀取數據
                byte[] tmp = new byte[1024];
                //randomAccessFile.seek(chunk * chunkLength);
                if (chunk+1<chunks){//中間分片
                    randomAccessFile.seek(chunk*chunkLength);
                    int n = 0;
                    long readLength = 0;//記錄已讀字節數
                    while (readLength <= chunkLength - 1024) {
                        n = randomAccessFile.read(tmp, 0, 1024);
                        readLength += 1024;
                        out.write(tmp, 0, n);
                    }
                    if (readLength <= chunkLength) {
                        n = randomAccessFile.read(tmp, 0, (int)(chunkLength - readLength));
                        out.write(tmp, 0, n);
                    }
                }else{
                    randomAccessFile.seek(chunk*chunkLength);
                    int n = 0;
                    while ((n = randomAccessFile.read(tmp, 0, 1024)) != -1) {
                        out.write(tmp, 0, n);
                    }
                }
                out.flush();
            } finally {
                randomAccessFile.close();
            }
        }
    }

    public File getFile() {
        return this.file;
    }
}

文件分塊上傳模型類ChunkInfo

 * Created by xiehui on 2016/10/21.
 */
public class ChunkInfo  extends FileInfo implements Serializable{
    /**
     * 文件的當前分片值
     */
    private int chunk=1;
    /**
     * 文件總分片值
     */
    private int chunks=1;
    /**
     * 下載進度值
     */
    private int progress=1;

    public int getChunks() {
        return chunks;
    }

    public void setChunks(int chunks) {
        this.chunks = chunks;
    }

    public int getChunk() {
        return chunk;
    }

    public void setChunk(int chunk) {
        this.chunk = chunk;
    }

    public int getProgress() {
        return progress;
    }

    public void setProgress(int progress) {
        this.progress = progress;
    }

    @Override
    public String toString() {
        return "ChunkInfo{" +
                "chunk=" + chunk +
                ", chunks=" + chunks +
                ", progress=" + progress +
                '}';
    }
}

具體上傳實現

 public String uploadFile() {
        String retMsg = "1";
        CustomMultipartEntity mpEntity = new CustomMultipartEntity(
                new CustomMultipartEntity.ProgressListener() {
                    @Override
                    public void transferred(long num) {
                        Intent intent2 = new Intent();
                        ChunkInfo chunkIntent = new ChunkInfo();
                        chunkIntent.setChunks(chunkInfo.getChunks());
                        chunkIntent.setChunk(chunkInfo.getChunk());
                        chunkIntent.setProgress((int) num);
                        intent2.putExtra("chunkIntent", chunkIntent);
                        intent2.setAction("ACTION_UPDATE");
                        context.sendBroadcast(intent2);
                    }
                });
        try {
            mpEntity.addPart("chunk", new StringBody(chunkInfo.getChunk() + ""));
            mpEntity.addPart("chunks", new StringBody(chunkInfo.getChunks() + ""));
             mpEntity.addPart("fileLength", new StringBody(chunkInfo.getFileLength()));
            mpEntity.addPart("md5", new StringBody(chunkInfo.getMd5()));

        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        CustomFileBody customFileBody = new CustomFileBody(chunkInfo);
        mpEntity.addPart("file", customFileBody);
        HttpPost post = new HttpPost(actionUrl);
        // 發送請求體
        post.setEntity(mpEntity);
        DefaultHttpClient dhc = new DefaultHttpClient();
        try {
            dhc.getParams().setParameter(
                    CoreConnectionPNames.CONNECTION_TIMEOUT, 10000);
            HttpResponse response = dhc.execute(post);
            int res = response.getStatusLine().getStatusCode();
            switch (res) {
                case 200:
                    //流形式獲得
                    StringBuilder builder = new StringBuilder();
                    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
                    for (String s = bufferedReader.readLine(); s != null; s = bufferedReader.readLine()) {
                        builder.append(s);
                    }
                    retMsg = builder.toString();
                    break;
                case 404:
                    retMsg = "-1";
                    break;
                default:
                    retMsg = "500";
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retMsg;

    }

到此文件分塊上傳已基本完畢。那麼此時你可能會問秒傳的實現在哪了呢?別激動,在前面的分析中我們上傳的參數有一個是md5,我們上傳文件後將此值保存在數據庫,以及圖片的url鏈接,那麼當我們上傳文件之前先通過這個調用一個接口並上傳參數md5,服務接口查詢數據庫是否有此md5的文件,如果有的話,直接將圖片url返回即可,此時就提示用戶文件上傳成功,如果數據庫沒有此md5文件,則上傳文件。

接口延伸

由於客戶端上傳的是文件塊,當最後一塊上傳完成後,如果接口是每一分塊保存了一個臨時文件,則需要對分塊的文件進行合併及刪除。這個服務器FileChannel進行進行讀寫,當然也可以使用RandomAccessFile,因爲我們上傳了文件的總大小,則接口接收到分塊文件時直接創建一個文件並調用randomAccessFile.setLength();方法設置長度,之後通過上傳的seek方法在指定位置寫入數據到文件即可。

到此,本篇文章真的結束了,若文章有不足或者錯誤的地方,歡迎指正,以防止給其他讀者錯誤引導

發佈了47 篇原創文章 · 獲贊 42 · 訪問量 35萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章