Android大文件上传秒传之实战篇

picture.png

源码传送门


在上一篇文章我们介绍了获取大文件的一个唯一的特征值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方法在指定位置写入数据到文件即可。

到此,本篇文章真的结束了,若文章有不足或者错误的地方,欢迎指正,以防止给其他读者错误引导

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,510评论 25 707
  • 前言 现在越来越多的应用开始有上传大文件的需求,以及秒传,续传功能。由于最近学习大文件分隔上传,以及秒传的实现,给...
    Code4Android阅读 4,160评论 2 52
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 风去来兮染眼眉,佳人一去独留香。 盼伊何时归相逢,抚琴弹曲相思泪。 红装惹人千百回,墨笔勾勒如娇媚。 举杯新人饮青...
    兴海客阅读 153评论 0 2
  • 开学第一课 ——我上九年级啦 踏着九月的节拍,我们又走回了校园,开始了一个新的起点——九年级。九年级意味着初中的脚...
    珍珠雨8810阅读 192评论 0 0