您好,欢迎来到源码搜藏网!分享精神,快乐你我!
[加入VIP] 设为首页 | 收藏本站 | 网站地图 | Sitemap | TAG标签
  • 首 页
  • 在线工具
  • jquery手册
  • 当前位置:首页 > 安卓源码 > 技术博客 >

    Android大文件上传技巧之实战篇

    时间:2018-02-01 17:44 来源:互联网 作者:源码搜藏 浏览:收藏 挑错 推荐 打印

    在上一篇文章我们介绍了获取大文件的一个唯一的特征值MD5,通过MD5我们可以唯一的标识一个文件,并可以实现秒传效果,今天的这篇文章主要介绍大文件的上传操作,当然谈到上传文件,网络是必不可少的,现在也有很多较为流行的网络框架,如排球,OkHttp,改造.

    在上一篇文章我们介绍了获取大文件的一个唯一的特征值MD5,通过MD5我们可以唯一的标识一个文件,并可以实现秒传效果,今天的这篇文章主要介绍大文件的上传操作,当然谈到上传文件,网络是必不可少的,现在也有很多较为流行的网络框架,如排球,OkHttp,改造..而今天的这篇文章是采用最原始的上传文件的方法,通过HttpClient的上传文件的方式。

    HttpClient API

    在API 23(6.0系统)之前,HttpClient类是Android API中本身自带的方法,但是在23以及以后的版本中谷歌放弃了HttpClient,如果想要使用需要在gradle文件中加上下面代码

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

    加入上面的代码后,我们构建一下就可以API23及以后版本中可以继续使用HttpClient的,在使用HttpClient的上传文件时可以使用MultipartEntity,FileBody,要使用这个类对象的话,我们需要导入相关的jar包,在此我使用的是httpmine-4.1.3.jar。可能有些人说了,为何废弃了,还要用,不要问为什么,因为我也不知道,哈哈,其实是懒,主要是公司老项目用的是这个,还没准备大动,所以就在这基础上做的。当然后期肯定要使用最新最流行的的技术,暂时未考虑(写文章的时候正在学习改造+ RxJava,也学的已经差不多了,入了门道,准备开刀)。

    演示运行图

    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();
            }
            

    查看。

    在上传整个文件的时候我们看到主要用到的是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对文件随机读取,通过求()指定方法的读取起始位置
    假如我们我们的文件的英文长度大小文件长度,我们将分块大小是chunkLength。那么我们分块数量计算为

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

    这样我们就计算了分块总数,则我们可以计算我们每一次上传的块的起始位置如下

    offset=chunk*chunkLength;//我们服务器将第一块为0块,如果你的服务接口设的是从1开始,那就是offset就为(chunk-1)*chunkLength;

    计算出了偏移,我们上传每一块只需要执行代码 randomAccessFile.seek (块* 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();方法设置长度,之后通过上传的寻求方法在指定位置写入数据到文件即可。

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

    Android大文件上传技巧之实战篇转载http://www.codesocang.com/appboke/38421.html
    标签:网站源码